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 } type FileMode string const ( ModeRegex FileMode = "regex" ModeXML FileMode = "xml" ModeJSON FileMode = "json" ) var stats GlobalStats var logger *log.Logger var ( fileModeFlag = flag.String("mode", "regex", "Processing mode: regex, xml, json") verboseFlag = flag.Bool("verbose", false, "Enable verbose output") ) func init() { log.SetFlags(log.Lmicroseconds | log.Lshortfile) logger = log.New(os.Stdout, "", log.Lmicroseconds|log.Lshortfile) stats = GlobalStats{} } func main() { 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, " -mode string\n") fmt.Fprintf(os.Stderr, " Processing mode: regex, xml, json (default \"regex\")\n") fmt.Fprintf(os.Stderr, " -xpath string\n") fmt.Fprintf(os.Stderr, " XPath expression (for XML mode)\n") fmt.Fprintf(os.Stderr, " -jsonpath string\n") fmt.Fprintf(os.Stderr, " JSONPath expression (for JSON mode)\n") fmt.Fprintf(os.Stderr, " -verbose\n") fmt.Fprintf(os.Stderr, " Enable verbose output\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 -mode=xml -xpath=\"//value\" \"*1.5\" data.xml\n", os.Args[0]) fmt.Fprintf(os.Stderr, " JSON mode:\n") fmt.Fprintf(os.Stderr, " %s -mode=json -jsonpath=\"$.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, " 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 { fmt.Fprintf(os.Stderr, "%s mode requires %d arguments minimum\n", *fileModeFlag, 3) flag.Usage() return } // Get the appropriate pattern and expression based on mode var pattern, luaExpr string var filePatterns []string if *fileModeFlag == "regex" { pattern = args[0] luaExpr = args[1] filePatterns = args[2:] } else { // For XML/JSON modes, pattern comes from flags luaExpr = args[0] filePatterns = args[1:] } // 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 *fileModeFlag { case "regex": proc = &processor.RegexProcessor{} logger.Printf("Starting regex modifier with pattern %q, expression %q on %d files", pattern, luaExpr, len(files)) // case "xml": // proc = &processor.XMLProcessor{} // pattern = *xpathFlag // logger.Printf("Starting XML modifier with XPath %q, expression %q on %d files", // pattern, luaExpr, len(files)) // case "json": // proc = &processor.JSONProcessor{} // pattern = *jsonpathFlag // logger.Printf("Starting JSON modifier with JSONPath %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) modCount, matchCount, err := proc.Process(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 }