3 Commits

3 changed files with 233 additions and 64 deletions

153
main.go
View File

@@ -96,6 +96,23 @@ func main() {
flag.Usage() flag.Usage()
return return
} }
// Collect global modifiers from special entries and filter them out
vars := map[string]interface{}{}
filtered := make([]utils.ModifyCommand, 0, len(commands))
for _, c := range commands {
if len(c.Modifiers) > 0 && c.Name == "" && c.Regex == "" && len(c.Regexes) == 0 && c.Lua == "" && len(c.Files) == 0 {
for k, v := range c.Modifiers {
vars[k] = v
}
continue
}
filtered = append(filtered, c)
}
if len(vars) > 0 {
mainLogger.Info("Loaded %d global modifiers", len(vars))
processor.SetVariables(vars)
}
commands = filtered
mainLogger.Info("Loaded %d commands", len(commands)) mainLogger.Info("Loaded %d commands", len(commands))
if *utils.Filter != "" { if *utils.Filter != "" {
@@ -111,7 +128,11 @@ func main() {
for _, command := range commands { for _, command := range commands {
mainLogger.Trace("Command: %s", command.Name) mainLogger.Trace("Command: %s", command.Name)
if len(command.Regexes) > 0 {
mainLogger.Trace("Regexes: %v", command.Regexes)
} else {
mainLogger.Trace("Regex: %s", command.Regex) mainLogger.Trace("Regex: %s", command.Regex)
}
mainLogger.Trace("Files: %v", command.Files) mainLogger.Trace("Files: %v", command.Files)
mainLogger.Trace("Lua: %s", command.Lua) mainLogger.Trace("Lua: %s", command.Lua)
mainLogger.Trace("Reset: %t", command.Reset) mainLogger.Trace("Reset: %t", command.Reset)
@@ -370,29 +391,96 @@ func CreateExampleConfig() {
createExampleConfigLogger := logger.Default.WithPrefix("CreateExampleConfig") createExampleConfigLogger := logger.Default.WithPrefix("CreateExampleConfig")
createExampleConfigLogger.Debug("Creating example configuration file") createExampleConfigLogger.Debug("Creating example configuration file")
commands := []utils.ModifyCommand{ commands := []utils.ModifyCommand{
// Global modifiers only entry (no name/regex/lua/files)
{ {
Name: "DoubleNumericValues", Modifiers: map[string]interface{}{
Regex: "<value>(\\d+)<\\/value>", "foobar": 4,
Lua: "v1 * 2", "multiply": 1.5,
Files: []string{"data/*.xml"}, "prefix": "NEW_",
LogLevel: "INFO", "enabled": true,
}, },
},
// Multi-regex example using $variable in Lua
{ {
Name: "UpdatePrices", Name: "RFToolsMultiply",
Regex: "price=\"(\\d+)\"", Regexes: []string{"generatePerTick = !num", "ticksPer\\w+ = !num", "generatorRFPerTick = !num"},
Lua: "if num(v1) < 100 then return v1 * 1.5 else return v1 end", Lua: "* $foobar",
Files: []string{"items/*.xml", "shop/*.xml"}, Files: []string{"polymc/instances/**/rftools*.toml", `polymc\\instances\\**\\rftools*.toml`},
Reset: true,
// LogLevel defaults to INFO
},
// Named capture groups with arithmetic and string ops
{
Name: "UpdateAmountsAndItems",
Regex: `(?P<amount>!num)\s+units\s+of\s+(?P<item>[A-Za-z_\-]+)`,
Lua: `amount = amount * $multiply; item = upper(item); return true`,
Files: []string{"data/**/*.txt"},
// INFO log level
},
// Full replacement via Lua 'replacement' variable
{
Name: "BumpMinorVersion",
Regex: `version\s*=\s*"(?P<major>!num)\.(?P<minor>!num)\.(?P<patch>!num)"`,
Lua: `replacement = format("version=\"%s.%s.%s\"", major, num(minor)+1, 0); return true`,
Files: []string{"config/*.ini", "config/*.cfg"},
},
// Multiline regex example (DOTALL is auto-enabled). Captures numeric in nested XML.
{
Name: "XMLNestedValueMultiply",
Regex: `<item>\s*\s*<name>!any<\/name>\s*\s*<value>(!num)<\/value>\s*\s*<\/item>`,
Lua: `* $multiply`,
Files: []string{"data/**/*.xml"},
// Demonstrates multiline regex in YAML
},
// Multiline regexES array, with different patterns handled by same Lua
{
Name: "MultiLinePatterns",
Regexes: []string{
`<entry>\s*\n\s*<id>(?P<id>!num)</id>\s*\n\s*<score>(?P<score>!num)</score>\s*\n\s*</entry>`,
`\[block\]\nkey=(?P<key>[A-Za-z_]+)\nvalue=(?P<val>!num)`,
},
Lua: `if is_number(score) then score = score * 2 end; if is_number(val) then val = val * 3 end; return true`,
Files: []string{"examples/**/*.*"},
LogLevel: "DEBUG", LogLevel: "DEBUG",
}, },
// Use equals operator shorthand and boolean variable
{ {
Name: "IsolatedTagUpdate", Name: "EnableFlags",
Regex: "<tag>(.*?)<\\/tag>", Regex: `enabled\s*=\s*(true|false)`,
Lua: "string.upper(s1)", Lua: `= $enabled`,
Files: []string{"config.xml"}, Files: []string{"**/*.toml"},
Isolate: true, },
// Demonstrate NoDedup to allow overlapping replacements
{
Name: "OverlappingGroups",
Regex: `(?P<a>!num)(?P<b>!num)`,
Lua: `a = num(a) + 1; b = num(b) + 1; return true`,
Files: []string{"overlap/**/*.txt"},
NoDedup: true, NoDedup: true,
},
// Isolate command example operating on entire matched block
{
Name: "IsolateUppercaseBlock",
Regex: `BEGIN\n(?P<block>!any)\nEND`,
Lua: `block = upper(block); return true`,
Files: []string{"logs/**/*.log"},
Isolate: true,
LogLevel: "TRACE", LogLevel: "TRACE",
}, },
// Using !rep placeholder and arrays of files
{
Name: "RepeatPlaceholderExample",
Regex: `name: (.*) !rep(, .* , 2)`,
Lua: `-- no-op, just demonstrate placeholder; return false`,
Files: []string{"lists/**/*.yml", "lists/**/*.yaml"},
},
// Using string variable in Lua expression
{
Name: "PrefixKeys",
Regex: `(?P<key>[A-Za-z0-9_]+)\s*=`,
Lua: `key = $prefix .. key; return true`,
Files: []string{"**/*.properties"},
},
} }
data, err := yaml.Marshal(commands) data, err := yaml.Marshal(commands)
@@ -428,31 +516,36 @@ func RunOtherCommands(file string, fileDataStr string, association utils.FileCom
cmdLogger = cmdLog cmdLogger = cmdLog
} }
cmdLogger.Debug("Begin processing file with command %q", command.Regex) patterns := command.Regexes
if len(patterns) == 0 {
patterns = []string{command.Regex}
}
for idx, pattern := range patterns {
tmpCmd := command
tmpCmd.Regex = pattern
cmdLogger.Debug("Begin processing file with command %q (pattern %d/%d)", command.Name, idx+1, len(patterns))
numCommandsConsidered++ numCommandsConsidered++
newModifications, err := processor.ProcessRegex(fileDataStr, command, file) newModifications, err := processor.ProcessRegex(fileDataStr, tmpCmd, file)
if err != nil { if err != nil {
runOtherCommandsLogger.Error("Failed to process file with command %q: %v", command.Regex, err) runOtherCommandsLogger.Error("Failed to process file with command %q: %v", command.Name, err)
continue continue
} }
modifications = append(modifications, newModifications...) modifications = append(modifications, newModifications...)
// It is not guranteed that all the commands will be executed...
// TODO: Make this better
// We'd have to pass the map to executemodifications or something...
count, ok := stats.ModificationsPerCommand.Load(command.Name) count, ok := stats.ModificationsPerCommand.Load(command.Name)
if !ok { if !ok {
count = 0 count = 0
} }
stats.ModificationsPerCommand.Store(command.Name, count.(int)+len(newModifications)) stats.ModificationsPerCommand.Store(command.Name, count.(int)+len(newModifications))
cmdLogger.Debug("Command %q generated %d modifications", command.Name, len(newModifications)) cmdLogger.Debug("Command %q generated %d modifications (pattern %d/%d)", command.Name, len(newModifications), idx+1, len(patterns))
cmdLogger.Trace("Modifications generated by command %q: %v", command.Name, newModifications) cmdLogger.Trace("Modifications generated by command %q: %v", command.Name, newModifications)
if len(newModifications) == 0 { if len(newModifications) == 0 {
cmdLogger.Debug("No modifications yielded by command %q", command.Name) cmdLogger.Debug("No modifications yielded by command %q (pattern %d/%d)", command.Name, idx+1, len(patterns))
}
} }
} }
runOtherCommandsLogger.Debug("Aggregated %d modifications from %d commands", len(modifications), numCommandsConsidered) runOtherCommandsLogger.Debug("Aggregated %d modifications from %d command-pattern runs", len(modifications), numCommandsConsidered)
runOtherCommandsLogger.Trace("All aggregated modifications: %v", modifications) runOtherCommandsLogger.Trace("All aggregated modifications: %v", modifications)
if len(modifications) == 0 { if len(modifications) == 0 {
@@ -480,14 +573,21 @@ func RunIsolateCommands(association utils.FileCommandAssociation, file string, f
anythingDone := false anythingDone := false
for _, isolateCommand := range association.IsolateCommands { for _, isolateCommand := range association.IsolateCommands {
runIsolateCommandsLogger.Debug("Begin processing file with isolate command %q", isolateCommand.Regex) runIsolateCommandsLogger.Debug("Begin processing file with isolate command %q", isolateCommand.Regex)
modifications, err := processor.ProcessRegex(fileDataStr, isolateCommand, file) patterns := isolateCommand.Regexes
if len(patterns) == 0 {
patterns = []string{isolateCommand.Regex}
}
for idx, pattern := range patterns {
tmpCmd := isolateCommand
tmpCmd.Regex = pattern
modifications, err := processor.ProcessRegex(fileDataStr, tmpCmd, file)
if err != nil { if err != nil {
runIsolateCommandsLogger.Error("Failed to process file with isolate command %q: %v", isolateCommand.Regex, err) runIsolateCommandsLogger.Error("Failed to process file with isolate command %q (pattern %d/%d): %v", isolateCommand.Name, idx+1, len(patterns), err)
continue continue
} }
if len(modifications) == 0 { if len(modifications) == 0 {
runIsolateCommandsLogger.Debug("Isolate command %q produced no modifications", isolateCommand.Name) runIsolateCommandsLogger.Debug("Isolate command %q produced no modifications (pattern %d/%d)", isolateCommand.Name, idx+1, len(patterns))
continue continue
} }
anythingDone = true anythingDone = true
@@ -502,6 +602,7 @@ func RunIsolateCommands(association utils.FileCommandAssociation, file string, f
runIsolateCommandsLogger.Info("Executed %d isolate modifications for file", count) runIsolateCommandsLogger.Info("Executed %d isolate modifications for file", count)
} }
}
if !anythingDone { if !anythingDone {
runIsolateCommandsLogger.Debug("No isolate modifications were made for file") runIsolateCommandsLogger.Debug("No isolate modifications were made for file")
return fileDataStr, NothingToDo return fileDataStr, NothingToDo

View File

@@ -4,6 +4,7 @@ import (
"fmt" "fmt"
"io" "io"
"net/http" "net/http"
"regexp"
"strings" "strings"
"cook/utils" "cook/utils"
@@ -18,6 +19,14 @@ var processorLogger = logger.Default.WithPrefix("processor")
// Maybe we make this an interface again for the shits and giggles // Maybe we make this an interface again for the shits and giggles
// We will see, it could easily be... // We will see, it could easily be...
var globalVariables = map[string]interface{}{}
func SetVariables(vars map[string]interface{}) {
for k, v := range vars {
globalVariables[k] = v
}
}
func NewLuaState() (*lua.LState, error) { func NewLuaState() (*lua.LState, error) {
newLStateLogger := processorLogger.WithPrefix("NewLuaState") newLStateLogger := processorLogger.WithPrefix("NewLuaState")
newLStateLogger.Debug("Creating new Lua state") newLStateLogger.Debug("Creating new Lua state")
@@ -42,6 +51,34 @@ func NewLuaState() (*lua.LState, error) {
} }
newLStateLogger.Debug("Lua helper functions initialized") newLStateLogger.Debug("Lua helper functions initialized")
// Inject global variables
if len(globalVariables) > 0 {
newLStateLogger.Debug("Injecting %d global variables into Lua state", len(globalVariables))
for k, v := range globalVariables {
switch val := v.(type) {
case int:
L.SetGlobal(k, lua.LNumber(float64(val)))
case int64:
L.SetGlobal(k, lua.LNumber(float64(val)))
case float32:
L.SetGlobal(k, lua.LNumber(float64(val)))
case float64:
L.SetGlobal(k, lua.LNumber(val))
case string:
L.SetGlobal(k, lua.LString(val))
case bool:
if val {
L.SetGlobal(k, lua.LTrue)
} else {
L.SetGlobal(k, lua.LFalse)
}
default:
// Fallback to string representation
L.SetGlobal(k, lua.LString(fmt.Sprintf("%v", val)))
}
}
}
newLStateLogger.Debug("New Lua state created successfully") newLStateLogger.Debug("New Lua state created successfully")
return L, nil return L, nil
} }
@@ -246,6 +283,9 @@ func BuildLuaScript(luaExpr string) string {
buildLuaScriptLogger := processorLogger.WithPrefix("BuildLuaScript").WithField("inputLuaExpr", luaExpr) buildLuaScriptLogger := processorLogger.WithPrefix("BuildLuaScript").WithField("inputLuaExpr", luaExpr)
buildLuaScriptLogger.Debug("Building full Lua script from expression") buildLuaScriptLogger.Debug("Building full Lua script from expression")
// Perform $var substitutions from globalVariables
luaExpr = replaceVariables(luaExpr)
luaExpr = PrependLuaAssignment(luaExpr) luaExpr = PrependLuaAssignment(luaExpr)
fullScript := fmt.Sprintf(` fullScript := fmt.Sprintf(`
@@ -260,6 +300,32 @@ func BuildLuaScript(luaExpr string) string {
return fullScript return fullScript
} }
func replaceVariables(expr string) string {
// $varName -> literal value
varNameRe := regexp.MustCompile(`\$(\w+)`)
return varNameRe.ReplaceAllStringFunc(expr, func(m string) string {
name := varNameRe.FindStringSubmatch(m)[1]
if v, ok := globalVariables[name]; ok {
switch val := v.(type) {
case int, int64, float32, float64:
return fmt.Sprintf("%v", val)
case bool:
if val {
return "true"
} else {
return "false"
}
case string:
// Quote strings for Lua literal
return fmt.Sprintf("%q", val)
default:
return fmt.Sprintf("%q", fmt.Sprintf("%v", val))
}
}
return m
})
}
func printToGo(L *lua.LState) int { func printToGo(L *lua.LState) int {
printToGoLogger := processorLogger.WithPrefix("printToGo") printToGoLogger := processorLogger.WithPrefix("printToGo")
printToGoLogger.Debug("Lua print function called, redirecting to Go logger") printToGoLogger.Debug("Lua print function called, redirecting to Go logger")

View File

@@ -15,15 +15,17 @@ import (
var modifyCommandLogger = logger.Default.WithPrefix("utils/modifycommand") var modifyCommandLogger = logger.Default.WithPrefix("utils/modifycommand")
type ModifyCommand struct { type ModifyCommand struct {
Name string `yaml:"name"` Name string `yaml:"name,omitempty"`
Regex string `yaml:"regex"` Regex string `yaml:"regex,omitempty"`
Lua string `yaml:"lua"` Regexes []string `yaml:"regexes,omitempty"`
Files []string `yaml:"files"` Lua string `yaml:"lua,omitempty"`
Reset bool `yaml:"reset"` Files []string `yaml:"files,omitempty"`
LogLevel string `yaml:"loglevel"` Reset bool `yaml:"reset,omitempty"`
Isolate bool `yaml:"isolate"` LogLevel string `yaml:"loglevel,omitempty"`
NoDedup bool `yaml:"nodedup"` Isolate bool `yaml:"isolate,omitempty"`
Disabled bool `yaml:"disable"` NoDedup bool `yaml:"nodedup,omitempty"`
Disabled bool `yaml:"disable,omitempty"`
Modifiers map[string]interface{} `yaml:"modifiers,omitempty"`
} }
type CookFile []ModifyCommand type CookFile []ModifyCommand
@@ -31,7 +33,7 @@ type CookFile []ModifyCommand
func (c *ModifyCommand) Validate() error { func (c *ModifyCommand) Validate() error {
validateLogger := modifyCommandLogger.WithPrefix("Validate").WithField("commandName", c.Name) validateLogger := modifyCommandLogger.WithPrefix("Validate").WithField("commandName", c.Name)
validateLogger.Debug("Validating command") validateLogger.Debug("Validating command")
if c.Regex == "" { if c.Regex == "" && len(c.Regexes) == 0 {
validateLogger.Error("Validation failed: Regex pattern is required") validateLogger.Error("Validation failed: Regex pattern is required")
return fmt.Errorf("pattern is required") return fmt.Errorf("pattern is required")
} }