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/logger" "modify/processor" ) type GlobalStats struct { TotalMatches int TotalModifications int ProcessedFiles int FailedFiles int } var stats GlobalStats var stdLogger *log.Logger // Legacy logger for compatibility 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") logLevel = flag.String("loglevel", "INFO", "Set log level: ERROR, WARNING, INFO, DEBUG, TRACE") repo *git.Repository worktree *git.Worktree ) func init() { // Keep standard logger setup for compatibility with legacy code log.SetFlags(log.Lmicroseconds | log.Lshortfile) stdLogger = 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, " -loglevel string\n") fmt.Fprintf(os.Stderr, " Set logging level: ERROR, WARNING, INFO, DEBUG, TRACE (default \"INFO\")\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() // Initialize logger with the specified log level level := logger.ParseLevel(*logLevel) logger.Init(level) logger.Info("Initializing with log level: %s", level.String()) args := flag.Args() if *resetFlag { *gitFlag = true } if len(args) < 3 { logger.Error("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.Debug("Transformed Lua expression from %q to %q", originalLuaExpr, luaExpr) } 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 } } // Expand file patterns with glob support logger.Debug("Expanding file patterns: %v", filePatterns) 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 } // Create the processor based on mode var proc processor.Processor switch { case *xmlFlag: proc = &processor.XMLProcessor{} logger.Info("Starting XML modifier with XPath %q, expression %q on %d files", pattern, luaExpr, len(files)) case *jsonFlag: proc = &processor.JSONProcessor{} logger.Info("Starting JSON modifier with JSONPath %q, expression %q on %d files", pattern, luaExpr, len(files)) default: proc = &processor.RegexProcessor{} logger.Info("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) 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) } } func setupGit() error { cwd, err := os.Getwd() if err != nil { return fmt.Errorf("failed to get current working directory: %w", err) } logger.Debug("Current working directory obtained: %s", cwd) logger.Debug("Attempting to open git repository at %s", cwd) repo, err = git.PlainOpen(cwd) if err != nil { logger.Debug("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.Info("Successfully initialized a new git repository at %s", cwd) } else { logger.Info("Successfully opened existing git repository at %s", cwd) } logger.Debug("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.Debug("Successfully obtained worktree for repository at %s", cwd) return nil } func expandFilePatterns(patterns []string) ([]string, error) { var files []string filesMap := make(map[string]bool) cwd, err := os.Getwd() if err != nil { return nil, fmt.Errorf("failed to get current working directory: %w", err) } logger.Debug("Expanding patterns from directory: %s", cwd) for _, pattern := range patterns { logger.Trace("Processing pattern: %s", pattern) matches, _ := doublestar.Glob(os.DirFS(cwd), pattern) logger.Debug("Found %d matches for pattern %s", len(matches), pattern) for _, m := range matches { info, err := os.Stat(m) if err != nil { logger.Warning("Error getting file info for %s: %v", m, err) continue } if !info.IsDir() && !filesMap[m] { logger.Trace("Adding file to process list: %s", m) filesMap[m], files = true, append(files, m) } } } if len(files) > 0 { logger.Debug("Found %d files to process: %v", len(files), files) } return files, nil } func cleanupGitFiles(files []string) error { for _, file := range files { logger.Debug("Checking git status for file: %s", file) status, err := worktree.Status() if err != nil { logger.Error("Error getting worktree status: %v", err) 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.Info("Detected untracked file: %s. Adding to git index.", file) _, err = worktree.Add(file) if err != nil { logger.Error("Error adding file to git: %v", err) 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.Info("File %s added successfully. Committing 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 { logger.Error("Error committing file: %v", err) fmt.Fprintf(os.Stderr, "Error committing file: %v\n", err) return fmt.Errorf("error committing file: %w", err) } logger.Info("Successfully committed file: %s", filename) } else { logger.Info("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 { logger.Error("Error restoring file: %v", err) fmt.Fprintf(os.Stderr, "Error restoring file: %v\n", err) return fmt.Errorf("error restoring file: %w", err) } logger.Info("File %s restored successfully", file) } } return nil }