package main import ( "flag" "fmt" "log" "os" "sync" "github.com/bmatcuk/doublestar/v4" "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") ) 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, " -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 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) } // 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 } // 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 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 }