From 6d15419ec80a2cbb8a9e833eea99504561aada38 Mon Sep 17 00:00:00 2001 From: Ayooluwa Isaiah Date: Sat, 4 May 2024 08:15:15 +0100 Subject: [PATCH] wip: debug mode --- app/app.go | 73 +++++++-- cmd/f2/main.go | 19 ++- f2.go | 26 ++-- find/find.go | 160 ++++++++++++++------ go.mod | 10 +- go.sum | 14 +- internal/config/config.go | 82 +++++----- internal/file/file.go | 20 ++- internal/sortfiles/sortfiles.go | 4 +- replace/replace.go | 256 ++++++++++++++++++++++++++++++-- 10 files changed, 528 insertions(+), 136 deletions(-) diff --git a/app/app.go b/app/app.go index 06f0d9c..c2598d2 100644 --- a/app/app.go +++ b/app/app.go @@ -3,6 +3,7 @@ package app import ( "fmt" "io" + "log/slog" "net/http" "os" "strings" @@ -69,6 +70,11 @@ func getDefaultOptsCtx() *cli.Context { var defaultCtx *cli.Context if optsEnv, exists := os.LookupEnv(EnvDefaultOpts); exists { + slog.Debug( + "found default options in environment", + slog.String("default_opts", optsEnv), + ) + defaultOpts := make([]string, len(os.Args)) copy(defaultOpts, os.Args) @@ -85,14 +91,24 @@ func getDefaultOptsCtx() *cli.Context { } // Run needs to be called here so that `defaultCtx` is populated - // It errors out when // TODO: complete this + // The only expected error is if the provided flags or arguments + // are incorrect err := app.Run(defaultOpts) if err != nil { - // TODO: Decide what to do here + slog.Debug("default options parse error", + slog.String("error", fmt.Sprintf("%v", err)), + ) + pterm.Fprintln( os.Stderr, - pterm.Error.Sprintf("error parsing default optons: %v", err), + pterm.Error.Sprintf( + "error parsing %s: %v", + EnvDefaultOpts, + err, + ), ) + + os.Exit(1) } } @@ -155,15 +171,18 @@ func Get(reader io.Reader, writer io.Writer) *cli.App { app.Before = func(ctx *cli.Context) error { if ctx.Bool("no-color") { + slog.Debug("disabling styling") pterm.DisableStyling() } if ctx.Bool("quiet") { + slog.Debug("disabling output") pterm.DisableOutput() } // print short help and exit if no arguments or flags are present if ctx.NumFlags() == 0 && !ctx.Args().Present() { + slog.Debug("print short help and exit") pterm.Println(ShortHelp(ctx.App)) os.Exit(1) } @@ -172,6 +191,10 @@ func Get(reader io.Reader, writer io.Writer) *cli.App { app.Metadata["writer"] = writer if ctx.NumFlags() == 0 { + slog.Debug( + "simple mode detected", + slog.Int("num_flags", ctx.NumFlags()), + ) app.Metadata["simple-mode"] = true } @@ -181,21 +204,45 @@ func Get(reader io.Reader, writer io.Writer) *cli.App { return nil } - // TODO: simplify - for _, opt := range supportedDefaultOptions { - value := fmt.Sprintf("%v", defaultCtx.Value(opt)) + for _, defaultOpt := range supportedDefaultOptions { + defaultValue := fmt.Sprintf("%v", defaultCtx.Value(defaultOpt)) + + if ctx.IsSet(defaultOpt) && defaultCtx.IsSet(defaultOpt) { + cliValue := fmt.Sprintf("%v", ctx.Value(defaultOpt)) + slog.Debug( + fmt.Sprintf( + "command line flag overrides default option for: %s", + defaultOpt, + ), + slog.String("flag", defaultOpt), + slog.String("command_line_value", cliValue), + slog.String("default_value", defaultValue), + ) + + continue + } - if !ctx.IsSet(opt) && defaultCtx.IsSet(opt) { - if x, ok := defaultCtx.Value(opt).(cli.StringSlice); ok { - value = strings.Join(x.Value(), "|") + if !ctx.IsSet(defaultOpt) && defaultCtx.IsSet(defaultOpt) { + if x, ok := defaultCtx.Value(defaultOpt).(cli.StringSlice); ok { + defaultValue = strings.Join(x.Value(), "|") } - err := ctx.Set(opt, value) + slog.Debug( + fmt.Sprintf("set default option for flag: %s", defaultOpt), + slog.String("flag", defaultOpt), + slog.String("default_value", defaultValue), + ) + + err := ctx.Set(defaultOpt, defaultValue) if err != nil { + slog.Debug("failed to set default option for: %s", + slog.String("flag", defaultOpt), + slog.String("default_value", defaultValue), + ) pterm.Fprintln(os.Stderr, pterm.Warning.Sprintf( "Unable to set default option for: %s", - opt, + defaultOpt, ), ) } @@ -231,6 +278,10 @@ or: FIND [REPLACE] [PATHS TO FILES AND DIRECTORIES...]` DefaultText: "", TakesFile: true, }, + &cli.BoolFlag{ + Name: "debug", + Usage: "Enable debug mode", + }, &cli.StringFlag{ Name: "exiftool-opts", Usage: "Provide custom options when using ExifTool variables", diff --git a/cmd/f2/main.go b/cmd/f2/main.go index 6830bac..00ffd9d 100644 --- a/cmd/f2/main.go +++ b/cmd/f2/main.go @@ -1,15 +1,32 @@ package main import ( + "log/slog" "os" "github.com/pterm/pterm" "github.com/ayoisaiah/f2" + + slogctx "github.com/veqryn/slog-context" ) +func initLogger() { + opts := &slog.HandlerOptions{ + Level: slog.LevelInfo, + } + + h := slogctx.NewHandler(slog.NewJSONHandler(os.Stderr, opts), nil) + + l := slog.New(h) + + slog.SetDefault(l) +} + func main() { - app := f2.GetApp(os.Stdin, os.Stdout) + initLogger() + + app := f2.New(os.Stdin, os.Stdout) err := app.Run(os.Args) if err != nil { diff --git a/f2.go b/f2.go index 38497ab..e49f536 100644 --- a/f2.go +++ b/f2.go @@ -2,7 +2,9 @@ package f2 import ( "errors" + "fmt" "io" + "log/slog" "github.com/urfave/cli/v2" @@ -21,11 +23,15 @@ var errConflictDetected = errors.New( // run starts a new renaming operation. func run(ctx *cli.Context) error { + // TODO: Log the final context + conf, err := config.Init(ctx) if err != nil { return err } + slog.Debug("configuration", slog.Any("config", conf)) + report.Stdout = conf.Stdout report.Stderr = conf.Stderr @@ -39,15 +45,25 @@ func run(ctx *cli.Context) error { } if len(matches) == 0 { + slog.Debug("no matches were found", slog.Any("find_matches", matches)) report.NoMatches(conf.JSON) + return nil } + slog.Debug( + fmt.Sprintf("found %d matches", len(matches)), + slog.Any("find_matches", matches), + slog.Int("num_matches", len(matches)), + ) + changes, err := replace.Replace(conf, matches) if err != nil { return err } + slog.Debug("replacements done", slog.Any("changes", changes)) + conflicts := validate.Validate( changes, conf.AutoFixConflicts, @@ -63,17 +79,9 @@ func run(ctx *cli.Context) error { return rename.Rename(conf, changes) } -func GetApp(reader io.Reader, writer io.Writer) *cli.App { +func New(reader io.Reader, writer io.Writer) *cli.App { f2App := app.Get(reader, writer) f2App.Action = run return f2App } - -// NewApp creates a new app instance. -func NewApp() *cli.App { - f2App := app.New() - f2App.Action = run - - return f2App -} diff --git a/find/find.go b/find/find.go index 34e3ef2..e89e734 100644 --- a/find/find.go +++ b/find/find.go @@ -2,6 +2,7 @@ package find import ( "io/fs" + "log/slog" "os" "path/filepath" "strings" @@ -42,7 +43,7 @@ func skipFileIfHidden( includeHidden bool, ) (bool, error) { if includeHidden { - return false, nil + return false, nil // No need to check if we're including hidden files } isHidden, err := checkIfHidden(filepath.Base(path), filepath.Dir(path)) @@ -51,7 +52,7 @@ func skipFileIfHidden( } if !isHidden { - return false, nil + return false, nil // No need to check further if the file isn't hidden } entryAbsPath, err := filepath.Abs(path) @@ -59,9 +60,7 @@ func skipFileIfHidden( return false, err } - skipFile := true - - // Ensure that explicitly included file arguments are not affected + // Ensure that file path arguments are included regardless of hidden status for _, pathArg := range filesAndDirPaths { argAbsPath, err := filepath.Abs(pathArg) if err != nil { @@ -69,35 +68,60 @@ func skipFileIfHidden( } if strings.EqualFold(entryAbsPath, argAbsPath) { - skipFile = false + slog.Debug( + "hidden file is explicitly included, not skipping", + slog.String("path", path), + ) + + return false, nil } } - return skipFile, nil + return true, nil // Skip the hidden file } // isMaxDepth reports whether the configured max depth has been reached. -func isMaxDepth(rootPath, currentPath string, maxDept int) bool { +func isMaxDepth(rootPath, currentPath string, maxDepth int) bool { if rootPath == filepath.Dir(currentPath) { return false } - if maxDept == -1 { + if maxDepth == -1 { return true } p := strings.Replace(currentPath, rootPath+string(os.PathSeparator), "", 1) - if strings.Count(p, string(os.PathSeparator)) > maxDept && maxDept != 0 { + if strings.Count(p, string(os.PathSeparator)) > maxDepth && maxDepth != 0 { return true } return false } +func createFileChange(dirPath string, fileInfo fs.FileInfo) *file.Change { + baseDir := filepath.Dir(dirPath) + fileName := fileInfo.Name() + + match := &file.Change{ + BaseDir: baseDir, + IsDir: fileInfo.IsDir(), + Source: fileName, + OriginalSource: fileName, + RelSourcePath: filepath.Join(baseDir, fileName), + } + + return match +} + // searchPaths walks through the filesystem and finds matches for the provided // search pattern. func searchPaths(conf *config.Config) ([]*file.Change, error) { + slog.Debug( + "searching path arguments for matches", + slog.Any("paths", conf.FilesAndDirPaths), + ) + processedPaths := make(map[string]bool) var matches []*file.Change @@ -111,36 +135,57 @@ func searchPaths(conf *config.Config) ([]*file.Change, error) { } if !fileInfo.IsDir() { + slog.Debug( + "processing root file argument", + slog.Any("path", rootPath), + ) + if processedPaths[rootPath] { + slog.Debug( + "skipping processed file", + slog.String("path", rootPath), + ) + continue } - baseDir := filepath.Dir(rootPath) - fileName := fileInfo.Name() + if conf.SearchRegex.MatchString(fileInfo.Name()) { + match := createFileChange(rootPath, fileInfo) - match := &file.Change{ - BaseDir: baseDir, - IsDir: fileInfo.IsDir(), - Source: fileName, - OriginalSource: fileName, - RelSourcePath: filepath.Join(baseDir, fileName), - } + if !shouldFilter(conf, match) { + slog.Debug( + "match found and passed filters", + slog.String("path", rootPath), + ) - excludeMatch := shouldFilter(conf, match) - if !excludeMatch { - matches = append(matches, match) - - processedPaths[rootPath] = true + matches = append(matches, match) + } else { + slog.Debug("match found but excluded", slog.String("path", rootPath)) + } + } else { + slog.Debug("file not matched for renaming", slog.String("path", rootPath)) } + processedPaths[rootPath] = true + continue } - maxDepth := -1 + maxDepth := -1 // default value for non-recursive iterations if conf.Recursive { maxDepth = conf.MaxDepth + + slog.Debug( + "recursively traversing directories to search for matches", + slog.Int("max_depth", maxDepth), + ) } + slog.Debug( + "processing root directory argument", + slog.Any("path", rootPath), + ) + err = filepath.WalkDir( rootPath, func(currentPath string, entry fs.DirEntry, err error) error { @@ -150,19 +195,24 @@ func searchPaths(conf *config.Config) ([]*file.Change, error) { // skip the root path and already processed paths if rootPath == currentPath || processedPaths[currentPath] { + slog.Debug( + "skipping processed path", + slog.String("path", currentPath), + slog.Bool("is_root", rootPath == currentPath), + ) + return nil } - skipHidden, herr := skipFileIfHidden( + if skipHidden, hiddenErr := skipFileIfHidden( currentPath, conf.FilesAndDirPaths, conf.IncludeHidden, - ) - if herr != nil { - return herr - } + ); hiddenErr != nil { + return hiddenErr + } else if skipHidden { + slog.Debug("skipping hidden path", slog.String("path", currentPath)) - if skipHidden { if entry.IsDir() { return fs.SkipDir } @@ -171,6 +221,12 @@ func searchPaths(conf *config.Config) ([]*file.Change, error) { } if isMaxDepth(rootPath, currentPath, maxDepth) { + slog.Debug( + "skipping entire directory: max depth reached", + slog.String("path", currentPath), + slog.String("parent_dir", filepath.Dir(currentPath)), + ) + return fs.SkipDir } @@ -180,31 +236,39 @@ func searchPaths(conf *config.Config) ([]*file.Change, error) { if conf.IgnoreExt && !entryIsDir { fileName = pathutil.StripExtension(fileName) - } - matched := conf.SearchRegex.MatchString(fileName) - if !matched { - return nil + slog.Debug( + "extension stripped", + slog.String("old_filename", entry.Name()), + slog.String("new_filename", fileName), + slog.Bool("is_dir", entryIsDir), + ) } - fileName = entry.Name() - baseDir := filepath.Dir(currentPath) + if conf.SearchRegex.MatchString(fileName) { + fileInfo, infoErr := entry.Info() + if infoErr != nil { + return infoErr + } - match := &file.Change{ - BaseDir: baseDir, - IsDir: entryIsDir, - Source: fileName, - OriginalSource: fileName, - RelSourcePath: filepath.Join(baseDir, fileName), - } + match := createFileChange(currentPath, fileInfo) - excludeMatch := shouldFilter(conf, match) - if !excludeMatch { - matches = append(matches, match) + if !shouldFilter(conf, match) { + slog.Debug( + "match found and passed filters", + slog.Any("path", currentPath), + ) - processedPaths[currentPath] = true + matches = append(matches, match) + } else { + slog.Debug("match found but excluded", slog.String("path", currentPath)) + } + } else { + slog.Debug("file not matched for renaming", slog.String("path", currentPath)) } + processedPaths[currentPath] = true + return nil }, ) diff --git a/go.mod b/go.mod index 4eef5ad..9c6a24f 100644 --- a/go.mod +++ b/go.mod @@ -19,9 +19,12 @@ require ( require ( github.com/MagicalTux/natsort v0.0.0-20220626140124-f8bd634d5139 github.com/araddon/dateparse v0.0.0-20210429162001-6b43995a97de - github.com/davecgh/go-spew v1.1.1 + github.com/djherbis/times v1.6.0 + github.com/jessevdk/go-flags v1.5.0 github.com/olekukonko/tablewriter v0.0.5 github.com/sebdah/goldie/v2 v2.5.3 + github.com/stretchr/testify v1.8.4 + github.com/veqryn/slog-context v0.7.0 golang.org/x/exp v0.0.0-20221028150844-83b7d23a625f ) @@ -30,16 +33,15 @@ require ( atomicgo.dev/keyboard v0.2.8 // indirect github.com/containerd/console v1.0.3 // indirect github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect - github.com/djherbis/times v1.6.0 // indirect + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/go-logr/logr v1.4.1 // indirect github.com/gookit/color v1.5.2 // indirect - github.com/jessevdk/go-flags v1.5.0 // indirect github.com/lithammer/fuzzysearch v1.1.5 // indirect github.com/mattn/go-runewidth v0.0.13 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/rivo/uniseg v0.4.2 // indirect github.com/russross/blackfriday/v2 v2.1.0 // indirect github.com/sergi/go-diff v1.2.0 // indirect - github.com/stretchr/testify v1.8.4 // indirect github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect golang.org/x/term v0.0.0-20220722155259-a9ba230a4035 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect diff --git a/go.sum b/go.sum index 3df72b6..fc1662d 100644 --- a/go.sum +++ b/go.sum @@ -17,8 +17,6 @@ github.com/adrg/xdg v0.4.0/go.mod h1:N6ag73EX4wyxeaoeHctc1mas01KZgsj5tYiAIwqJE/E github.com/araddon/dateparse v0.0.0-20210429162001-6b43995a97de h1:FxWPpzIjnTlhPwqqXc4/vE0f7GvRjuAsbW+HOIe8KnA= github.com/araddon/dateparse v0.0.0-20210429162001-6b43995a97de/go.mod h1:DCaWoUhZrYW9p1lxo/cm8EmUOOzAPSEZNGF2DK1dJgw= github.com/atomicgo/cursor v0.0.1/go.mod h1:cBON2QmmrysudxNBFthvMtN32r3jxVRIvzkUiF/RuIk= -github.com/barasher/go-exiftool v1.8.0 h1:u8bEi1mhLtpVC5aG/ZJlRS/r+SkK+rcgbZQwcKUb424= -github.com/barasher/go-exiftool v1.8.0/go.mod h1:F9s/a3uHSM8YniVfwF+sbQUtP8Gmh9nyzigNF+8vsWo= github.com/barasher/go-exiftool v1.10.0 h1:f5JY5jc42M7tzR6tbL9508S2IXdIcG9QyieEXNMpIhs= github.com/barasher/go-exiftool v1.10.0/go.mod h1:F9s/a3uHSM8YniVfwF+sbQUtP8Gmh9nyzigNF+8vsWo= github.com/containerd/console v1.0.3 h1:lIr7SlA5PxZyMV30bDW0MGbiOPXwc63yRuCP0ARubLw= @@ -32,6 +30,8 @@ github.com/dhowden/tag v0.0.0-20220618230019-adf36e896086 h1:ORubSQoKnncsBnR4zD9 github.com/dhowden/tag v0.0.0-20220618230019-adf36e896086/go.mod h1:Z3Lomva4pyMWYezjMAU5QWRh0p1VvO4199OHlFnyKkM= github.com/djherbis/times v1.6.0 h1:w2ctJ92J8fBvWPxugmXIv7Nz7Q3iDMKNx9v5ocVH20c= github.com/djherbis/times v1.6.0/go.mod h1:gOHeRAz2h+VJNZ5Gmc/o7iD9k4wW7NMVqieYCY99oc0= +github.com/go-logr/logr v1.4.1 h1:pKouT5E8xu9zeFC39JXRDukb6JFQPXM5p5I91188VAQ= +github.com/go-logr/logr v1.4.1/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/gookit/color v1.4.2/go.mod h1:fqRyamkC1W8uxl+lxCQxOT09l/vYfZ+QeiX3rKQHCoQ= @@ -46,8 +46,10 @@ github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa02 github.com/klauspost/cpuid/v2 v2.0.10/go.mod h1:g2LTdtYhdyuGPqyWyv7qRAmj1WBqxuObKfj5c0PQa7c= github.com/klauspost/cpuid/v2 v2.0.12 h1:p9dKCg8i4gmOxtv35DvrYoWqYzQrvEVdjQ762Y0OqZE= github.com/klauspost/cpuid/v2 v2.0.12/go.mod h1:g2LTdtYhdyuGPqyWyv7qRAmj1WBqxuObKfj5c0PQa7c= +github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/lithammer/fuzzysearch v1.1.5 h1:Ag7aKU08wp0R9QCfF4GoGST9HbmAIeLP7xwMrOBEp1c= github.com/lithammer/fuzzysearch v1.1.5/go.mod h1:1R1LRNk7yKid1BaQkmuLQaHruxcC4HmAH30Dh61Ih1Q= @@ -90,12 +92,13 @@ github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81P github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/urfave/cli/v2 v2.4.10 h1:4qBCceIE7UP0T1qwloKzyyt1k/FcVNl2V6HBroizVRE= github.com/urfave/cli/v2 v2.4.10/go.mod h1:oDzoM7pVwz6wHn5ogWgFUU1s4VJayeQS+aEZDqXIEJs= +github.com/veqryn/slog-context v0.7.0 h1:Ne7ajlR6Mjs2rQQtpg8k0eO6krR5wzpareh5VpV+V2s= +github.com/veqryn/slog-context v0.7.0/go.mod h1:E+qpdyiQs2YKRxFnX1JjpdFE1z3Ka94Kem2q9ZG6Jjo= github.com/xo/terminfo v0.0.0-20210125001918-ca9a967f8778/go.mod h1:2MuV+tbUrU1zIOPMxZ5EncGwgmMJsa+9ucAQZXxsObs= github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no= github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM= @@ -110,10 +113,6 @@ golang.org/x/sys v0.0.0-20211013075003-97ac67df715c/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20211025201205-69cdffdb9359/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220319134239-a9b59b0215f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220615213510-4f61da869c0c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.1.0 h1:kunALQeHf1/185U1i0GOB/fy1IPRDDpuoOOqRReG57U= -golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.16.0 h1:xWw16ngr6ZMtmxDyKyIgsE93KNKz5HKmMa3b8ALHidU= -golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.19.0 h1:q5f1RH2jigJ1MoAWp2KTp3gm5zAGFUTarQZ5U386+4o= golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20210220032956-6a3ed077a48d/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= @@ -125,6 +124,7 @@ golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/djherbis/times.v1 v1.3.0 h1:uxMS4iMtH6Pwsxog094W0FYldiNnfY/xba00vq6C2+o= gopkg.in/djherbis/times.v1 v1.3.0/go.mod h1:AQlg6unIsrsCEdQYhTzERy542dz6SFdQFZFv6mUY0P8= diff --git a/internal/config/config.go b/internal/config/config.go index 4394687..66b8adc 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -30,50 +30,51 @@ var conf *Config // ExiftoolOpts defines supported options for customizing Exitool's output type ExiftoolOpts struct { - API string `long:"api"` // corresponds to the `-api` flag - Charset string `long:"charset"` // corresponds to the `-charset` flag - CoordFormat string `long:"coordFormat"` // corresponds to the `-coordFormat` flag - DateFormat string `long:"dateFormat"` // corresponds to the `-dateFormat` flag - ExtractEmbedded bool `long:"extractEmbedded"` // corresponds to the `-extractEmbedded` flag + API string `long:"api" json:"api"` // corresponds to the `-api` flag + Charset string `long:"charset" json:"charset"` // corresponds to the `-charset` flag + CoordFormat string `long:"coordFormat" json:"coord_format"` // corresponds to the `-coordFormat` flag + DateFormat string `long:"dateFormat" json:"date_format"` // corresponds to the `-dateFormat` flag + ExtractEmbedded bool `long:"extractEmbedded" json:"extract_embedded"` // corresponds to the `-extractEmbedded` flag } // Config represents the program configuration. type Config struct { - Date time.Time - Stdin io.Reader - Stderr io.Writer - Stdout io.Writer - SearchRegex *regexp.Regexp - CSVFilename string - Sort string - Replacement string - WorkingDir string - FindSlice []string - ExcludeRegex *regexp.Regexp - ReplacementSlice []string - FilesAndDirPaths []string - NumberOffset []int - MaxDepth int - StartNumber int - ReplaceLimit int - Recursive bool - IgnoreCase bool - ReverseSort bool - OnlyDir bool - Revert bool - IncludeDir bool - IgnoreExt bool - AllowOverwrites bool - Verbose bool - IncludeHidden bool - Quiet bool - AutoFixConflicts bool - Exec bool - StringLiteralMode bool - SimpleMode bool - JSON bool - Interactive bool - ExiftoolOpts ExiftoolOpts + Date time.Time `json:"date"` + Stdin io.Reader `json:"-"` + Stderr io.Writer `json:"-"` + Stdout io.Writer `json:"-"` + SearchRegex *regexp.Regexp `json:"search_regex"` + CSVFilename string `json:"csv_filename"` + Sort string `json:"sort"` + Replacement string `json:"replacement"` + WorkingDir string `json:"working_dir"` + FindSlice []string `json:"find_slice"` + ExcludeRegex *regexp.Regexp `json:"exclude_regex"` + ReplacementSlice []string `json:"replacement_slice"` + FilesAndDirPaths []string `json:"files_and_dir_paths"` + NumberOffset []int `json:"number_offset"` + MaxDepth int `json:"max_depth"` + StartNumber int `json:"start_number"` + ReplaceLimit int `json:"replace_limit"` + Recursive bool `json:"recursive"` + IgnoreCase bool `json:"ignore_case"` + ReverseSort bool `json:"reverse_sort"` + OnlyDir bool `json:"only_dir"` + Revert bool `json:"revert"` + IncludeDir bool `json:"include_dir"` + IgnoreExt bool `json:"ignore_ext"` + AllowOverwrites bool `json:"allow_overwrites"` + Verbose bool `json:"verbose"` + IncludeHidden bool `json:"include_hidden"` + Quiet bool `json:"quiet"` + AutoFixConflicts bool `json:"auto_fix_conflicts"` + Exec bool `json:"exec"` + StringLiteralMode bool `json:"string_literal_mode"` + SimpleMode bool `json:"simple_mode"` + JSON bool `json:"json"` + Interactive bool `json:"interactive"` + Debug bool `json:"debug"` + ExiftoolOpts ExiftoolOpts `json:"exiftool_opts"` } // SetFindStringRegex compiles a regular expression for the @@ -119,6 +120,7 @@ func (c *Config) setOptions(ctx *cli.Context) error { c.ReplacementSlice = ctx.StringSlice("replace") c.CSVFilename = ctx.String("csv") c.Revert = ctx.Bool("undo") + c.Debug = ctx.Bool("debug") c.FilesAndDirPaths = ctx.Args().Slice() if len(ctx.String("exiftool-opts")) != 0 { diff --git a/internal/file/file.go b/internal/file/file.go index e92a493..43d2df1 100644 --- a/internal/file/file.go +++ b/internal/file/file.go @@ -1,6 +1,7 @@ package file import ( + "log/slog" "path/filepath" "github.com/ayoisaiah/f2/internal/status" @@ -19,11 +20,28 @@ type Change struct { // RelTargetPath is BaseDir + Target RelTargetPath string `json:"-"` CSVRow []string `json:"-"` - Index int `json:"-"` + Index int `json:"-"` // TODO: Rename to position? IsDir bool `json:"is_dir"` WillOverwrite bool `json:"will_overwrite"` } +func (c Change) LogValue() slog.Value { + return slog.GroupValue( + slog.Any("error", c.Error), + slog.String("original_source", c.OriginalSource), + slog.Any("status", c.Status), + slog.String("base_dir", c.BaseDir), + slog.String("source", c.Source), + slog.String("target", c.Target), + slog.String("rel_source_path", c.RelSourcePath), + slog.String("rel_target_path", c.RelTargetPath), + slog.Any("csv_row", c.CSVRow), + slog.Int("index", c.Index), + slog.Bool("is_dir", c.IsDir), + slog.Bool("will_overwrite", c.WillOverwrite), + ) +} + func (c *Change) SourcePath() string { return filepath.Join(c.BaseDir, c.Source) } diff --git a/internal/sortfiles/sortfiles.go b/internal/sortfiles/sortfiles.go index beeb852..963fbc5 100644 --- a/internal/sortfiles/sortfiles.go +++ b/internal/sortfiles/sortfiles.go @@ -41,9 +41,9 @@ func FilesBeforeDirs(changes []*file.Change, revert bool) []*file.Change { return changes } -// DirectoryHierarchy ensures all files in the same directory are sorted before +// EnforceHierarchicalOrder ensures all files in the same directory are sorted before // children directories. -func DirectoryHierarchy(changes []*file.Change) []*file.Change { +func EnforceHierarchicalOrder(changes []*file.Change) []*file.Change { sort.SliceStable(changes, func(i, j int) bool { compareElement1 := changes[i] compareElement2 := changes[j] diff --git a/replace/replace.go b/replace/replace.go index 7ed8a17..7bf0f53 100644 --- a/replace/replace.go +++ b/replace/replace.go @@ -4,13 +4,18 @@ package replace import ( + "context" "errors" + "fmt" + "log/slog" "math" "path/filepath" "regexp" "strconv" "strings" + slogctx "github.com/veqryn/slog-context" + "github.com/ayoisaiah/f2/internal/config" "github.com/ayoisaiah/f2/internal/file" "github.com/ayoisaiah/f2/internal/pathutil" @@ -25,6 +30,10 @@ type numbersToSkip struct { max int } +func (s numbersToSkip) LogValue() slog.Value { + return slog.StringValue(fmt.Sprintf("min: %d, max:%d", s.min, s.max)) +} + type indexVarMatch struct { regex *regexp.Regexp index string @@ -38,6 +47,20 @@ type indexVarMatch struct { startNumber int } +func (v indexVarMatch) LogAttr() slog.Attr { + return slog.Group( + "index_var_match", + slog.String("regex", v.regex.String()), + slog.String("index", v.index), + slog.String("format", v.format), + slog.String("skip", fmt.Sprintf("%v", v.skip)), + slog.Bool("step_set", v.step.isSet), + slog.Int("step_value", v.step.value), + slog.Int("start_number", v.startNumber), + slog.Any("val", v.val), + ) +} + type indexVars struct { capturVarIndex []int matches []indexVarMatch @@ -52,6 +75,17 @@ type transformVarMatch struct { val []string } +func (v transformVarMatch) LogAttr() slog.Attr { + return slog.Group( + "transform_var_match", + slog.String("regex", v.regex.String()), + slog.String("token", v.token), + slog.String("capture_var", v.captureVar), + slog.String("input_str", v.inputStr), + slog.Any("val", v.val), + ) +} + type transformVars struct { matches []transformVarMatch } @@ -63,6 +97,16 @@ type exiftoolVarMatch struct { val []string } +func (v exiftoolVarMatch) LogAttr() slog.Attr { + return slog.Group( + "exiftool_var_match", + slog.String("regex", v.regex.String()), + slog.String("transform_token", v.transformToken), + slog.String("attr", v.attr), + slog.Any("val", v.val), + ) +} + type exiftoolVars struct { matches []exiftoolVarMatch } @@ -75,6 +119,16 @@ type exifVarMatch struct { val []string } +func (v exifVarMatch) LogAttr() slog.Attr { + return slog.Group( + "exif_var_match", + slog.String("regex", v.regex.String()), + slog.String("transform_token", v.transformToken), + slog.String("attr", v.attr), + slog.Any("val", v.val), + ) +} + type exifVars struct { matches []exifVarMatch } @@ -86,6 +140,16 @@ type id3VarMatch struct { val []string } +func (v id3VarMatch) LogAttr() slog.Attr { + return slog.Group( + "date_var_match", + slog.String("regex", v.regex.String()), + slog.String("transform_token", v.transformToken), + slog.String("tag", v.tag), + slog.Any("val", v.val), + ) +} + type id3Vars struct { matches []id3VarMatch } @@ -98,6 +162,17 @@ type dateVarMatch struct { val []string } +func (v dateVarMatch) LogAttr() slog.Attr { + return slog.Group( + "date_var_match", + slog.String("regex", v.regex.String()), + slog.String("transform_token", v.transformToken), + slog.String("attr", v.attr), + slog.Any("val", v.val), + slog.String("token", v.token), + ) +} + type dateVars struct { matches []dateVarMatch } @@ -109,6 +184,16 @@ type hashVarMatch struct { val []string } +func (v hashVarMatch) LogAttr() slog.Attr { + return slog.Group( + "hash_var_match", + slog.String("regex", v.regex.String()), + slog.String("transform_token", v.transformToken), + slog.Any("val", v.val), + slog.Any("length", v.hashFn), + ) +} + type hashVars struct { matches []hashVarMatch } @@ -117,10 +202,20 @@ type randomVarMatch struct { regex *regexp.Regexp characters string transformToken string - val []string + val []string // TODO: rename this property length int } +func (v randomVarMatch) LogAttr() slog.Attr { + return slog.Group( + "random_var_match", + slog.String("regex", v.regex.String()), + slog.String("transform_token", v.transformToken), + slog.Any("val", v.val), + slog.Int("length", v.length), + ) +} + type randomVars struct { matches []randomVarMatch } @@ -131,6 +226,15 @@ type csvVarMatch struct { column int } +func (v csvVarMatch) LogAttr() slog.Attr { + return slog.Group( + "csv_var_match", + slog.String("regex", v.regex.String()), + slog.String("transform_token", v.transformToken), + slog.Int("column", v.column), + ) +} + type csvVars struct { submatches [][]string values []csvVarMatch @@ -141,6 +245,14 @@ type filenameVarMatch struct { transformToken string } +func (v filenameVarMatch) LogAttr() slog.Attr { + return slog.Group( + "filename_var_match", + slog.String("regex", v.regex.String()), + slog.String("transform_token", v.transformToken), + ) +} + type filenameVars struct { matches []filenameVarMatch } @@ -150,6 +262,14 @@ type extVarMatch struct { transformToken string } +func (e extVarMatch) LogAttr() slog.Attr { + return slog.Group( + "ext_var_match", + slog.String("regex", e.regex.String()), + slog.String("transform_token", e.transformToken), + ) +} + type extVars struct { matches []extVarMatch } @@ -160,6 +280,15 @@ type parentDirVarMatch struct { parent int } +func (v parentDirVarMatch) LogAttr() slog.Attr { + return slog.Group( + "parent_dir_var_match", + slog.String("regex", v.regex.String()), + slog.String("transform_token", v.transformToken), + slog.Int("parent", v.parent), + ) +} + type parentDirVars struct { matches []parentDirVarMatch } @@ -179,6 +308,69 @@ type variables struct { parentDir parentDirVars } +func (v variables) LogValue() slog.Value { + var slogAttr []slog.Attr + + for _, v := range v.filename.matches { + slogAttr = append(slogAttr, v.LogAttr()) + } + + for _, v := range v.ext.matches { + slogAttr = append(slogAttr, v.LogAttr()) + } + + for _, v := range v.parentDir.matches { + slogAttr = append(slogAttr, v.LogAttr()) + } + + if len(v.csv.submatches) > 0 { + slogAttr = append( + slogAttr, + slog.Any("csv_submatches", v.csv.submatches), + ) + } + + for _, v := range v.csv.values { + slogAttr = append(slogAttr, v.LogAttr()) + } + + for _, v := range v.transform.matches { + slogAttr = append(slogAttr, v.LogAttr()) + } + + for _, v := range v.random.matches { + slogAttr = append(slogAttr, v.LogAttr()) + } + + for _, v := range v.date.matches { + slogAttr = append(slogAttr, v.LogAttr()) + } + + for _, v := range v.hash.matches { + slogAttr = append(slogAttr, v.LogAttr()) + } + + for _, v := range v.id3.matches { + slogAttr = append(slogAttr, v.LogAttr()) + } + + for _, v := range v.index.matches { + slogAttr = append(slogAttr, v.LogAttr()) + } + + for _, v := range v.exiftool.matches { + slogAttr = append(slogAttr, v.LogAttr()) + } + + for _, v := range v.exif.matches { + slogAttr = append(slogAttr, v.LogAttr()) + } + + return slog.GroupValue( + slogAttr..., + ) +} + // getCSVVars retrieves all the csv variables in the replacement // string if any. func getCSVVars(replacementInput string) (csvVars, error) { @@ -852,8 +1044,14 @@ func replaceMatches( return nil, err } + slog.Debug("extracted variables", slog.Any("vars", vars)) + if len(vars.index.matches) > 0 { - matches = sortfiles.DirectoryHierarchy(matches) + matches = sortfiles.EnforceHierarchicalOrder(matches) + slog.Debug( + "sorted matches based on directory level", + slog.Any("matches", matches), + ) } for i := range matches { @@ -868,12 +1066,16 @@ func replaceMatches( change.Target = replaceString(conf, originalName) + slog.Debug("regex replacement result", slog.Any("change", change)) + // Replace any variables present with their corresponding values err = replaceVariables(conf, change, &vars) if err != nil { return nil, err } + slog.Debug("variable replacement result", slog.Any("change", change)) + // Reattach the original extension to the new file name if conf.IgnoreExt && !change.IsDir { change.Target += fileExt @@ -895,6 +1097,14 @@ func handleReplacementChain( replacementSlice := conf.ReplacementSlice for i, v := range replacementSlice { + ctx := slogctx.Append(context.Background(), + slog.String("find_arg", conf.SearchRegex.String()), + slog.String("replace_arg", v), + slog.Int("replacement_index", i), + ) + + slog.DebugContext(ctx, "executing find and replace") + config.SetReplacement(v) var err error @@ -904,6 +1114,17 @@ func handleReplacementChain( return nil, err } + slog.DebugContext( + ctx, + "find/replace result", + slog.Any("matches", matches), + ) + + if len(replacementSlice) == 1 || + (i > 0 && i == len(replacementSlice)-1) { + return matches, nil + } + for j := range matches { change := matches[j] @@ -912,19 +1133,17 @@ func handleReplacementChain( if i != len(replacementSlice)-1 { matches[j].Source = change.Target } - - // After the last replacement, update the Source - // back to the original - if i > 0 && i == len(replacementSlice)-1 { - matches[j].Source = change.OriginalSource - } } - if i != len(replacementSlice)-1 { - err := conf.SetFindStringRegex(i + 1) - if err != nil { - return nil, err - } + slog.DebugContext( + ctx, + "updated sources for next find/replace", + slog.Any("matches", matches), + ) + + err = conf.SetFindStringRegex(i + 1) + if err != nil { + return nil, err } } @@ -940,10 +1159,21 @@ func Replace( var err error if conf.Sort != "" { + slog.Debug( + "sorting matches before replacement", + slog.String("sort", conf.Sort), + slog.Bool("reverse_sort", conf.ReverseSort), + ) + changes, err = sortfiles.Changes(changes, conf.Sort, conf.ReverseSort) if err != nil { return nil, err } + + slog.Debug( + "updated match order according to sort value", + slog.Any("sorted_matches", changes), + ) } changes, err = handleReplacementChain(conf, changes)