diff --git a/cmd/f2/main.go b/cmd/f2/main.go index 2dd7f6d..c4f4f2e 100644 --- a/cmd/f2/main.go +++ b/cmd/f2/main.go @@ -3,6 +3,8 @@ package main import ( "os" + "github.com/pterm/pterm" + f2 "github.com/ayoisaiah/f2/src" ) @@ -13,6 +15,8 @@ func run(args []string) error { func main() { err := run(os.Args) if err != nil { + pterm.EnableOutput() + pterm.Error.Println(err) os.Exit(1) } } diff --git a/src/app.go b/src/app.go index d8323a3..5af3471 100644 --- a/src/app.go +++ b/src/app.go @@ -12,32 +12,7 @@ import ( func init() { // Override the default help template - cli.AppHelpTemplate = `DESCRIPTION: - {{.Usage}} - -USAGE: - {{.HelpName}} {{if .UsageText}}{{ .UsageText }}{{end}} -{{if len .Authors}} -AUTHOR: - {{range .Authors}}{{ . }}{{end}}{{end}} -{{if .Version}} -VERSION: - {{.Version}}{{end}} -{{if .VisibleFlags}} -FLAGS:{{range .VisibleFlags}}{{ if (eq .Name "find" "undo" "replace") }} - {{if .Aliases}}-{{range $element := .Aliases}}{{$element}},{{end}}{{end}} --{{.Name}} {{.DefaultText}} - {{.Usage}} - {{end}}{{end}} -OPTIONS:{{range .VisibleFlags}}{{ if not (eq .Name "find" "replace" "undo") }} - {{if .Aliases}}-{{range $element := .Aliases}}{{$element}},{{end}}{{end}} --{{.Name}} {{ .DefaultText }} - {{.Usage}} - {{end}}{{end}}{{end}} -DOCUMENTATION: - https://github.com/ayoisaiah/f2/wiki - -WEBSITE: - https://github.com/ayoisaiah/f2 -` + cli.AppHelpTemplate = helpText() // Override the default version printer oldVersionPrinter := cli.VersionPrinter @@ -126,68 +101,68 @@ func GetApp() *cli.App { Email: "ayo@freshman.tech", }, }, - Usage: "F2 is a command-line tool for batch renaming multiple files and directories quickly and safely", - UsageText: "FLAGS [OPTIONS] [PATHS...]", - Version: "v1.7.0", + Usage: "F2 is a command-line tool for batch renaming multiple files and directories quickly and safely.", + UsageText: "FLAGS [OPTIONS] [PATHS TO FILES OR DIRECTORIES...]", + Version: "v1.7.1", EnableBashCompletion: true, Flags: []cli.Flag{ &cli.StringSliceFlag{ Name: "find", Aliases: []string{"f"}, - Usage: "Search pattern. Treated as a regular expression by default unless --string-mode is also used. If omitted, it defaults to the entire file name (including the extension).", + Usage: "Search pattern. Treated as a regular expression by default unless --string-mode is also used.\n\t\t\t\tDefaults to the entire file name if omitted.", DefaultText: "", }, &cli.StringSliceFlag{ Name: "replace", Aliases: []string{"r"}, - Usage: "Replacement string. If omitted, defaults to an empty string. Supports built-in and regex capture variables. Learn more about variable support here: https://github.com/ayoisaiah/f2/wiki/Built-in-variables", + Usage: "Replacement string. If omitted, defaults to an empty string. Supports several kinds of variables.\n\t\t\t\tLearn more: https://github.com/ayoisaiah/f2/wiki/Built-in-variables.", DefaultText: "", }, + &cli.BoolFlag{ + Name: "undo", + Aliases: []string{"u"}, + Usage: "Undo the last operation performed in the current working directory if possible.\n\t\t\t\tLearn more: https://github.com/ayoisaiah/f2/wiki/Undoing-a-renaming-operation.", + }, &cli.StringFlag{ Name: "csv", - Usage: "Load a CSV file, and rename according to its contents. File names will be matched according to the content in the first column", + Usage: "Load a CSV file, and rename according to its contents.\n\t\t\t\tLearn more: https://github.com/ayoisaiah/f2/wiki/Renaming-from-a-CSV-file.", DefaultText: "", }, &cli.IntFlag{ Name: "replace-limit", Aliases: []string{"l"}, - Usage: "Limit the number of replacements to be made on the file name (replaces all matches if set to 0). Can be set to a negative integer to start replacing from the end of the file name.", + Usage: "Limit the number of replacements to be made on each matched file (replaces all matches if set to 0).\n\t\t\t\tCan be set to a negative integer to start replacing from the end of the file name.", Value: 0, DefaultText: "", }, &cli.BoolFlag{ Name: "string-mode", Aliases: []string{"s"}, - Usage: "Opt into string literal mode. The presence of this flag causes the search pattern to be treated as a non-regex string.", + Usage: "Treats the search pattern as a non-regex string.", }, &cli.StringSliceFlag{ Name: "exclude", Aliases: []string{"E"}, - Usage: "Exclude files/directories that match the given search pattern. Treated as a regular expression. Multiple exclude patterns can be specified.", + Usage: "Exclude files/directories that match the given search pattern. Treated as a regular expression.\n\t\t\t\tMultiple exclude patterns can be specified by repeating this option.", DefaultText: "", }, &cli.BoolFlag{ Name: "exec", Aliases: []string{"x"}, - Usage: "Execute the batch renaming operation. This will commit the changes to your filesystem.", + Usage: "Commit the renaming operation to the filesystem.", }, &cli.BoolFlag{ Name: "recursive", Aliases: []string{"R"}, - Usage: "Recursively traverse all directories when searching for matches. Use the --max-depth flag to control the maximum allowed depth (no limit by default).", + Usage: "Recursively traverse directories when searching for matches.", }, &cli.UintFlag{ Name: "max-depth", Aliases: []string{"m"}, - Usage: "Positive integer indicating the maximum depth for a recursive search (set to 0 for no limit).", + Usage: "Indicates the maximum depth for a recursive search (set to 0 by default for no limit).", Value: 0, DefaultText: "", }, - &cli.BoolFlag{ - Name: "undo", - Aliases: []string{"u"}, - Usage: "Undo the last operation performed in the current working directory if possible. Learn more: https://github.com/ayoisaiah/f2/wiki/Undoing-a-renaming-operation", - }, &cli.StringFlag{ Name: "sort", Usage: `Sort the matches according to the provided ''. @@ -208,12 +183,12 @@ func GetApp() *cli.App { &cli.BoolFlag{ Name: "ignore-case", Aliases: []string{"i"}, - Usage: "When this flag is provided, the given pattern will be searched case insensitively.", + Usage: "Search for matches case insensitively.", }, &cli.BoolFlag{ Name: "quiet", Aliases: []string{"q"}, - Usage: "Activate silent mode which doesn't print out any information including errors", + Usage: "Don't print out any information (except errors).", }, &cli.BoolFlag{ Name: "ignore-ext", @@ -223,55 +198,58 @@ func GetApp() *cli.App { &cli.BoolFlag{ Name: "include-dir", Aliases: []string{"d"}, - Usage: "Include directories when searching for matches as they are exempted by default.", + Usage: "Include directories (they are exempted by default).", }, &cli.BoolFlag{ Name: "only-dir", Aliases: []string{"D"}, - Usage: "Rename only directories, not files (implies --include-dir)", + Usage: "Rename only directories, not files (implies --include-dir).", }, &cli.BoolFlag{ Name: "hidden", Aliases: []string{"H"}, - Usage: "Include hidden directories and files in the matches (they are skipped by default). A hidden file or directory is one whose name starts with a period (all operating systems) or one whose hidden attribute is set to true (Windows only)", + Usage: "Include hidden files (they are skipped by default).", }, &cli.BoolFlag{ Name: "verbose", Aliases: []string{"V"}, - Usage: "Enable verbose output. Each renaming operation will be printed out if this flag is provided.", + Usage: "Enable verbose output.", }, &cli.BoolFlag{ Name: "no-color", - Usage: "Disable coloured output", + Usage: "Disable coloured output.", }, &cli.BoolFlag{ Name: "fix-conflicts", Aliases: []string{"F"}, - Usage: "Automatically fix conflicts based on predefined rules. Learn more: https://github.com/ayoisaiah/f2/wiki/Validation-and-conflict-detection", + Usage: "Automatically fix conflicts based on predefined rules.\n\t\t\t\tLearn more: https://github.com/ayoisaiah/f2/wiki/Validation-and-conflict-detection.", }, &cli.BoolFlag{ Name: "allow-overwrites", - Usage: "Allow the overwriting of existing files", + Usage: "Allow the overwriting of existing files.", }, }, UseShortOptionHandling: true, Action: func(c *cli.Context) error { + if c.NumFlags() == 0 { + pterm.Println(shortHelp(c.App)) + os.Exit(1) + } + if c.Bool("no-color") { disableStyling() } - op, err := newOperation(c) - if err != nil { - printError(false, err) - return err + if c.Bool("quiet") { + pterm.DisableOutput() } - err = op.run() + op, err := newOperation(c) if err != nil { - printError(op.quiet, err) + return err } - return err + return op.run() }, } } diff --git a/src/help.go b/src/help.go new file mode 100644 index 0000000..f9469e5 --- /dev/null +++ b/src/help.go @@ -0,0 +1,80 @@ +package f2 + +import ( + "fmt" + + "github.com/pterm/pterm" + "github.com/urfave/cli/v2" +) + +func helpText() string { + description := fmt.Sprintf( + "%s\n\t\t{{.Usage}}\n\n", + pterm.Yellow("DESCRIPTION"), + ) + usage := fmt.Sprintf( + "%s\n\t\t{{.HelpName}} {{if .UsageText}}{{ .UsageText }}{{end}}\n\n", + pterm.Yellow("USAGE"), + ) + author := fmt.Sprintf( + "{{if len .Authors}}%s\n\t\t{{range .Authors}}{{ . }}{{end}}{{end}}\n\n", + pterm.Yellow("AUTHOR"), + ) + + version := fmt.Sprintf( + "{{if .Version}}%s\n\t\t{{.Version}}{{end}}\n\n", + pterm.Yellow("VERSION"), + ) + flags := fmt.Sprintf( + "{{if .VisibleFlags}}%s\n{{range .VisibleFlags}}{{ if (eq .Name `find` `undo` `replace` `csv`) }}\t\t{{if .Aliases}}-{{range $element := .Aliases}}%s,{{end}}{{end}} %s\n\t\t\t\t{{.Usage}}\n\n{{end}}{{end}}", + pterm.Yellow("FLAGS"), + pterm.Green("{{$element}}"), + pterm.Green("--{{.Name}} {{.DefaultText}}"), + ) + options := fmt.Sprintf( + "%s\n{{range .VisibleFlags}}{{ if not (eq .Name `find` `undo` `replace` `csv`) }}\t\t{{if .Aliases}}-{{range $element := .Aliases}}%s,{{end}}{{end}} %s\n\t\t\t\t{{.Usage}}\n\n{{end}}{{end}}{{end}}", + pterm.Yellow("OPTIONS"), + pterm.Green("{{$element}}"), + pterm.Green("--{{.Name}} {{.DefaultText}}"), + ) + + docs := fmt.Sprintf( + "%s\n\t\t%s\n\n", + pterm.Yellow("DOCUMENTATION"), + "https://github.com/ayoisaiah/f2/wiki", + ) + website := fmt.Sprintf( + "%s\n\t\thttps://github.com/ayoisaiah/f2\n", + pterm.Yellow("WEBSITE"), + ) + + return description + usage + author + version + flags + options + docs + website +} + +func shortHelp(app *cli.App) string { + heading := fmt.Sprintf( + "F2 — Command-line bulk renaming tool [version %s]\n\n", + app.Version, + ) + + usage := fmt.Sprintf("Usage: %s\n", app.UsageText) + + description := ` +F2 helps you organise your filesystem through batch renaming. +The simplest usage is to do a basic find and replace: + +$ f2 -f 'Screenshot' -r 'Image' ++--------------------+---------------+--------+ +| INPUT | OUTPUT | STATUS | ++--------------------+---------------+--------+ +| Screenshot (1).png | Image (1).png | ok | +| Screenshot (2).png | Image (2).png | ok | +| Screenshot (3).png | Image (3).png | ok | ++--------------------+---------------+--------+ + +For more usage examples, see: https://github.com/ayoisaiah/f2/wiki + +Use f2 --help to see the full list of options.` + + return heading + usage + description +} diff --git a/src/operation.go b/src/operation.go index 1d62ce3..ef344cc 100644 --- a/src/operation.go +++ b/src/operation.go @@ -83,7 +83,6 @@ type Operation struct { maxDepth int sort string reverseSort bool - quiet bool errors []renameError revert bool numberOffset []int @@ -91,6 +90,7 @@ type Operation struct { allowOverwrites bool verbose bool csvFilename string + quiet bool } type backupFile struct { @@ -366,9 +366,7 @@ func (op *Operation) noMatches() { msg = "No operations to undo" } - if !op.quiet { - pterm.Info.Println(msg) - } + pterm.Info.Println(msg) } // execute applies the renaming operation to the filesystem. @@ -389,7 +387,7 @@ func (op *Operation) execute() error { return op.backup() } - if !op.quiet && !op.revert { + if !op.revert { pterm.Info.Println("No files were renamed") } @@ -400,10 +398,11 @@ func (op *Operation) execute() error { func (op *Operation) dryRun() { if !op.quiet { op.printChanges() - pterm.Info.Printfln( - "Use the -x or --exec flag to apply the above changes", - ) } + + pterm.Info.Printfln( + "Use the -x or --exec flag to apply the above changes", + ) } // apply prints the changes to be made in dry-run mode @@ -419,9 +418,7 @@ func (op *Operation) apply() error { op.detectConflicts() if len(op.conflicts) > 0 && !op.fixConflicts { - if !op.quiet { - op.reportConflicts() - } + op.reportConflicts() return errConflictDetected } @@ -762,7 +759,11 @@ func (op *Operation) handleCSV(paths map[string][]fs.DirEntry) error { } if !found && op.verbose { - pterm.Warning.Printfln("Source file '%s' was not found, so row '%d' was skipped", source, i+1) + pterm.Warning.Printfln( + "Source file '%s' was not found, so row '%d' was skipped", + source, + i+1, + ) } loop: @@ -822,12 +823,12 @@ func setOptions(op *Operation, c *cli.Context) error { op.stringLiteralMode = c.Bool("string-mode") op.excludeFilter = c.StringSlice("exclude") op.maxDepth = int(c.Uint("max-depth")) - op.quiet = c.Bool("quiet") op.revert = c.Bool("undo") op.verbose = c.Bool("verbose") op.allowOverwrites = c.Bool("allow-overwrites") op.replaceLimit = c.Int("replace-limit") op.csvFilename = c.String("csv") + op.quiet = c.Bool("quiet") // Sorting if c.String("sort") != "" { diff --git a/src/operation_test.go b/src/operation_test.go index 0840009..b16354b 100644 --- a/src/operation_test.go +++ b/src/operation_test.go @@ -17,6 +17,7 @@ import ( "github.com/adrg/xdg" "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" + "github.com/pterm/pterm" "github.com/urfave/cli/v2" ) @@ -151,6 +152,8 @@ func action(args []string) (ActionResult, error) { op.quiet = true + pterm.DisableOutput() + result.applyError = op.run() result.changes = op.matches result.backupFile = backupFilePath diff --git a/src/utils.go b/src/utils.go index 97eebfc..8fc21e4 100644 --- a/src/utils.go +++ b/src/utils.go @@ -7,15 +7,8 @@ import ( "path/filepath" "github.com/olekukonko/tablewriter" - "github.com/pterm/pterm" ) -func printError(silent bool, err error) { - if !silent { - pterm.Error.Println(err) - } -} - func removeHidden( de []os.DirEntry, baseDir string,