mirror of
https://github.com/coalaura/ffwebp.git
synced 2025-12-02 18:22:53 +00:00
patterns and help topics
This commit is contained in:
@@ -14,6 +14,7 @@ import (
|
|||||||
_ "github.com/coalaura/ffwebp/internal/builtins"
|
_ "github.com/coalaura/ffwebp/internal/builtins"
|
||||||
"github.com/coalaura/ffwebp/internal/codec"
|
"github.com/coalaura/ffwebp/internal/codec"
|
||||||
"github.com/coalaura/ffwebp/internal/effects"
|
"github.com/coalaura/ffwebp/internal/effects"
|
||||||
|
"github.com/coalaura/ffwebp/internal/help"
|
||||||
"github.com/coalaura/ffwebp/internal/logx"
|
"github.com/coalaura/ffwebp/internal/logx"
|
||||||
"github.com/coalaura/ffwebp/internal/opts"
|
"github.com/coalaura/ffwebp/internal/opts"
|
||||||
"github.com/nfnt/resize"
|
"github.com/nfnt/resize"
|
||||||
@@ -33,7 +34,7 @@ func main() {
|
|||||||
&cli.StringFlag{
|
&cli.StringFlag{
|
||||||
Name: "output",
|
Name: "output",
|
||||||
Aliases: []string{"o"},
|
Aliases: []string{"o"},
|
||||||
Usage: "output file, directory, or pattern (\"-\" = stdout)",
|
Usage: "output file, directory, or pattern (\"-\" = stdout, supports %d and templates)",
|
||||||
Value: "-",
|
Value: "-",
|
||||||
},
|
},
|
||||||
&cli.IntFlag{
|
&cli.IntFlag{
|
||||||
@@ -104,6 +105,9 @@ func main() {
|
|||||||
EnableShellCompletion: true,
|
EnableShellCompletion: true,
|
||||||
UseShortOptionHandling: true,
|
UseShortOptionHandling: true,
|
||||||
Suggest: true,
|
Suggest: true,
|
||||||
|
Commands: []*cli.Command{
|
||||||
|
help.Command(),
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := app.Run(context.Background(), os.Args); err != nil {
|
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
|
var outputs []string
|
||||||
|
|
||||||
if len(inputs) == 1 {
|
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 {
|
} else {
|
||||||
switch {
|
switch {
|
||||||
case output == "-":
|
case output == "-":
|
||||||
return fmt.Errorf("multiple inputs require an output pattern or directory, not '-' ")
|
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):
|
case hasSeq(output):
|
||||||
outs := make([]string, len(inputs))
|
outs := make([]string, len(inputs))
|
||||||
|
|
||||||
@@ -196,7 +218,7 @@ func run(_ context.Context, cmd *cli.Command) error {
|
|||||||
outs := make([]string, len(inputs))
|
outs := make([]string, len(inputs))
|
||||||
|
|
||||||
for i := range inputs {
|
for i := range inputs {
|
||||||
outs[i] = output
|
outs[i] = outDir
|
||||||
}
|
}
|
||||||
|
|
||||||
outputs = outs
|
outputs = outs
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import (
|
|||||||
"regexp"
|
"regexp"
|
||||||
"sort"
|
"sort"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
func hasSeq(s string) bool {
|
func hasSeq(s string) bool {
|
||||||
@@ -161,3 +162,49 @@ func expandSeq(pat string) ([]string, error) {
|
|||||||
|
|
||||||
return result, nil
|
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)
|
||||||
|
}
|
||||||
|
|||||||
93
internal/help/help.go
Normal file
93
internal/help/help.go
Normal file
@@ -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
|
||||||
|
}
|
||||||
28
internal/help/topics/patterns.txt
Normal file
28
internal/help/topics/patterns.txt
Normal file
@@ -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.
|
||||||
Reference in New Issue
Block a user