From cf9de214956583b7ba9e44f699276d5a861e87e6 Mon Sep 17 00:00:00 2001 From: Lilleman auf Larv Date: Tue, 1 Sep 2026 23:02:19 +0200 Subject: [PATCH] GNU-style flags: --name, short aliases, any position, help on stdout --- README.md | 39 +++---- cmd/fejkdata/main.go | 244 +++++++++++++++++++++++++++++-------------- 2 files changed, 188 insertions(+), 95 deletions(-) diff --git a/README.md b/README.md index 13aeb6f..08ef2d2 100644 --- a/README.md +++ b/README.md @@ -28,31 +28,32 @@ lacked the locale coverage and format control we needed. ## CLI -Install the `fejkdata` command, then give it one or more `-data-path` directories +Install the `fejkdata` command, then give it one or more `--data-path` directories and a path — it prints one value to stdout. Each dot segment descends one level: folders, then the category (a JSON file), then fields inside it. ```sh go install gitea.larvit.se/larvit/fejkdata/cmd/fejkdata@latest -fejkdata -data-path ./data/sv_SE person # Sara Eriksson -fejkdata -data-path ./data/sv_SE person.last # Eriksson (dotted path into a category) -fejkdata -data-path ./data sv_SE.person # point at the tree; the folder is a segment -fejkdata -data-path ./data/sv_SE -data-path ./mydata word # layer dirs; the last wins a name clash -fejkdata -seed 42 -data-path ./data/sv_SE address -fejkdata -repeat 3 -data-path ./data/sv_SE person # three values, one per line -fejkdata -repeat 3 -separator ', ' -data-path ./data/sv_SE word # nät, barn, sol -fejkdata -data-path ./data/sv_SE -list # every path this data offers +fejkdata --data-path ./data/sv_SE person # Sara Eriksson +fejkdata --data-path ./data/sv_SE person.last # Eriksson (dotted path into a category) +fejkdata --data-path ./data sv_SE.person # point at the tree; the folder is a segment +fejkdata -d ./data/sv_SE -d ./mydata word # layer dirs; the last wins a name clash +fejkdata --seed 42 --data-path ./data/sv_SE address +fejkdata --repeat 3 --data-path ./data/sv_SE person # three values, one per line +fejkdata -n 3 --separator ', ' --data-path ./data/sv_SE word # nät, barn, sol +fejkdata --data-path ./data/sv_SE --list # every path this data offers ``` -`-data-path` is repeatable (last wins a name clash) and the path comes last — -all flags must precede it. `-repeat N` renders the path N times — each an -independent draw — joined by `-separator` (default a newline, so values land one -per line). Not sure what a data set offers? `-list` prints every path you can ask -for; `-version` prints the build version. +Flags are GNU-style: `--name value` or `--name=value`, short aliases `-d`, `-n`, +`-s`, `-h`, in any position; `--` ends the flags. `--data-path` is repeatable +(last wins a name clash). `--repeat N` renders the path N times — each an +independent draw — joined by `--separator` (default a newline, so values land one +per line). Not sure what a data set offers? `--list` prints every path you can ask +for; `--version` prints the build version. Without installing, run it from a checkout with `go run ./cmd/fejkdata …`. Exit -codes: `0` success (including `-list`, `-version`, `-h`), `1` runtime error +codes: `0` success (including `--list`, `--version`, `--help`), `1` runtime error (missing dir, unknown path), `2` misuse. ### Generating a file from a custom template @@ -78,15 +79,15 @@ the `),(` separator; the outer `V#ALUES(…)` wraps that into one valid row list letter token — see [Data format](#data-format).) ```sh -fejkdata -seed 1 -data-path ./data/sv_SE sql +fejkdata --seed 1 --data-path ./data/sv_SE sql # INSERT INTO users VALUES('zoom'),('wahoo'),('blip'); ``` Raise the template's `repeat` for more rows per statement; use the CLI's -`-repeat` for more statements — together they build a whole seed file: +`--repeat` for more statements — together they build a whole seed file: ```sh -fejkdata -repeat 100 -data-path ./data/sv_SE sql > seed.sql +fejkdata --repeat 100 --data-path ./data/sv_SE sql > seed.sql ``` ## Library @@ -145,7 +146,7 @@ av == bv // true ``` `f.List()` returns the sorted paths the loaded data offers — the categories, their -dotted fields and folder segments (what the CLI's `-list` prints). Every path it +dotted fields and folder segments (what the CLI's `--list` prints). Every path it lists renders. A `*Generator` is **not** safe for concurrent use — create one per goroutine. diff --git a/cmd/fejkdata/main.go b/cmd/fejkdata/main.go index 24ac085..7d34f7c 100644 --- a/cmd/fejkdata/main.go +++ b/cmd/fejkdata/main.go @@ -1,122 +1,214 @@ -// Command fejkdata prints one fake value from one or more data directories. +// Command fejkdata prints fake values from one or more data directories. // -// fejkdata -data-path ./data/sv_SE person # a full person -// fejkdata -data-path ./data/sv_SE person.last # just the surname (dotted path) -// fejkdata -data-path ./data sv_SE.person # point at the tree, address by folder -// fejkdata -data-path ./data/sv_SE -data-path ./mydata person # layer custom data; last dir wins -// fejkdata -seed 42 -data-path ./data/sv_SE address -// -// It is a thin CLI over the fejkdata library: New(dirs) then Fake(path). +// fejkdata --data-path ./data/sv_SE person # a full person +// fejkdata --data-path ./data/sv_SE person.last # just the surname +// fejkdata --data-path ./data sv_SE.person # point at the tree, address by folder +// fejkdata --data-path ./data/sv_SE --data-path ./mydata person # layer custom data; last dir wins +// fejkdata --seed 42 --data-path ./data/sv_SE address package main import ( "errors" - "flag" "fmt" "io" "os" "runtime/debug" + "strconv" "strings" "gitea.larvit.se/larvit/fejkdata" ) -const usage = `Usage: fejkdata -data-path D [-data-path D]... [-seed N] [-repeat N] [-separator S] +const usage = `Usage: fejkdata [flags] - -data-path D a data directory, e.g. ./data/sv_SE (repeatable; last wins on clash) - a category, or a dotted path into one (person, person.last) - -seed N seed for reproducible output - -repeat N render the path N times (default 1) - -separator S string between repeated values (default newline) - -list list the paths the data offers, then exit - -version print the version, then exit + a category, or a dotted path into one (person, person.last) -Flags must come before .` + -d, --data-path D a data directory, e.g. ./data/sv_SE (repeatable; last wins on a clash) + -h, --help print this help, then exit + --list list the paths the data offers, then exit + -n, --repeat N render the path N times (default 1) + -s, --seed N seed for reproducible output + --separator S string between repeated values (default newline) + --version print the version, then exit -// stringList collects a repeatable string flag, preserving order. -type stringList []string +Flags may come before or after ; -- ends the flags. +` -func (s *stringList) String() string { return strings.Join(*s, ",") } +type invocation struct { + dirs []string + help bool + list bool + paths []string + repeat int + seed uint64 + seeded bool + separator string + version bool +} -func (s *stringList) Set(v string) error { *s = append(*s, v); return nil } +type flagDef struct { + long string + short string + value bool + set func(*invocation, string) error +} + +var flagDefs = []flagDef{ + {"data-path", "d", true, func(in *invocation, v string) error { in.dirs = append(in.dirs, v); return nil }}, + {"help", "h", false, func(in *invocation, _ string) error { in.help = true; return nil }}, + {"list", "", false, func(in *invocation, _ string) error { in.list = true; return nil }}, + {"repeat", "n", true, func(in *invocation, v string) error { + n, err := strconv.Atoi(v) + if err != nil || n < 1 { + return fmt.Errorf("--repeat needs a positive integer, got %q", v) + } + in.repeat = n + return nil + }}, + {"seed", "s", true, func(in *invocation, v string) error { + n, err := strconv.ParseUint(v, 10, 64) + if err != nil { + return fmt.Errorf("--seed needs an unsigned integer, got %q", v) + } + in.seed, in.seeded = n, true + return nil + }}, + {"separator", "", true, func(in *invocation, v string) error { in.separator = v; return nil }}, + {"version", "", false, func(in *invocation, _ string) error { in.version = true; return nil }}, +} + +func flagByLong(name string) *flagDef { + for i := range flagDefs { + if flagDefs[i].long == name { + return &flagDefs[i] + } + } + return nil +} + +func flagByShort(name string) *flagDef { + for i := range flagDefs { + if flagDefs[i].short == name { + return &flagDefs[i] + } + } + return 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 == "--": + 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) + } + _ = def.set(&in, "") + continue + } + if !hasValue { + if i++; i >= len(argv) { + return in, fmt.Errorf("--%s needs a value", name) + } + value = argv[i] + } + if err := 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 +} func main() { os.Exit(run(os.Args[1:], os.Stdout, os.Stderr)) } -// run is main's testable core: it returns the process exit code (0 ok, 1 -// runtime error, 2 misuse) and writes only to the given streams. +// run returns the exit code: 0 ok, 1 runtime error, 2 misuse. func run(args []string, stdout, stderr io.Writer) int { - fs := flag.NewFlagSet("fejkdata", flag.ContinueOnError) - fs.SetOutput(stderr) - fs.Usage = func() { fmt.Fprintln(stderr, usage) } - seed := fs.Uint64("seed", 0, "seed for reproducible output") - repeat := fs.Int("repeat", 1, "render the path this many times") - sep := fs.String("separator", "\n", "string between repeated values") - list := fs.Bool("list", false, "list the paths the data offers, then exit") - showVersion := fs.Bool("version", false, "print the version, then exit") - var dirs stringList - fs.Var(&dirs, "data-path", "a data directory to load (repeatable)") - if err := fs.Parse(args); err != nil { - if errors.Is(err, flag.ErrHelp) { // -h/-help already printed usage - return 0 - } - return 2 + in, err := parseArgs(args) + if err != nil { + return misuse(stderr, err) } - if *showVersion { + if in.help { + fmt.Fprint(stdout, usage) + return 0 + } + if in.version { fmt.Fprintln(stdout, "fejkdata "+buildVersion()) return 0 } - if len(dirs) == 0 { - fs.Usage() - return 2 + if len(in.dirs) == 0 { + return misuse(stderr, errors.New("--data-path is required")) + } + 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 - fs.Visit(func(fl *flag.Flag) { - if fl.Name == "seed" { - opts = append(opts, fejkdata.WithSeed(*seed)) - } - }) - - if *list { - f, err := fejkdata.New(dirs, opts...) - if err != nil { - fmt.Fprintln(stderr, err) - return 1 - } + if in.seeded { + opts = append(opts, fejkdata.WithSeed(in.seed)) + } + f, err := fejkdata.New(in.dirs, opts...) + if err != nil { + fmt.Fprintln(stderr, err) + return 1 + } + if in.list { for _, p := range f.List() { fmt.Fprintln(stdout, p) } return 0 } - if fs.NArg() != 1 { - fs.Usage() - return 2 - } - if *repeat < 1 { - fmt.Fprintln(stderr, "repeat must be a positive integer") - return 2 - } - - path := fs.Arg(0) - f, err := fejkdata.New(dirs, opts...) - if err != nil { - fmt.Fprintln(stderr, err) - return 1 - } - vals := make([]string, *repeat) + vals := make([]string, in.repeat) for i := range vals { - if vals[i], err = f.Fake(path); err != nil { + if vals[i], err = f.Fake(in.paths[0]); err != nil { fmt.Fprintln(stderr, err) return 1 } } - fmt.Fprintln(stdout, strings.Join(vals, *sep)) + fmt.Fprintln(stdout, strings.Join(vals, in.separator)) return 0 } -// buildVersion reports the module version stamped into the binary by `go install` -// (or "devel" for a local build), read from the build info — no version constant -// to bump, no extra dependency. +func misuse(stderr io.Writer, err error) int { + fmt.Fprintf(stderr, "fejkdata: %v\ntry 'fejkdata --help'\n", err) + return 2 +} + +// buildVersion is the module version go install stamps into the binary, or "devel". func buildVersion() string { if info, ok := debug.ReadBuildInfo(); ok && info.Main.Version != "" { return info.Main.Version