From ff0f9c9a872f908fbc12910753a59073a94d7ac4 Mon Sep 17 00:00:00 2001 From: Laura Date: Wed, 8 Oct 2025 23:26:48 +0200 Subject: [PATCH] patterns and help topics --- cmd/ffwebp/main.go | 28 +++++++++- cmd/ffwebp/pattern.go | 47 ++++++++++++++++ internal/help/help.go | 93 +++++++++++++++++++++++++++++++ internal/help/topics/patterns.txt | 28 ++++++++++ 4 files changed, 193 insertions(+), 3 deletions(-) create mode 100644 internal/help/help.go create mode 100644 internal/help/topics/patterns.txt diff --git a/cmd/ffwebp/main.go b/cmd/ffwebp/main.go index 49fd60c..24950aa 100644 --- a/cmd/ffwebp/main.go +++ b/cmd/ffwebp/main.go @@ -14,6 +14,7 @@ import ( _ "github.com/coalaura/ffwebp/internal/builtins" "github.com/coalaura/ffwebp/internal/codec" "github.com/coalaura/ffwebp/internal/effects" + "github.com/coalaura/ffwebp/internal/help" "github.com/coalaura/ffwebp/internal/logx" "github.com/coalaura/ffwebp/internal/opts" "github.com/nfnt/resize" @@ -33,7 +34,7 @@ func main() { &cli.StringFlag{ Name: "output", Aliases: []string{"o"}, - Usage: "output file, directory, or pattern (\"-\" = stdout)", + Usage: "output file, directory, or pattern (\"-\" = stdout, supports %d and templates)", Value: "-", }, &cli.IntFlag{ @@ -104,6 +105,9 @@ func main() { EnableShellCompletion: true, UseShortOptionHandling: true, Suggest: true, + Commands: []*cli.Command{ + help.Command(), + }, } if err := app.Run(context.Background(), os.Args); err != nil { @@ -164,11 +168,29 @@ func run(_ context.Context, cmd *cli.Command) error { var outputs []string if len(inputs) == 1 { - outputs = []string{output} + out := output + + if out != "-" { + if hasTemplate(out) { + out = formatTemplate(out, inputs[0], 0, startNum) + } else if hasSeq(out) { + out = formatSeq(out, 0, startNum) + } + } + + outputs = []string{out} } else { switch { case output == "-": return fmt.Errorf("multiple inputs require an output pattern or directory, not '-' ") + case hasTemplate(output): + outs := make([]string, len(inputs)) + + for i := range inputs { + outs[i] = formatTemplate(output, inputs[i], i, startNum) + } + + outputs = outs case hasSeq(output): outs := make([]string, len(inputs)) @@ -196,7 +218,7 @@ func run(_ context.Context, cmd *cli.Command) error { outs := make([]string, len(inputs)) for i := range inputs { - outs[i] = output + outs[i] = outDir } outputs = outs diff --git a/cmd/ffwebp/pattern.go b/cmd/ffwebp/pattern.go index 6f1382b..d91c5dc 100644 --- a/cmd/ffwebp/pattern.go +++ b/cmd/ffwebp/pattern.go @@ -7,6 +7,7 @@ import ( "regexp" "sort" "strconv" + "strings" ) func hasSeq(s string) bool { @@ -161,3 +162,49 @@ func expandSeq(pat string) ([]string, error) { return result, nil } + +func hasTemplate(s string) bool { + if !strings.Contains(s, "{") { + return false + } + return strings.Contains(s, "{/.}") || + strings.Contains(s, "{/}") || + strings.Contains(s, "{.}") || + strings.Contains(s, "{}") || + strings.Contains(s, "{//}") || + strings.Contains(s, "{#}") +} + +func trimExt(p string) string { + return strings.TrimSuffix(p, filepath.Ext(p)) +} + +func formatTemplate(pattern, in string, idx, startNum int) string { + if hasSeq(pattern) { + pattern = formatSeq(pattern, idx, startNum) + } + + dir := filepath.Dir(in) + base := filepath.Base(in) + + // Special-case "-" (stdin) + if in == "-" { + dir = "." + base = "stdin" + } + + fullNoExt := trimExt(in) + baseNoExt := trimExt(base) + n := idx + startNum - 1 + + r := strings.NewReplacer( + "{/.}", baseNoExt, + "{//}", dir, + "{/}", base, + "{.}", fullNoExt, + "{}", in, + "{#}", strconv.Itoa(n), + ) + + return r.Replace(pattern) +} diff --git a/internal/help/help.go b/internal/help/help.go new file mode 100644 index 0000000..20556ad --- /dev/null +++ b/internal/help/help.go @@ -0,0 +1,93 @@ +package help + +import ( + "context" + _ "embed" + "fmt" + "io" + "strings" + + "github.com/urfave/cli/v3" +) + +type HelpTopic struct { + Name string + Description string + Content []byte +} + +var ( + //go:embed topics/patterns.txt + topicPatterns []byte + + topics = []HelpTopic{ + {"topics", "List available help topics", nil}, + {"patterns", "Input/output path patterns: globs, %d sequences, templates", topicPatterns}, + } +) + +func Command() *cli.Command { + return &cli.Command{ + Name: "help", + Usage: "Show help or a specific topic", + ArgsUsage: "[topic]", + Action: func(ctx context.Context, c *cli.Command) error { + args := c.Args().Slice() + + if len(args) == 0 { + parent := c.Root() + + if parent == nil { + parent = c + } + + return cli.ShowAppHelp(parent) + } + + topic := strings.ToLower(strings.TrimSpace(args[0])) + + switch topic { + case "topics": + return printTopics(c.Writer) + default: + return printTopic(c.Writer, topic) + } + }, + } +} + +func printTopics(w io.Writer) error { + var length int + + for _, topic := range topics { + length = max(length, len(topic.Name)) + } + + fmt.Fprintln(w, "USAGE:") + fmt.Fprintln(w, " ffwebp help [topic]") + fmt.Fprintln(w) + fmt.Fprintln(w, "TOPICS:") + + for _, topic := range topics { + fmt.Fprintf(w, " %-*s - %s\n", length, topic.Name, topic.Description) + } + + return nil +} + +func printTopic(w io.Writer, name string) error { + var topic *HelpTopic + + for _, tp := range topics { + if tp.Name == name { + topic = &tp + } + } + + if topic == nil { + return fmt.Errorf("unknown help topic: %q (see `ffwebp help topics`)", name) + } + + _, err := w.Write(topic.Content) + return err +} diff --git a/internal/help/topics/patterns.txt b/internal/help/topics/patterns.txt new file mode 100644 index 0000000..3c49fc0 --- /dev/null +++ b/internal/help/topics/patterns.txt @@ -0,0 +1,28 @@ +USAGE: + ffwebp [global options] + +TOPIC: + Path patterns for input (-i) and output (-o) + +INPUT PATTERNS: + - Globs: *, ?, [..] (shell-expanded on Unix; expanded by ffwebp on Windows) + - Numeric sequences: %d, %02d (start at --start-number, default 1) + +OUTPUT PATTERNS: + - %d sequences (same numbering as input/index) + - Templates (GNU-parallel style): + {} full input path + {.} input path without last extension + {/} basename (file name) + {/.} basename without extension + {//} directory (no trailing slash) + {#} sequence number (uses --start-number) + +EXAMPLES: + ffwebp -i "*.png" -o "{/.}.webp" + ffwebp -i "frames/%03d.png" -o "out/frame-{#}.png" --start-number 10 + ffwebp -i "images/**/*.jpg" -o "out/{//}/{/.}.avif" + +NOTES: + - Output codec is inferred from the final extension unless --codec is set. + - With multiple inputs, output must be a pattern or directory. \ No newline at end of file