package main import ( "flag" "fmt" "io" "log" "os" "regexp" "strings" "sync" "github.com/bmatcuk/doublestar/v4" "modify/processor" ) var Error *log.Logger var Warning *log.Logger var Info *log.Logger var Success *log.Logger // GlobalStats tracks all modifications across files type GlobalStats struct { TotalMatches int TotalModifications int Modifications []processor.ModificationRecord ProcessedFiles int FailedFiles int } // FileMode defines how we interpret and process files type FileMode string const ( ModeRegex FileMode = "regex" // Default mode using regex ModeXML FileMode = "xml" // XML mode using XPath ModeJSON FileMode = "json" // JSON mode using JSONPath ) var stats GlobalStats func init() { // Configure standard logging to be hidden by default log.SetFlags(log.Lmicroseconds | log.Lshortfile) log.SetOutput(io.Discard) // Disable default logging to stdout // Set up custom loggers with different severity levels Error = log.New(io.MultiWriter(os.Stderr, os.Stdout), fmt.Sprintf("%sERROR:%s ", "\033[0;101m", "\033[0m"), log.Lmicroseconds|log.Lshortfile) Warning = log.New(os.Stdout, fmt.Sprintf("%sWarning:%s ", "\033[0;93m", "\033[0m"), log.Lmicroseconds|log.Lshortfile) Info = log.New(os.Stdout, fmt.Sprintf("%sInfo:%s ", "\033[0;94m", "\033[0m"), log.Lmicroseconds|log.Lshortfile) Success = log.New(os.Stdout, fmt.Sprintf("%s✨ SUCCESS:%s ", "\033[0;92m", "\033[0m"), log.Lmicroseconds|log.Lshortfile) // Initialize global stats stats = GlobalStats{ Modifications: make([]processor.ModificationRecord, 0), } } func main() { // Define flags fileModeFlag := flag.String("mode", "regex", "Processing mode: regex, xml, json") xpathFlag := flag.String("xpath", "", "XPath expression (for XML mode)") jsonpathFlag := flag.String("jsonpath", "", "JSONPath expression (for JSON mode)") verboseFlag := flag.Bool("verbose", false, "Enable verbose output") 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() // Set up verbose mode if !*verboseFlag { // If not verbose, suppress Info level logs Info.SetOutput(io.Discard) } args := flag.Args() requiredArgCount := 3 // Default for regex mode // XML/JSON modes need one fewer positional argument if *fileModeFlag == "xml" || *fileModeFlag == "json" { requiredArgCount = 2 } if len(args) < requiredArgCount { Error.Printf("%s mode requires %d arguments minimum", *fileModeFlag, requiredArgCount) flag.Usage() return } // Validate mode-specific parameters if *fileModeFlag == "xml" && *xpathFlag == "" { Error.Printf("XML mode requires an XPath expression with -xpath flag") return } if *fileModeFlag == "json" && *jsonpathFlag == "" { Error.Printf("JSON mode requires a JSONPath expression with -jsonpath flag") return } // Get the appropriate pattern and expression based on mode var regexPattern string var luaExpr string var filePatterns []string // In regex mode, we need both pattern arguments // In XML/JSON modes, we only need the lua expression from args if *fileModeFlag == "regex" { regexPattern = args[0] luaExpr = args[1] filePatterns = args[2:] // Process files with regex mode processFilesWithRegex(regexPattern, luaExpr, filePatterns) } else { // XML/JSON modes luaExpr = args[0] filePatterns = args[1:] // Prepare the Lua expression originalLuaExpr := luaExpr luaExpr = processor.BuildLuaScript(luaExpr) if originalLuaExpr != luaExpr { Info.Printf("Transformed Lua expression from '%s' to '%s'", originalLuaExpr, luaExpr) } // Expand file patterns with glob support files, err := expandFilePatterns(filePatterns) if err != nil { Error.Printf("Error expanding file patterns: %v", err) return } if len(files) == 0 { Error.Printf("No files found matching the specified patterns") return } // Create the processor based on mode var proc processor.Processor if *fileModeFlag == "xml" { Info.Printf("Starting XML modifier with XPath '%s', expression '%s' on %d files", *xpathFlag, luaExpr, len(files)) proc = processor.NewXMLProcessor(Info) } else { Info.Printf("Starting JSON modifier with JSONPath '%s', expression '%s' on %d files", *jsonpathFlag, luaExpr, len(files)) proc = processor.NewJSONProcessor(Info) } var wg sync.WaitGroup // Process each file for _, file := range files { wg.Add(1) go func(file string) { defer wg.Done() Info.Printf("šŸ”„ Processing file: %s", file) // Pass the appropriate path expression as the pattern var pattern string if *fileModeFlag == "xml" { pattern = *xpathFlag } else { pattern = *jsonpathFlag } modCount, matchCount, err := proc.Process(file, pattern, luaExpr, originalLuaExpr) if err != nil { Error.Printf("āŒ Failed to process file %s: %v", file, err) stats.FailedFiles++ } else { Info.Printf("āœ… Successfully processed file: %s", file) stats.ProcessedFiles++ stats.TotalMatches += matchCount stats.TotalModifications += modCount } }(file) } wg.Wait() } // Print summary of all modifications printSummary(luaExpr) } // processFilesWithRegex handles regex mode pattern processing for multiple files func processFilesWithRegex(regexPattern string, luaExpr string, filePatterns []string) { // Prepare the Lua expression originalLuaExpr := luaExpr luaExpr = processor.BuildLuaScript(luaExpr) if originalLuaExpr != luaExpr { Info.Printf("Transformed Lua expression from '%s' to '%s'", originalLuaExpr, luaExpr) } // Handle special pattern modifications originalPattern := regexPattern patternModified := false if strings.Contains(regexPattern, "!num") { regexPattern = strings.ReplaceAll(regexPattern, "!num", "(-?\\d*\\.?\\d+)") patternModified = true } // Make sure the regex can match across multiple lines by adding (?s) flag if !strings.HasPrefix(regexPattern, "(?s)") { regexPattern = "(?s)" + regexPattern patternModified = true } if patternModified { Info.Printf("Modified regex pattern from '%s' to '%s'", originalPattern, regexPattern) } // Compile the pattern for file processing pattern, err := regexp.Compile(regexPattern) if err != nil { Error.Printf("Invalid regex pattern '%s': %v", regexPattern, err) return } // Expand file patterns with glob support files, err := expandFilePatterns(filePatterns) if err != nil { Error.Printf("Error expanding file patterns: %v", err) return } if len(files) == 0 { Error.Printf("No files found matching the specified patterns") return } Info.Printf("Starting regex modifier with pattern '%s', expression '%s' on %d files", regexPattern, luaExpr, len(files)) // Create the regex processor proc := processor.NewRegexProcessor(pattern, Info) var wg sync.WaitGroup // Process each file for _, file := range files { wg.Add(1) go func(file string) { defer wg.Done() Info.Printf("šŸ”„ Processing file: %s", file) modCount, matchCount, err := proc.Process(file, regexPattern, luaExpr, originalLuaExpr) if err != nil { Error.Printf("āŒ Failed to process file %s: %v", file, err) stats.FailedFiles++ } else { Info.Printf("āœ… Successfully processed file: %s", file) stats.ProcessedFiles++ stats.TotalMatches += matchCount stats.TotalModifications += modCount } }(file) } wg.Wait() } // printSummary outputs a formatted summary of all modifications made func printSummary(operation string) { if stats.TotalModifications == 0 { Warning.Printf("No modifications were made in any files") return } Success.Printf("Operation complete! Modified %d values in %d/%d files using '%s'", stats.TotalModifications, stats.ProcessedFiles, stats.ProcessedFiles+stats.FailedFiles, operation) // Group modifications by file for better readability fileGroups := make(map[string][]processor.ModificationRecord) for _, mod := range stats.Modifications { fileGroups[mod.File] = append(fileGroups[mod.File], mod) } // Print modifications by file for file, mods := range fileGroups { Success.Printf("šŸ“„ %s: %d modifications", file, len(mods)) // Show some sample modifications (max 5 per file to avoid overwhelming output) maxSamples := 5 if len(mods) > maxSamples { for i := 0; i < maxSamples; i++ { mod := mods[i] Success.Printf(" %d. '%s' → '%s' %s", i+1, limitString(mod.OldValue, 20), limitString(mod.NewValue, 20), mod.Context) } Success.Printf(" ... and %d more modifications", len(mods)-maxSamples) } else { for i, mod := range mods { Success.Printf(" %d. '%s' → '%s' %s", i+1, limitString(mod.OldValue, 20), limitString(mod.NewValue, 20), mod.Context) } } } // Print a nice visual indicator of success if stats.TotalModifications > 0 { Success.Printf("šŸŽ‰ All done! Modified %d values successfully!", stats.TotalModifications) } } // limitString truncates a string to maxLen and adds "..." if truncated func limitString(s string, maxLen int) string { s = strings.ReplaceAll(s, "\n", "\\n") if len(s) <= maxLen { return s } return s[:maxLen-3] + "..." } 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 { Info.Printf("Found %d files to process", len(files)) } return files, nil }