One path reads a flag's value: short flags attach and bundle, = after one is rejected naming the spelling
Tests / vet + fmt + tests (pull_request) Successful in 52s

This commit is contained in:
2026-09-02 14:09:48 +02:00
parent f6782cf477
commit 0ad88028c4
2 changed files with 125 additions and 70 deletions
+9 -5
View File
@@ -49,8 +49,9 @@ level — folders, then the category (a JSON file), then fields.
| `--list` | print every path, then exit | | `--list` | print every path, then exit |
| `--version`, `-h`, `--help` | print, then exit | | `--version`, `-h`, `--help` | print, then exit |
`--name value` and `--name=value` both work (see [Decisions](#decisions)); flags `--name value` and `--name=value` both work, a short flag's value attaches or
go anywhere, `--` ends them. Exit codes: `0` success, `1` runtime error (missing 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 …`. dir, unknown path), `2` misuse. From a checkout: `go run ./cmd/fejkdata …`.
### Your own data ### 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 - **`{a|b}` stays beside nested choices.** `[[…], […]]` picks the same way, but
its arms are anonymous; `{femalefirst|malefirst}` keeps `person.femalefirst` its arms are anonymous; `{femalefirst|malefirst}` keeps `person.femalefirst`
addressable. addressable.
- **`--name value` and `--name=value` both work.** GNU getopt_long convention, - **Flags follow getopt_long.** `--name value` and `--name=value` both work; a
which every shell user expects. A single-dash long flag is rejected naming the short flag's value attaches or follows (`-s42`, `-s 42`) and short flags bundle
double-dash spelling. (`-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 - **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/` to have would make `--seed 42` machine-dependent. Data still lives in `data/`
as JSON; `--data-path` layers over it. as JSON; `--data-path` layers over it.
+116 -65
View File
@@ -21,7 +21,7 @@ import (
const usage = `Usage: fejkdata [flags] <path> const usage = `Usage: fejkdata [flags] <path>
<path> a category, or a dotted path into one (person, person.last) <path> 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) -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 -h, --help print this help, then exit
@@ -32,7 +32,8 @@ const usage = `Usage: fejkdata [flags] <path>
--separator S string between repeated values (default newline) --separator S string between repeated values (default newline)
--version print the version, then exit --version print the version, then exit
Flags may come before or after <path>; -- ends the flags. Flags may come before or after <path>; -- ends the flags. A short flag's value
attaches or follows (-n3, -n 3); short flags bundle (-hn 3).
` `
type invocation struct { type invocation struct {
@@ -98,62 +99,131 @@ func flagByShort(name string) *flagDef {
return nil 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) { func parseArgs(argv []string) (invocation, error) {
in := invocation{repeat: 1, separator: "\n"} in := invocation{repeat: 1, separator: "\n"}
for i := 0; i < len(argv); i++ { for i := 0; i < len(argv); i++ {
arg := argv[i] arg := argv[i]
switch { if arg == "--" {
case arg == "--":
in.paths = append(in.paths, argv[i+1:]...) in.paths = append(in.paths, argv[i+1:]...)
return in, nil return in, nil
case strings.HasPrefix(arg, "--"): }
name, value, hasValue := strings.Cut(arg[2:], "=") if len(arg) < 2 || arg[0] != '-' {
def := flagByLong(name) in.paths = append(in.paths, arg)
if def == nil { continue
return in, fmt.Errorf("unknown flag --%s", name) }
} flags, err := splitFlags(arg)
if !def.value { if err != nil {
if hasValue { return in, err
return in, fmt.Errorf("--%s takes no value", name) }
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 continue
} }
if !hasValue { value := fl.value
if !fl.inline {
if i++; i >= len(argv) { 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] value = argv[i]
} }
if err := def.set(&in, value); err != nil { if err := fl.def.set(&in, value); err != nil {
return in, err 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 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)) } func main() { os.Exit(run(os.Args[1:], os.Stdout, os.Stderr)) }
// run returns the exit code: 0 ok, 1 runtime error, 2 misuse. // 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()) fmt.Fprintln(stdout, "fejkdata "+buildVersion())
return 0 return 0
} }
if in.noShipped && len(in.dirs) == 0 { if err := in.check(); err != nil {
return misuse(stderr, errors.New("--no-shipped-data needs at least one --data-path")) return misuse(stderr, err)
} }
if in.list && len(in.paths) > 0 { f, err := fejkdata.New(in.options()...)
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...)
if err != nil { if err != nil {
fmt.Fprintln(stderr, err) fmt.Fprintln(stderr, err)
return 1 return 1
@@ -201,14 +254,12 @@ func run(args []string, stdout, stderr io.Writer) int {
} }
return 0 return 0
} }
vals := make([]string, in.repeat) out, err := in.values(f)
for i := range vals { if err != nil {
if vals[i], err = f.Fake(in.paths[0]); err != nil { fmt.Fprintln(stderr, err)
fmt.Fprintln(stderr, err) return 1
return 1
}
} }
fmt.Fprintln(stdout, strings.Join(vals, in.separator)) fmt.Fprintln(stdout, out)
return 0 return 0
} }