package main import ( "flag" "fmt" "log" "os" "path/filepath" "sync" "time" "github.com/bmatcuk/doublestar/v4" "github.com/go-git/go-git/v5" "github.com/go-git/go-git/v5/plumbing/object" "modify/processor" ) type GlobalStats struct { TotalMatches int TotalModifications int ProcessedFiles int FailedFiles int } var stats GlobalStats var logger *log.Logger var ( jsonFlag = flag.Bool("json", false, "Process JSON files") xmlFlag = flag.Bool("xml", false, "Process XML files") gitFlag = flag.Bool("git", false, "Use git to manage files") resetFlag = flag.Bool("reset", false, "Reset files to their original state") repo *git.Repository worktree *git.Worktree ) func init() { log.SetFlags(log.Lmicroseconds | log.Lshortfile) logger = log.New(os.Stdout, "", log.Lmicroseconds|log.Lshortfile) stats = GlobalStats{} } func main() { // TODO: Implement some sort of git integration // Maybe use go-git // Specify a -git flag // If we are operating with git then: // Inmitialize a repo if one doesn't exist (try to open right?) // For each file matched by glob first figure out if it's already tracked // If not tracked then track it and commit (either it alone or maybe multiple together somehow) // Then reset the file (to undo previous modifications) // THEN change the file // In addition add a -undo flag that will ONLY reset the files without changing them // Only for the ones matched by glob // ^ important because binary files would fuck us up flag.Usage = func() { fmt.Fprintf(os.Stderr, "Usage: %s [options] <...files_or_globs>\n", os.Args[0]) fmt.Fprintf(os.Stderr, "\nOptions:\n") fmt.Fprintf(os.Stderr, " -json\n") fmt.Fprintf(os.Stderr, " Process JSON files\n") fmt.Fprintf(os.Stderr, " -xml\n") fmt.Fprintf(os.Stderr, " Process XML files\n") fmt.Fprintf(os.Stderr, " -git\n") fmt.Fprintf(os.Stderr, " Use git to manage files\n") fmt.Fprintf(os.Stderr, " -reset\n") fmt.Fprintf(os.Stderr, " Reset files to their original state\n") fmt.Fprintf(os.Stderr, " -mode string\n") fmt.Fprintf(os.Stderr, " Processing mode: regex, xml, json (default \"regex\")\n") fmt.Fprintf(os.Stderr, "\nExamples:\n") fmt.Fprintf(os.Stderr, " Regex mode (default):\n") fmt.Fprintf(os.Stderr, " %s \"(\\d+)\" \"*1.5\" data.xml\n", os.Args[0]) fmt.Fprintf(os.Stderr, " XML mode:\n") fmt.Fprintf(os.Stderr, " %s -xml \"//value\" \"*1.5\" data.xml\n", os.Args[0]) fmt.Fprintf(os.Stderr, " JSON mode:\n") fmt.Fprintf(os.Stderr, " %s -json \"$.items[*].value\" \"*1.5\" data.json\n", os.Args[0]) fmt.Fprintf(os.Stderr, "\nNote: v1, v2, etc. are used to refer to capture groups as numbers.\n") fmt.Fprintf(os.Stderr, " s1, s2, etc. are used to refer to capture groups as strings.\n") fmt.Fprintf(os.Stderr, " Helper functions: num(str) converts string to number, str(num) converts number to string\n") fmt.Fprintf(os.Stderr, " is_number(str) checks if a string is numeric\n") fmt.Fprintf(os.Stderr, " For XML and JSON, the captured values are exposed as 'v', which can be of any type we capture (string, number, table).\n") fmt.Fprintf(os.Stderr, " If expression starts with an operator like *, /, +, -, =, etc., v1 is automatically prepended\n") fmt.Fprintf(os.Stderr, " You can use any valid Lua code, including if statements, loops, etc.\n") fmt.Fprintf(os.Stderr, " Glob patterns are supported for file selection (*.xml, data/**.xml, etc.)\n") } flag.Parse() args := flag.Args() if *resetFlag { *gitFlag = true } if len(args) < 3 { log.Printf("At least %d arguments are required", 3) flag.Usage() return } // Get the appropriate pattern and expression based on mode var pattern, luaExpr string var filePatterns []string pattern = args[0] luaExpr = args[1] filePatterns = args[2:] // Prepare the Lua expression originalLuaExpr := luaExpr luaExpr = processor.BuildLuaScript(luaExpr) if originalLuaExpr != luaExpr { logger.Printf("Transformed Lua expression from %q to %q", originalLuaExpr, luaExpr) } if *gitFlag { err := setupGit() if err != nil { fmt.Fprintf(os.Stderr, "Error setting up git: %v\n", err) return } } // Expand file patterns with glob support files, err := expandFilePatterns(filePatterns) if err != nil { fmt.Fprintf(os.Stderr, "Error expanding file patterns: %v\n", err) return } if len(files) == 0 { fmt.Fprintf(os.Stderr, "No files found matching the specified patterns\n") return } if *gitFlag { err := cleanupGitFiles(files) if err != nil { fmt.Fprintf(os.Stderr, "Error cleaning up git files: %v\n", err) return } } if *resetFlag { log.Printf("Files reset to their original state, nothing more to do") return } // Create the processor based on mode var proc processor.Processor switch { case *xmlFlag: proc = &processor.XMLProcessor{} logger.Printf("Starting XML modifier with XPath %q, expression %q on %d files", pattern, luaExpr, len(files)) case *jsonFlag: proc = &processor.JSONProcessor{} logger.Printf("Starting JSON modifier with JSONPath %q, expression %q on %d files", pattern, luaExpr, len(files)) default: proc = &processor.RegexProcessor{} logger.Printf("Starting regex modifier with pattern %q, expression %q on %d files", pattern, luaExpr, len(files)) } var wg sync.WaitGroup // Process each file for _, file := range files { wg.Add(1) go func(file string) { defer wg.Done() logger.Printf("Processing file: %s", file) // It's a bit fucked, maybe I could do better to call it from proc... But it'll do for now modCount, matchCount, err := processor.Process(proc, file, pattern, luaExpr) if err != nil { fmt.Fprintf(os.Stderr, "Failed to process file %s: %v\n", file, err) stats.FailedFiles++ } else { logger.Printf("Successfully processed file: %s", file) stats.ProcessedFiles++ stats.TotalMatches += matchCount stats.TotalModifications += modCount } }(file) } wg.Wait() // Print summary if stats.TotalModifications == 0 { fmt.Fprintf(os.Stderr, "No modifications were made in any files\n") } else { fmt.Printf("Operation complete! Modified %d values in %d/%d files\n", stats.TotalModifications, stats.ProcessedFiles, stats.ProcessedFiles+stats.FailedFiles) } } func setupGit() error { cwd, err := os.Getwd() if err != nil { return fmt.Errorf("failed to get current working directory: %w", err) } logger.Printf("Current working directory obtained: %s", cwd) logger.Printf("Attempting to open git repository at %s", cwd) repo, err = git.PlainOpen(cwd) if err != nil { logger.Printf("No existing git repository found at %s, attempting to initialize a new git repository.", cwd) repo, err = git.PlainInit(cwd, false) if err != nil { return fmt.Errorf("failed to initialize a new git repository at %s: %w", cwd, err) } logger.Printf("Successfully initialized a new git repository at %s", cwd) } else { logger.Printf("Successfully opened existing git repository at %s", cwd) } logger.Printf("Attempting to obtain worktree for repository at %s", cwd) worktree, err = repo.Worktree() if err != nil { return fmt.Errorf("failed to obtain worktree for repository at %s: %w", cwd, err) } logger.Printf("Successfully obtained worktree for repository at %s", cwd) return nil } func expandFilePatterns(patterns []string) ([]string, error) { var files []string filesMap := make(map[string]bool) for _, pattern := range patterns { matches, _ := doublestar.Glob(os.DirFS("."), pattern) for _, m := range matches { if info, err := os.Stat(m); err == nil && !info.IsDir() && !filesMap[m] { filesMap[m], files = true, append(files, m) } } } if len(files) > 0 { logger.Printf("Found %d files to process", len(files)) } return files, nil } func cleanupGitFiles(files []string) error { for _, file := range files { logger.Printf("Checking file: %s", file) status, err := worktree.Status() if err != nil { fmt.Fprintf(os.Stderr, "Error getting worktree status: %v\n", err) return fmt.Errorf("error getting worktree status: %w", err) } if status.IsUntracked(file) { logger.Printf("Detected untracked file: %s. Attempting to add it to the git index.", file) _, err = worktree.Add(file) if err != nil { fmt.Fprintf(os.Stderr, "Error adding file to git: %v\n", err) return fmt.Errorf("error adding file to git: %w", err) } filename := filepath.Base(file) logger.Printf("File %s added successfully. Now committing it with message: 'Track %s'", filename, filename) _, err = worktree.Commit("Track "+filename, &git.CommitOptions{ Author: &object.Signature{ Name: "Big Chef", Email: "bigchef@bigchef.com", When: time.Now(), }, }) if err != nil { fmt.Fprintf(os.Stderr, "Error committing file: %v\n", err) return fmt.Errorf("error committing file: %w", err) } logger.Printf("Successfully committed file: %s with message: 'Track %s'", filename, filename) } else { logger.Printf("File %s is already tracked. Restoring it to the working tree.", file) err := worktree.Restore(&git.RestoreOptions{ Files: []string{file}, Staged: true, Worktree: true, }) if err != nil { fmt.Fprintf(os.Stderr, "Error restoring file: %v\n", err) return fmt.Errorf("error restoring file: %w", err) } logger.Printf("File %s restored successfully.", file) } } return nil }