Refactor modify and replace to their own files

This commit is contained in:
2025-03-27 22:18:12 +01:00
parent f6def1e5a5
commit f008efd5e1
4 changed files with 200 additions and 172 deletions

184
main.go
View File

@@ -10,6 +10,8 @@ import (
"sync" "sync"
"time" "time"
"modify/utils"
"github.com/bmatcuk/doublestar/v4" "github.com/bmatcuk/doublestar/v4"
"github.com/go-git/go-git/v5" "github.com/go-git/go-git/v5"
"github.com/go-git/go-git/v5/plumbing/object" "github.com/go-git/go-git/v5/plumbing/object"
@@ -25,49 +27,12 @@ type GlobalStats struct {
FailedFiles int FailedFiles int
} }
var stats GlobalStats = GlobalStats{}
var ( var (
gitFlag = flag.Bool("git", false, "Use git to manage files") repo *git.Repository
resetFlag = flag.Bool("reset", false, "Reset files to their original state") worktree *git.Worktree
logLevel = flag.String("loglevel", "INFO", "Set log level: ERROR, WARNING, INFO, DEBUG, TRACE") stats GlobalStats = GlobalStats{}
cookfile = flag.String("cook", "**/cook.yml", "Path to cook config files, can be globbed")
parallelfiles = flag.Int("P", 100, "Number of files to process in parallel")
repo *git.Repository
worktree *git.Worktree
) )
type ModifyCommand struct {
Pattern string `yaml:"pattern"`
LuaExpr string `yaml:"lua"`
Files []string `yaml:"files"`
Git bool `yaml:"git"`
Reset bool `yaml:"reset"`
LogLevel string `yaml:"loglevel"`
}
type ReplaceCommand struct {
From int
To int
With string
}
type CookFile []ModifyCommand
func (c *ModifyCommand) Validate() error {
if c.Pattern == "" {
return fmt.Errorf("pattern is required")
}
if c.LuaExpr == "" {
return fmt.Errorf("lua expression is required")
}
if len(c.Files) == 0 {
return fmt.Errorf("at least one file is required")
}
if c.LogLevel == "" {
c.LogLevel = "INFO"
}
return nil
}
func main() { func main() {
flag.Usage = func() { flag.Usage = func() {
fmt.Fprintf(os.Stderr, "Usage: %s [options] <pattern> <lua_expression> <...files_or_globs>\n", os.Args[0]) fmt.Fprintf(os.Stderr, "Usage: %s [options] <pattern> <lua_expression> <...files_or_globs>\n", os.Args[0])
@@ -92,13 +57,13 @@ func main() {
flag.Parse() flag.Parse()
args := flag.Args() args := flag.Args()
level := logger.ParseLevel(*logLevel) level := logger.ParseLevel(*utils.LogLevel)
logger.Init(level) logger.Init(level)
logger.Info("Initializing with log level: %s", level.String()) logger.Info("Initializing with log level: %s", level.String())
// The plan is: // The plan is:
// Load all commands // Load all commands
commands, err := LoadCommands(args) commands, err := utils.LoadCommands(args)
if err != nil { if err != nil {
logger.Error("Failed to load commands: %v", err) logger.Error("Failed to load commands: %v", err)
flag.Usage() flag.Usage()
@@ -106,7 +71,7 @@ func main() {
} }
// Then aggregate all the globs and deduplicate them // Then aggregate all the globs and deduplicate them
globs := AggregateGlobs(commands) globs := utils.AggregateGlobs(commands)
// Resolve all the files for all the globs // Resolve all the files for all the globs
logger.Info("Found %d unique file patterns", len(globs)) logger.Info("Found %d unique file patterns", len(globs))
@@ -121,7 +86,7 @@ func main() {
// For each file check every glob of every command // For each file check every glob of every command
// Maybe memoize this part // Maybe memoize this part
// That way we know what commands affect what files // That way we know what commands affect what files
associations, err := AssociateFilesWithCommands(files, commands) associations, err := utils.AssociateFilesWithCommands(files, commands)
if err != nil { if err != nil {
logger.Error("Failed to associate files with commands: %v", err) logger.Error("Failed to associate files with commands: %v", err)
return return
@@ -139,7 +104,7 @@ func main() {
fileDataStr := string(fileData) fileDataStr := string(fileData)
// Aggregate all the modifications and execute them // Aggregate all the modifications and execute them
modifications := []ReplaceCommand{} modifications := []utils.ReplaceCommand{}
for _, command := range commands { for _, command := range commands {
logger.Info("Processing file %q with command %q", file, command.Pattern) logger.Info("Processing file %q with command %q", file, command.Pattern)
// TODO: Run processor and return modifications // TODO: Run processor and return modifications
@@ -151,7 +116,8 @@ func main() {
}) })
logger.Trace("Applying %d replacement commands in reverse order", len(modifications)) logger.Trace("Applying %d replacement commands in reverse order", len(modifications))
fileDataStr = ExecuteModifications(modifications, fileDataStr) fileDataStr, count := utils.ExecuteModifications(modifications, fileDataStr)
logger.Info("Executed %d modifications for file %q", count, file)
err = os.WriteFile(file, []byte(fileDataStr), 0644) err = os.WriteFile(file, []byte(fileDataStr), 0644)
if err != nil { if err != nil {
@@ -257,132 +223,6 @@ func main() {
} }
} }
func ExecuteModifications(modifications []ReplaceCommand, fileData string) string {
var err error
for _, modification := range modifications {
fileData, err = modification.Execute(fileData)
if err != nil {
logger.Error("Failed to execute modification: %v", err)
continue
}
}
return fileData
}
func (m *ReplaceCommand) Execute(fileDataStr string) (string, error) {
err := m.Validate(len(fileDataStr))
if err != nil {
return "", fmt.Errorf("failed to validate modification: %v", err)
}
logger.Trace("Replace pos %d-%d with %q", m.From, m.To, m.With)
return fileDataStr[:m.From] + m.With + fileDataStr[m.To:], nil
}
func (m *ReplaceCommand) Validate(maxsize int) error {
if m.To < m.From {
return fmt.Errorf("command to is less than from: %v", m)
}
if m.From > maxsize || m.To > maxsize {
return fmt.Errorf("command from or to is greater than replacement length: %v", m)
}
return nil
}
func AssociateFilesWithCommands(files []string, commands []ModifyCommand) (map[string][]ModifyCommand, error) {
associationCount := 0
fileCommands := make(map[string][]ModifyCommand)
for _, file := range files {
for _, command := range commands {
for _, glob := range command.Files {
// TODO: Maybe memoize this function call
matches, err := doublestar.Match(glob, file)
if err != nil {
logger.Trace("Failed to match glob %s with file %s: %v", glob, file, err)
continue
}
if matches {
logger.Debug("Found match for file %q and command %q", file, command.Pattern)
fileCommands[file] = append(fileCommands[file], command)
associationCount++
}
}
}
logger.Debug("Found %d commands for file %q", len(fileCommands[file]), file)
if len(fileCommands[file]) == 0 {
logger.Info("No commands found for file %q", file)
}
}
logger.Info("Found %d associations between %d files and %d commands", associationCount, len(files), len(commands))
return fileCommands, nil
}
func AggregateGlobs(commands []ModifyCommand) map[string]struct{} {
logger.Info("Aggregating globs for %d commands", len(commands))
globs := make(map[string]struct{})
for _, command := range commands {
for _, glob := range command.Files {
globs[glob] = struct{}{}
}
}
logger.Info("Found %d unique globs", len(globs))
return globs
}
func LoadCommands(args []string) ([]ModifyCommand, error) {
commands := []ModifyCommand{}
logger.Info("Loading commands from cook files: %s", *cookfile)
newcommands, err := LoadCommandsFromCookFiles(*cookfile)
if err != nil {
return nil, fmt.Errorf("failed to load commands from cook files: %w", err)
}
logger.Info("Successfully loaded %d commands from cook files", len(newcommands))
commands = append(commands, newcommands...)
logger.Info("Now total commands: %d", len(commands))
logger.Info("Loading commands from arguments: %v", args)
newcommands, err = LoadCommandFromArgs(args)
if err != nil {
return nil, fmt.Errorf("failed to load commands from args: %w", err)
}
logger.Info("Successfully loaded %d commands from args", len(newcommands))
commands = append(commands, newcommands...)
logger.Info("Now total commands: %d", len(commands))
return commands, nil
}
func LoadCommandFromArgs(args []string) ([]ModifyCommand, error) {
// Cannot reset without git, right?
if *resetFlag {
*gitFlag = true
}
if len(args) < 3 {
return nil, fmt.Errorf("at least %d arguments are required", 3)
}
command := ModifyCommand{
Pattern: args[0],
LuaExpr: args[1],
Files: args[2:],
Git: *gitFlag,
Reset: *resetFlag,
LogLevel: *logLevel,
}
if err := command.Validate(); err != nil {
return nil, fmt.Errorf("invalid command: %w", err)
}
return []ModifyCommand{command}, nil
}
func LoadCommandsFromCookFiles(s string) ([]ModifyCommand, error) {
return nil, nil
}
func setupGit() error { func setupGit() error {
cwd, err := os.Getwd() cwd, err := os.Getwd()
if err != nil { if err != nil {

13
utils/flags.go Normal file
View File

@@ -0,0 +1,13 @@
package utils
import (
"flag"
)
var (
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")
Cookfile = flag.String("cook", "**/cook.yml", "Path to cook config files, can be globbed")
ParallelFiles = flag.Int("P", 100, "Number of files to process in parallel")
)

128
utils/modifycommand.go Normal file
View File

@@ -0,0 +1,128 @@
package utils
import (
"fmt"
"modify/logger"
"github.com/bmatcuk/doublestar/v4"
)
type ModifyCommand struct {
Pattern string `yaml:"pattern"`
LuaExpr string `yaml:"lua"`
Files []string `yaml:"files"`
Git bool `yaml:"git"`
Reset bool `yaml:"reset"`
LogLevel string `yaml:"loglevel"`
}
type CookFile []ModifyCommand
func (c *ModifyCommand) Validate() error {
if c.Pattern == "" {
return fmt.Errorf("pattern is required")
}
if c.LuaExpr == "" {
return fmt.Errorf("lua expression is required")
}
if len(c.Files) == 0 {
return fmt.Errorf("at least one file is required")
}
if c.LogLevel == "" {
c.LogLevel = "INFO"
}
return nil
}
func AssociateFilesWithCommands(files []string, commands []ModifyCommand) (map[string][]ModifyCommand, error) {
associationCount := 0
fileCommands := make(map[string][]ModifyCommand)
for _, file := range files {
for _, command := range commands {
for _, glob := range command.Files {
// TODO: Maybe memoize this function call
matches, err := doublestar.Match(glob, file)
if err != nil {
logger.Trace("Failed to match glob %s with file %s: %v", glob, file, err)
continue
}
if matches {
logger.Debug("Found match for file %q and command %q", file, command.Pattern)
fileCommands[file] = append(fileCommands[file], command)
associationCount++
}
}
}
logger.Debug("Found %d commands for file %q", len(fileCommands[file]), file)
if len(fileCommands[file]) == 0 {
logger.Info("No commands found for file %q", file)
}
}
logger.Info("Found %d associations between %d files and %d commands", associationCount, len(files), len(commands))
return fileCommands, nil
}
func AggregateGlobs(commands []ModifyCommand) map[string]struct{} {
logger.Info("Aggregating globs for %d commands", len(commands))
globs := make(map[string]struct{})
for _, command := range commands {
for _, glob := range command.Files {
globs[glob] = struct{}{}
}
}
logger.Info("Found %d unique globs", len(globs))
return globs
}
func LoadCommands(args []string) ([]ModifyCommand, error) {
commands := []ModifyCommand{}
logger.Info("Loading commands from cook files: %s", *Cookfile)
newcommands, err := LoadCommandsFromCookFiles(*Cookfile)
if err != nil {
return nil, fmt.Errorf("failed to load commands from cook files: %w", err)
}
logger.Info("Successfully loaded %d commands from cook files", len(newcommands))
commands = append(commands, newcommands...)
logger.Info("Now total commands: %d", len(commands))
logger.Info("Loading commands from arguments: %v", args)
newcommands, err = LoadCommandFromArgs(args)
if err != nil {
return nil, fmt.Errorf("failed to load commands from args: %w", err)
}
logger.Info("Successfully loaded %d commands from args", len(newcommands))
commands = append(commands, newcommands...)
logger.Info("Now total commands: %d", len(commands))
return commands, nil
}
func LoadCommandFromArgs(args []string) ([]ModifyCommand, error) {
// Cannot reset without git, right?
if *ResetFlag {
*GitFlag = true
}
if len(args) < 3 {
return nil, fmt.Errorf("at least %d arguments are required", 3)
}
command := ModifyCommand{
Pattern: args[0],
LuaExpr: args[1],
Files: args[2:],
Git: *GitFlag,
Reset: *ResetFlag,
LogLevel: *LogLevel,
}
if err := command.Validate(); err != nil {
return nil, fmt.Errorf("invalid command: %w", err)
}
return []ModifyCommand{command}, nil
}
func LoadCommandsFromCookFiles(s string) ([]ModifyCommand, error) {
return nil, nil
}

47
utils/replacecommand.go Normal file
View File

@@ -0,0 +1,47 @@
package utils
import (
"fmt"
"modify/logger"
)
type ReplaceCommand struct {
From int
To int
With string
}
func ExecuteModifications(modifications []ReplaceCommand, fileData string) (string, int) {
var err error
executed := 0
for _, modification := range modifications {
fileData, err = modification.Execute(fileData)
if err != nil {
logger.Error("Failed to execute modification: %v", err)
continue
}
executed++
}
logger.Info("Executed %d modifications", executed)
return fileData, executed
}
func (m *ReplaceCommand) Execute(fileDataStr string) (string, error) {
err := m.Validate(len(fileDataStr))
if err != nil {
return "", fmt.Errorf("failed to validate modification: %v", err)
}
logger.Trace("Replace pos %d-%d with %q", m.From, m.To, m.With)
return fileDataStr[:m.From] + m.With + fileDataStr[m.To:], nil
}
func (m *ReplaceCommand) Validate(maxsize int) error {
if m.To < m.From {
return fmt.Errorf("command to is less than from: %v", m)
}
if m.From > maxsize || m.To > maxsize {
return fmt.Errorf("command from or to is greater than replacement length: %v", m)
}
return nil
}