From 0ad88028c47c5974d91c89fde8df9baf7b2a7c01 Mon Sep 17 00:00:00 2001 From: Lilleman auf Larv Date: Wed, 2 Sep 2026 14:09:48 +0200 Subject: [PATCH] One path reads a flag's value: short flags attach and bundle, = after one is rejected naming the spelling --- README.md | 14 ++-- cmd/fejkdata/main.go | 181 +++++++++++++++++++++++++++---------------- 2 files changed, 125 insertions(+), 70 deletions(-) diff --git a/README.md b/README.md index 4804bf9..a07b7b5 100644 --- a/README.md +++ b/README.md @@ -49,8 +49,9 @@ level — folders, then the category (a JSON file), then fields. | `--list` | print every path, then exit | | `--version`, `-h`, `--help` | print, then exit | -`--name value` and `--name=value` both work (see [Decisions](#decisions)); flags -go anywhere, `--` ends them. Exit codes: `0` success, `1` runtime error (missing +`--name value` and `--name=value` both work, a short flag's value attaches or +follows (`-n3`, `-n 3`) and short flags bundle (`-hn 3`) — see +[Decisions](#decisions); flags go anywhere, `--` ends them. Exit codes: `0` success, `1` runtime error (missing dir, unknown path), `2` misuse. From a checkout: `go run ./cmd/fejkdata …`. ### Your own data @@ -337,9 +338,12 @@ tokens add cost in proportion to the output. - **`{a|b}` stays beside nested choices.** `[[…], […]]` picks the same way, but its arms are anonymous; `{femalefirst|malefirst}` keeps `person.femalefirst` addressable. -- **`--name value` and `--name=value` both work.** GNU getopt_long convention, - which every shell user expects. A single-dash long flag is rejected naming the - double-dash spelling. +- **Flags follow getopt_long.** `--name value` and `--name=value` both work; a + short flag's value attaches or follows (`-s42`, `-s 42`) and short flags bundle + (`-hn 3`), as every shell user expects. A single-dash long flag is rejected + naming the double-dash spelling, and `-s=42` is rejected naming both short + spellings: `=` belongs to the long form, and reading `=42` as the value would + make `-d=./x` a directory named `=./x`. - **The shipped data is embedded, not discovered.** A directory a machine happens to have would make `--seed 42` machine-dependent. Data still lives in `data/` as JSON; `--data-path` layers over it. diff --git a/cmd/fejkdata/main.go b/cmd/fejkdata/main.go index 9dd65ac..9f35f96 100644 --- a/cmd/fejkdata/main.go +++ b/cmd/fejkdata/main.go @@ -21,7 +21,7 @@ import ( const usage = `Usage: fejkdata [flags] - a category, or a dotted path into one (person, person.last) + a category, or a dotted path into one (person, person.last) -d, --data-path D a data directory to layer over the shipped data (repeatable; last wins on a clash) -h, --help print this help, then exit @@ -32,7 +32,8 @@ const usage = `Usage: fejkdata [flags] --separator S string between repeated values (default newline) --version print the version, then exit -Flags may come before or after ; -- ends the flags. +Flags may come before or after ; -- ends the flags. A short flag's value +attaches or follows (-n3, -n 3); short flags bundle (-hn 3). ` type invocation struct { @@ -98,62 +99,131 @@ func flagByShort(name string) *flagDef { return nil } +// flagArg is one flag as written: its definition and the value attached to it, +// if any. +type flagArg struct { + def *flagDef + value string + inline bool +} + +// splitFlags reads one argv element as flags: --name or --name=value, or -abc +// where each letter is a short flag and the first that takes a value takes the +// rest of the element. +func splitFlags(arg string) ([]flagArg, error) { + if strings.HasPrefix(arg, "--") { + name, value, inline := strings.Cut(arg[2:], "=") + def := flagByLong(name) + if def == nil { + return nil, fmt.Errorf("unknown flag --%s", name) + } + return []flagArg{{def, value, inline}}, nil + } + if name, _, _ := strings.Cut(arg[1:], "="); len(name) > 1 && flagByLong(name) != nil { + return nil, fmt.Errorf("unknown flag %s; use --%s", arg, name) + } + var flags []flagArg + letters := arg[1:] + for i := 0; i < len(letters); i++ { + letter := letters[i : i+1] + def := flagByShort(letter) + if def == nil { + return nil, fmt.Errorf("unknown flag -%s", letter) + } + if !def.value { + flags = append(flags, flagArg{def: def}) + continue + } + rest := letters[i+1:] + if strings.HasPrefix(rest, "=") { + return nil, fmt.Errorf("-%s takes its value attached (-%s%s) or next (-%s %s); = belongs to --%s=%s", + letter, letter, rest[1:], letter, rest[1:], def.long, rest[1:]) + } + return append(flags, flagArg{def, rest, rest != ""}), nil + } + return flags, nil +} + func parseArgs(argv []string) (invocation, error) { in := invocation{repeat: 1, separator: "\n"} for i := 0; i < len(argv); i++ { arg := argv[i] - switch { - case arg == "--": + if arg == "--" { in.paths = append(in.paths, argv[i+1:]...) return in, nil - case strings.HasPrefix(arg, "--"): - name, value, hasValue := strings.Cut(arg[2:], "=") - def := flagByLong(name) - if def == nil { - return in, fmt.Errorf("unknown flag --%s", name) - } - if !def.value { - if hasValue { - return in, fmt.Errorf("--%s takes no value", name) + } + if len(arg) < 2 || arg[0] != '-' { + in.paths = append(in.paths, arg) + continue + } + flags, err := splitFlags(arg) + if err != nil { + return in, err + } + for _, fl := range flags { + if !fl.def.value { + if fl.inline { + return in, fmt.Errorf("--%s takes no value", fl.def.long) } - _ = def.set(&in, "") + _ = fl.def.set(&in, "") continue } - if !hasValue { + value := fl.value + if !fl.inline { if i++; i >= len(argv) { - return in, fmt.Errorf("--%s needs a value", name) + return in, fmt.Errorf("--%s needs a value", fl.def.long) } value = argv[i] } - if err := def.set(&in, value); err != nil { + if err := fl.def.set(&in, value); err != nil { return in, err } - case len(arg) > 1 && arg[0] == '-': - name, _, _ := strings.Cut(arg[1:], "=") - def := flagByShort(name) - if def == nil { - if flagByLong(name) != nil { - return in, fmt.Errorf("unknown flag %s; use --%s", arg, name) - } - return in, fmt.Errorf("unknown flag %s", arg) - } - if !def.value { - _ = def.set(&in, "") - continue - } - if i++; i >= len(argv) { - return in, fmt.Errorf("--%s needs a value", def.long) - } - if err := def.set(&in, argv[i]); err != nil { - return in, err - } - default: - in.paths = append(in.paths, arg) } } return in, nil } +// check rejects a flag combination that cannot run. +func (in invocation) check() error { + if in.noShipped && len(in.dirs) == 0 { + return errors.New("--no-shipped-data needs at least one --data-path") + } + if in.list && len(in.paths) > 0 { + return errors.New("--list takes no path") + } + if !in.list && len(in.paths) != 1 { + return fmt.Errorf("expected one path, got %d", len(in.paths)) + } + return nil +} + +func (in invocation) options() []fejkdata.Option { + var opts []fejkdata.Option + if in.noShipped { + opts = append(opts, fejkdata.WithoutShippedData()) + } + for _, dir := range in.dirs { + opts = append(opts, fejkdata.WithDataPath(dir)) + } + if in.seeded { + opts = append(opts, fejkdata.WithSeed(in.seed)) + } + return opts +} + +// values renders the path repeat times, joined by the separator. +func (in invocation) values(f *fejkdata.Generator) (string, error) { + vals := make([]string, in.repeat) + for i := range vals { + v, err := f.Fake(in.paths[0]) + if err != nil { + return "", err + } + vals[i] = v + } + return strings.Join(vals, in.separator), nil +} + func main() { os.Exit(run(os.Args[1:], os.Stdout, os.Stderr)) } // run returns the exit code: 0 ok, 1 runtime error, 2 misuse. @@ -170,27 +240,10 @@ func run(args []string, stdout, stderr io.Writer) int { fmt.Fprintln(stdout, "fejkdata "+buildVersion()) return 0 } - if in.noShipped && len(in.dirs) == 0 { - return misuse(stderr, errors.New("--no-shipped-data needs at least one --data-path")) + if err := in.check(); err != nil { + return misuse(stderr, err) } - if in.list && len(in.paths) > 0 { - return misuse(stderr, errors.New("--list takes no path")) - } - if !in.list && len(in.paths) != 1 { - return misuse(stderr, fmt.Errorf("expected one path, got %d", len(in.paths))) - } - - var opts []fejkdata.Option - if in.noShipped { - opts = append(opts, fejkdata.WithoutShippedData()) - } - for _, dir := range in.dirs { - opts = append(opts, fejkdata.WithDataPath(dir)) - } - if in.seeded { - opts = append(opts, fejkdata.WithSeed(in.seed)) - } - f, err := fejkdata.New(opts...) + f, err := fejkdata.New(in.options()...) if err != nil { fmt.Fprintln(stderr, err) return 1 @@ -201,14 +254,12 @@ func run(args []string, stdout, stderr io.Writer) int { } return 0 } - vals := make([]string, in.repeat) - for i := range vals { - if vals[i], err = f.Fake(in.paths[0]); err != nil { - fmt.Fprintln(stderr, err) - return 1 - } + out, err := in.values(f) + if err != nil { + fmt.Fprintln(stderr, err) + return 1 } - fmt.Fprintln(stdout, strings.Join(vals, in.separator)) + fmt.Fprintln(stdout, out) return 0 }