package main import ( "flag" "fmt" "os" "sort" "sync" "modify/utils" "github.com/go-git/go-git/v5" "modify/logger" ) type GlobalStats struct { TotalMatches int TotalModifications int ProcessedFiles int FailedFiles int } var ( repo *git.Repository worktree *git.Worktree stats GlobalStats = 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, " -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, " -loglevel string\n") fmt.Fprintf(os.Stderr, " Set logging level: ERROR, WARNING, INFO, DEBUG, TRACE (default \"INFO\")\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, "\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() level := logger.ParseLevel(*utils.LogLevel) logger.Init(level) logger.Info("Initializing with log level: %s", level.String()) // The plan is: // Load all commands commands, err := utils.LoadCommands(args) if err != nil { logger.Error("Failed to load commands: %v", err) flag.Usage() return } // Then aggregate all the globs and deduplicate them globs := utils.AggregateGlobs(commands) // Resolve all the files for all the globs logger.Info("Found %d unique file patterns", len(globs)) files, err := utils.ExpandGLobs(globs) if err != nil { logger.Error("Failed to expand file patterns: %v", err) return } logger.Info("Found %d files to process", len(files)) // Somehow connect files to commands via globs.. // For each file check every glob of every command // Maybe memoize this part // That way we know what commands affect what files associations, err := utils.AssociateFilesWithCommands(files, commands) if err != nil { logger.Error("Failed to associate files with commands: %v", err) return } // TODO: Utilize parallel workers for this // Then for each file run all commands associated with the file workers := make(chan struct{}, *utils.ParallelFiles) wg := sync.WaitGroup{} for file, commands := range associations { workers <- struct{}{} wg.Add(1) go func(file string, commands []utils.ModifyCommand) { defer func() { <-workers }() defer wg.Done() fileData, err := os.ReadFile(file) if err != nil { logger.Error("Failed to read file %q: %v", file, err) return } logger.Trace("Loaded %d bytes of data for file %q", len(fileData), file) fileDataStr := string(fileData) // Aggregate all the modifications and execute them modifications := []utils.ReplaceCommand{} for _, command := range commands { logger.Info("Processing file %q with command %q", file, command.Pattern) // TODO: Run processor and return modifications } // Sort commands in reverse order for safe replacements sort.Slice(modifications, func(i, j int) bool { return modifications[i].From > modifications[j].From }) logger.Trace("Applying %d replacement commands in reverse order", len(modifications)) fileDataStr, count := utils.ExecuteModifications(modifications, fileDataStr) logger.Info("Executed %d modifications for file %q", count, file) err = os.WriteFile(file, []byte(fileDataStr), 0644) if err != nil { logger.Error("Failed to write file %q: %v", file, err) return } }(file, commands) } // This will also relieve processor of some of the file loading // But we will also have to rework the tests....... // Also give each command its own logger, maybe prefix it with something... Maybe give commands a name? // Do that with logger.WithField("loglevel", level.String()) // Since each command also has its own log level // TODO: Maybe even figure out how to run individual commands...? // TODO: What to do with git? Figure it out .... // if *gitFlag { // logger.Info("Git integration enabled, setting up git repository") // err := setupGit() // if err != nil { // logger.Error("Failed to setup git: %v", err) // fmt.Fprintf(os.Stderr, "Error setting up git: %v\n", err) // return // } // } // logger.Debug("Expanding file patterns") // files, err := expandFilePatterns(filePatterns) // if err != nil { // logger.Error("Failed to expand file patterns: %v", err) // fmt.Fprintf(os.Stderr, "Error expanding file patterns: %v\n", err) // return // } // if len(files) == 0 { // logger.Warning("No files found matching the specified patterns") // fmt.Fprintf(os.Stderr, "No files found matching the specified patterns\n") // return // } // if *gitFlag { // logger.Info("Cleaning up git files before processing") // err := cleanupGitFiles(files) // if err != nil { // logger.Error("Failed to cleanup git files: %v", err) // fmt.Fprintf(os.Stderr, "Error cleaning up git files: %v\n", err) // return // } // } // if *resetFlag { // logger.Info("Files reset to their original state, nothing more to do") // log.Printf("Files reset to their original state, nothing more to do") // return // } // Process each file // for _, file := range files { // wg.Add(1) // logger.SafeGoWithArgs(func(args ...interface{}) { // defer wg.Done() // fileToProcess := args[0].(string) // logger.Debug("Processing file: %s", fileToProcess) // // 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, fileToProcess, pattern, luaExpr) // if err != nil { // logger.Error("Failed to process file %s: %v", fileToProcess, err) // fmt.Fprintf(os.Stderr, "Failed to process file %s: %v\n", fileToProcess, err) // stats.FailedFiles++ // } else { // if modCount > 0 { // logger.Info("Successfully processed file %s: %d modifications from %d matches", // fileToProcess, modCount, matchCount) // } else if matchCount > 0 { // logger.Info("Found %d matches in file %s but made no modifications", // matchCount, fileToProcess) // } else { // logger.Debug("No matches found in file: %s", fileToProcess) // } // stats.ProcessedFiles++ // stats.TotalMatches += matchCount // stats.TotalModifications += modCount // } // }, file) // } // wg.Wait() // Print summary if stats.TotalModifications == 0 { logger.Warning("No modifications were made in any files") fmt.Fprintf(os.Stderr, "No modifications were made in any files\n") } else { logger.Info("Operation complete! Modified %d values in %d/%d files", stats.TotalModifications, stats.ProcessedFiles, stats.ProcessedFiles+stats.FailedFiles) fmt.Printf("Operation complete! Modified %d values in %d/%d files\n", stats.TotalModifications, stats.ProcessedFiles, stats.ProcessedFiles+stats.FailedFiles) } }