|
|
|
|
@@ -3,8 +3,6 @@ package main
|
|
|
|
|
import (
|
|
|
|
|
_ "embed"
|
|
|
|
|
"errors"
|
|
|
|
|
"flag"
|
|
|
|
|
"fmt"
|
|
|
|
|
"os"
|
|
|
|
|
"sort"
|
|
|
|
|
"sync"
|
|
|
|
|
@@ -14,6 +12,7 @@ import (
|
|
|
|
|
"cook/processor"
|
|
|
|
|
"cook/utils"
|
|
|
|
|
|
|
|
|
|
"github.com/spf13/cobra"
|
|
|
|
|
logger "git.site.quack-lab.dev/dave/cylogger"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
@@ -37,52 +36,114 @@ var (
|
|
|
|
|
}
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
// rootCmd represents the base command when called without any subcommands
|
|
|
|
|
var rootCmd *cobra.Command
|
|
|
|
|
|
|
|
|
|
func init() {
|
|
|
|
|
rootCmd = &cobra.Command{
|
|
|
|
|
Use: "modifier [options] <pattern> <lua_expression> <...files_or_globs>",
|
|
|
|
|
Short: "A powerful file modification tool with Lua scripting",
|
|
|
|
|
Long: `Modifier is a powerful file processing tool that supports regex patterns,
|
|
|
|
|
JSON manipulation, and YAML to TOML conversion with Lua scripting capabilities.
|
|
|
|
|
|
|
|
|
|
Features:
|
|
|
|
|
- Regex-based pattern matching and replacement
|
|
|
|
|
- JSON file processing with query support
|
|
|
|
|
- YAML to TOML conversion
|
|
|
|
|
- Lua scripting for complex transformations
|
|
|
|
|
- Parallel file processing
|
|
|
|
|
- Command filtering and organization`,
|
|
|
|
|
PersistentPreRun: func(cmd *cobra.Command, args []string) {
|
|
|
|
|
CreateExampleConfig()
|
|
|
|
|
logger.InitFlag()
|
|
|
|
|
mainLogger.Info("Initializing with log level: %s", logger.GetLevel().String())
|
|
|
|
|
mainLogger.Trace("Full argv: %v", os.Args)
|
|
|
|
|
},
|
|
|
|
|
Run: func(cmd *cobra.Command, args []string) {
|
|
|
|
|
if len(args) == 0 {
|
|
|
|
|
cmd.Usage()
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
runModifier(args, cmd)
|
|
|
|
|
},
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Global flags
|
|
|
|
|
rootCmd.PersistentFlags().StringP("loglevel", "l", "INFO", "Set logging level: ERROR, WARNING, INFO, DEBUG, TRACE")
|
|
|
|
|
|
|
|
|
|
// Local flags
|
|
|
|
|
rootCmd.Flags().IntP("parallel", "P", 100, "Number of files to process in parallel")
|
|
|
|
|
rootCmd.Flags().StringP("filter", "f", "", "Filter commands before running them")
|
|
|
|
|
rootCmd.Flags().Bool("json", false, "Enable JSON mode for processing JSON files")
|
|
|
|
|
rootCmd.Flags().BoolP("conv", "c", false, "Convert YAML files to TOML format")
|
|
|
|
|
|
|
|
|
|
// Set up examples in the help text
|
|
|
|
|
rootCmd.SetUsageTemplate(`Usage:{{if .Runnable}}
|
|
|
|
|
{{.UseLine}}{{end}}{{if .HasAvailableSubCommands}}
|
|
|
|
|
{{.CommandPath}} [command]{{end}} {{if gt (len .Aliases) 0}}
|
|
|
|
|
|
|
|
|
|
Aliases:
|
|
|
|
|
{{.NameAndAliases}}{{end}}{{if .HasExample}}
|
|
|
|
|
|
|
|
|
|
Examples:
|
|
|
|
|
{{.Example}}{{end}}{{if .HasAvailableSubCommands}}
|
|
|
|
|
|
|
|
|
|
Available Commands:{{range .Commands}}{{if (or .IsAvailableCommand (eq .Name "help"))}}
|
|
|
|
|
{{rpad .Name .NamePadding }} {{.Short}}{{end}}{{end}}{{end}}{{if .HasAvailableLocalFlags}}
|
|
|
|
|
|
|
|
|
|
Flags:
|
|
|
|
|
{{.LocalFlags.FlagUsages | trimTrailingWhitespaces}}{{end}}{{if .HasAvailableInheritedFlags}}
|
|
|
|
|
|
|
|
|
|
Global Flags:
|
|
|
|
|
{{.InheritedFlags.FlagUsages | trimTrailingWhitespaces}}{{end}}{{if .HasHelpSubCommands}}
|
|
|
|
|
|
|
|
|
|
Additional help topics:{{range .Commands}}{{if .IsAdditionalHelpTopicCommand}}
|
|
|
|
|
{{rpad .CommandPath .CommandPathPadding}} {{.Short}}{{end}}{{end}}{{end}}{{if .HasAvailableSubCommands}}
|
|
|
|
|
|
|
|
|
|
Use "{{.CommandPath}} [command] --help" for more information about a command.{{end}}
|
|
|
|
|
`)
|
|
|
|
|
|
|
|
|
|
// Add examples
|
|
|
|
|
rootCmd.Example = ` Regex mode (default):
|
|
|
|
|
modifier "<value>(\\d+)</value>" "*1.5" data.xml
|
|
|
|
|
|
|
|
|
|
JSON mode:
|
|
|
|
|
modifier -json data.json
|
|
|
|
|
|
|
|
|
|
YAML to TOML conversion:
|
|
|
|
|
modifier -conv *.yml
|
|
|
|
|
modifier -conv **/*.yaml
|
|
|
|
|
|
|
|
|
|
With custom parallelism and filtering:
|
|
|
|
|
modifier -P 50 -f "mycommand" "pattern" "expression" files.txt
|
|
|
|
|
|
|
|
|
|
Note: v1, v2, etc. are used to refer to capture groups as numbers.
|
|
|
|
|
s1, s2, etc. are used to refer to capture groups as strings.
|
|
|
|
|
Helper functions: num(str) converts string to number, str(num) converts number to string
|
|
|
|
|
is_number(str) checks if a string is numeric
|
|
|
|
|
If expression starts with an operator like *, /, +, -, =, etc., v1 is automatically prepended
|
|
|
|
|
You can use any valid Lua code, including if statements, loops, etc.
|
|
|
|
|
Glob patterns are supported for file selection (*.xml, data/**.xml, etc.)
|
|
|
|
|
|
|
|
|
|
` + processor.GetLuaFunctionsHelp()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func main() {
|
|
|
|
|
flag.Usage = func() {
|
|
|
|
|
CreateExampleConfig()
|
|
|
|
|
fmt.Fprintf(os.Stderr, "Usage: %s [options] <pattern> <lua_expression> <...files_or_globs>\n", os.Args[0])
|
|
|
|
|
fmt.Fprintf(os.Stderr, "\nOptions:\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, " -json\n")
|
|
|
|
|
fmt.Fprintf(os.Stderr, " Enable JSON mode for processing JSON files\n")
|
|
|
|
|
fmt.Fprintf(os.Stderr, " -conv\n")
|
|
|
|
|
fmt.Fprintf(os.Stderr, " Convert YAML files to TOML format\n")
|
|
|
|
|
fmt.Fprintf(os.Stderr, "\nExamples:\n")
|
|
|
|
|
fmt.Fprintf(os.Stderr, " Regex mode (default):\n")
|
|
|
|
|
fmt.Fprintf(os.Stderr, " %s \"<value>(\\\\d+)</value>\" \"*1.5\" data.xml\n", os.Args[0])
|
|
|
|
|
fmt.Fprintf(os.Stderr, " JSON mode:\n")
|
|
|
|
|
fmt.Fprintf(os.Stderr, " %s -json data.json\n", os.Args[0])
|
|
|
|
|
fmt.Fprintf(os.Stderr, " YAML to TOML conversion:\n")
|
|
|
|
|
fmt.Fprintf(os.Stderr, " %s -conv *.yml\n", os.Args[0])
|
|
|
|
|
fmt.Fprintf(os.Stderr, " %s -conv **/*.yaml\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")
|
|
|
|
|
fmt.Fprintf(os.Stderr, "\nLua Functions Available:\n")
|
|
|
|
|
fmt.Fprintf(os.Stderr, "%s\n", processor.GetLuaFunctionsHelp())
|
|
|
|
|
if err := rootCmd.Execute(); err != nil {
|
|
|
|
|
mainLogger.Error("Command execution failed: %v", err)
|
|
|
|
|
os.Exit(1)
|
|
|
|
|
}
|
|
|
|
|
// TODO: Fix bed shitting when doing *.yml in barotrauma directory
|
|
|
|
|
flag.Parse()
|
|
|
|
|
args := flag.Args()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
logger.InitFlag()
|
|
|
|
|
mainLogger.Info("Initializing with log level: %s", logger.GetLevel().String())
|
|
|
|
|
mainLogger.Trace("Full argv: %v", os.Args)
|
|
|
|
|
|
|
|
|
|
if flag.NArg() == 0 {
|
|
|
|
|
flag.Usage()
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
func runModifier(args []string, cmd *cobra.Command) {
|
|
|
|
|
// Get flag values from Cobra
|
|
|
|
|
convertFlag, _ := cmd.Flags().GetBool("conv")
|
|
|
|
|
parallelFlag, _ := cmd.Flags().GetInt("parallel")
|
|
|
|
|
filterFlag, _ := cmd.Flags().GetString("filter")
|
|
|
|
|
jsonFlag, _ := cmd.Flags().GetBool("json")
|
|
|
|
|
|
|
|
|
|
// Handle YAML to TOML conversion if -conv flag is set
|
|
|
|
|
if *utils.Convert {
|
|
|
|
|
if convertFlag {
|
|
|
|
|
mainLogger.Info("YAML to TOML conversion mode enabled")
|
|
|
|
|
conversionCount := 0
|
|
|
|
|
for _, arg := range args {
|
|
|
|
|
@@ -127,7 +188,7 @@ func main() {
|
|
|
|
|
commands, err := utils.LoadCommands(args)
|
|
|
|
|
if err != nil || len(commands) == 0 {
|
|
|
|
|
mainLogger.Error("Failed to load commands: %v", err)
|
|
|
|
|
flag.Usage()
|
|
|
|
|
cmd.Usage()
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
// Collect global modifiers from special entries and filter them out
|
|
|
|
|
@@ -149,9 +210,9 @@ func main() {
|
|
|
|
|
commands = filtered
|
|
|
|
|
mainLogger.Info("Loaded %d commands", len(commands))
|
|
|
|
|
|
|
|
|
|
if *utils.Filter != "" {
|
|
|
|
|
mainLogger.Info("Filtering commands by name: %s", *utils.Filter)
|
|
|
|
|
commands = utils.FilterCommands(commands, *utils.Filter)
|
|
|
|
|
if filterFlag != "" {
|
|
|
|
|
mainLogger.Info("Filtering commands by name: %s", filterFlag)
|
|
|
|
|
commands = utils.FilterCommands(commands, filterFlag)
|
|
|
|
|
mainLogger.Info("Filtered %d commands", len(commands))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@@ -221,9 +282,9 @@ func main() {
|
|
|
|
|
mainLogger.Debug("Files reset where necessary")
|
|
|
|
|
|
|
|
|
|
// Then for each file run all commands associated with the file
|
|
|
|
|
workers := make(chan struct{}, *utils.ParallelFiles)
|
|
|
|
|
workers := make(chan struct{}, parallelFlag)
|
|
|
|
|
wg := sync.WaitGroup{}
|
|
|
|
|
mainLogger.Debug("Starting file processing with %d parallel workers", *utils.ParallelFiles)
|
|
|
|
|
mainLogger.Debug("Starting file processing with %d parallel workers", parallelFlag)
|
|
|
|
|
|
|
|
|
|
// Add performance tracking
|
|
|
|
|
startTime := time.Now()
|
|
|
|
|
@@ -273,7 +334,7 @@ func main() {
|
|
|
|
|
|
|
|
|
|
isChanged := false
|
|
|
|
|
mainLogger.Debug("Running isolate commands for file %q", file)
|
|
|
|
|
fileDataStr, err = RunIsolateCommands(association, file, fileDataStr)
|
|
|
|
|
fileDataStr, err = RunIsolateCommands(association, file, fileDataStr, jsonFlag)
|
|
|
|
|
if err != nil && err != NothingToDo {
|
|
|
|
|
mainLogger.Error("Failed to run isolate commands for file %q: %v", file, err)
|
|
|
|
|
atomic.AddInt64(&stats.FailedFiles, 1)
|
|
|
|
|
@@ -284,7 +345,7 @@ func main() {
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
mainLogger.Debug("Running other commands for file %q", file)
|
|
|
|
|
fileDataStr, err = RunOtherCommands(file, fileDataStr, association, commandLoggers)
|
|
|
|
|
fileDataStr, err = RunOtherCommands(file, fileDataStr, association, commandLoggers, jsonFlag)
|
|
|
|
|
if err != nil && err != NothingToDo {
|
|
|
|
|
mainLogger.Error("Failed to run other commands for file %q: %v", file, err)
|
|
|
|
|
atomic.AddInt64(&stats.FailedFiles, 1)
|
|
|
|
|
@@ -333,40 +394,6 @@ func main() {
|
|
|
|
|
// 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 {
|
|
|
|
|
// mainLogger.Info("Git integration enabled, setting up git repository")
|
|
|
|
|
// err := setupGit()
|
|
|
|
|
// if err != nil {
|
|
|
|
|
// mainLogger.Error("Failed to setup git: %v", err)
|
|
|
|
|
// fmt.Fprintf(os.Stderr, "Error setting up git: %v\n", err)
|
|
|
|
|
// return
|
|
|
|
|
// }
|
|
|
|
|
// }
|
|
|
|
|
|
|
|
|
|
// mainLogger.Debug("Expanding file patterns")
|
|
|
|
|
// files, err := expandFilePatterns(filePatterns)
|
|
|
|
|
// if err != nil {
|
|
|
|
|
// mainLogger.Error("Failed to expand file patterns: %v", err)
|
|
|
|
|
// fmt.Fprintf(os.Stderr, "Error expanding file patterns: %v\n", err)
|
|
|
|
|
// return
|
|
|
|
|
// }
|
|
|
|
|
|
|
|
|
|
// if *gitFlag {
|
|
|
|
|
// mainLogger.Info("Cleaning up git files before processing")
|
|
|
|
|
// err := cleanupGitFiles(files)
|
|
|
|
|
// if err != nil {
|
|
|
|
|
// mainLogger.Error("Failed to cleanup git files: %v", err)
|
|
|
|
|
// fmt.Fprintf(os.Stderr, "Error cleaning up git files: %v\n", err)
|
|
|
|
|
// return
|
|
|
|
|
// }
|
|
|
|
|
// }
|
|
|
|
|
// if *resetFlag {
|
|
|
|
|
// mainLogger.Info("Files reset to their original state, nothing more to do")
|
|
|
|
|
// log.Printf("Files reset to their original state, nothing more to do")
|
|
|
|
|
// return
|
|
|
|
|
// }
|
|
|
|
|
|
|
|
|
|
// Print summary
|
|
|
|
|
totalModifications := atomic.LoadInt64(&stats.TotalModifications)
|
|
|
|
|
@@ -444,7 +471,7 @@ func CreateExampleConfig() {
|
|
|
|
|
|
|
|
|
|
var NothingToDo = errors.New("nothing to do")
|
|
|
|
|
|
|
|
|
|
func RunOtherCommands(file string, fileDataStr string, association utils.FileCommandAssociation, commandLoggers map[string]*logger.Logger) (string, error) {
|
|
|
|
|
func RunOtherCommands(file string, fileDataStr string, association utils.FileCommandAssociation, commandLoggers map[string]*logger.Logger, jsonFlag bool) (string, error) {
|
|
|
|
|
runOtherCommandsLogger := mainLogger.WithPrefix("RunOtherCommands").WithField("file", file)
|
|
|
|
|
runOtherCommandsLogger.Debug("Running other commands for file")
|
|
|
|
|
runOtherCommandsLogger.Trace("File data before modifications: %s", utils.LimitString(fileDataStr, 200))
|
|
|
|
|
@@ -454,7 +481,7 @@ func RunOtherCommands(file string, fileDataStr string, association utils.FileCom
|
|
|
|
|
regexCommands := []utils.ModifyCommand{}
|
|
|
|
|
|
|
|
|
|
for _, command := range association.Commands {
|
|
|
|
|
if command.JSON || *utils.JSON {
|
|
|
|
|
if command.JSON || jsonFlag {
|
|
|
|
|
jsonCommands = append(jsonCommands, command)
|
|
|
|
|
} else {
|
|
|
|
|
regexCommands = append(regexCommands, command)
|
|
|
|
|
@@ -548,7 +575,7 @@ func RunOtherCommands(file string, fileDataStr string, association utils.FileCom
|
|
|
|
|
return fileDataStr, nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func RunIsolateCommands(association utils.FileCommandAssociation, file string, fileDataStr string) (string, error) {
|
|
|
|
|
func RunIsolateCommands(association utils.FileCommandAssociation, file string, fileDataStr string, jsonFlag bool) (string, error) {
|
|
|
|
|
runIsolateCommandsLogger := mainLogger.WithPrefix("RunIsolateCommands").WithField("file", file)
|
|
|
|
|
runIsolateCommandsLogger.Debug("Running isolate commands for file")
|
|
|
|
|
runIsolateCommandsLogger.Trace("File data before isolate modifications: %s", utils.LimitString(fileDataStr, 200))
|
|
|
|
|
@@ -558,7 +585,7 @@ func RunIsolateCommands(association utils.FileCommandAssociation, file string, f
|
|
|
|
|
|
|
|
|
|
for _, isolateCommand := range association.IsolateCommands {
|
|
|
|
|
// Check if this isolate command should use JSON mode
|
|
|
|
|
if isolateCommand.JSON || *utils.JSON {
|
|
|
|
|
if isolateCommand.JSON || jsonFlag {
|
|
|
|
|
runIsolateCommandsLogger.Debug("Begin processing file with JSON isolate command %q", isolateCommand.Name)
|
|
|
|
|
modifications, err := processor.ProcessJSON(currentFileData, isolateCommand, file)
|
|
|
|
|
if err != nil {
|
|
|
|
|
|