GNU-style flags: --name, short aliases, any position, help on stdout

This commit is contained in:
2026-09-01 23:02:19 +02:00
parent f773738243
commit cf9de21495
2 changed files with 188 additions and 95 deletions
+20 -19
View File
@@ -28,31 +28,32 @@ lacked the locale coverage and format control we needed.
## CLI ## 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: 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. folders, then the category (a JSON file), then fields inside it.
```sh ```sh
go install gitea.larvit.se/larvit/fejkdata/cmd/fejkdata@latest 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 # Sara Eriksson
fejkdata -data-path ./data/sv_SE person.last # Eriksson (dotted path into a category) 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.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 -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 --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 --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 -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 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 — Flags are GNU-style: `--name value` or `--name=value`, short aliases `-d`, `-n`,
all flags must precede it. `-repeat N` renders the path N times — each an `-s`, `-h`, in any position; `--` ends the flags. `--data-path` is repeatable
independent draw — joined by `-separator` (default a newline, so values land one (last wins a name clash). `--repeat N` renders the path N times — each an
per line). Not sure what a data set offers? `-list` prints every path you can ask independent draw — joined by `--separator` (default a newline, so values land one
for; `-version` prints the build version. 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 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. (missing dir, unknown path), `2` misuse.
### Generating a file from a custom template ### 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).) letter token — see [Data format](#data-format).)
```sh ```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'); # INSERT INTO users VALUES('zoom'),('wahoo'),('blip');
``` ```
Raise the template's `repeat` for more rows per statement; use the CLI's 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 ```sh
fejkdata -repeat 100 -data-path ./data/sv_SE sql > seed.sql fejkdata --repeat 100 --data-path ./data/sv_SE sql > seed.sql
``` ```
## Library ## Library
@@ -145,7 +146,7 @@ av == bv // true
``` ```
`f.List()` returns the sorted paths the loaded data offers — the categories, their `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. lists renders.
A `*Generator` is **not** safe for concurrent use — create one per goroutine. A `*Generator` is **not** safe for concurrent use — create one per goroutine.
+161 -69
View File
@@ -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 # a full person
// fejkdata -data-path ./data/sv_SE person.last # just the surname (dotted path) // 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.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 --data-path ./data/sv_SE --data-path ./mydata person # layer custom data; last dir wins
// fejkdata -seed 42 -data-path ./data/sv_SE address // fejkdata --seed 42 --data-path ./data/sv_SE address
//
// It is a thin CLI over the fejkdata library: New(dirs) then Fake(path).
package main package main
import ( import (
"errors" "errors"
"flag"
"fmt" "fmt"
"io" "io"
"os" "os"
"runtime/debug" "runtime/debug"
"strconv"
"strings" "strings"
"gitea.larvit.se/larvit/fejkdata" "gitea.larvit.se/larvit/fejkdata"
) )
const usage = `Usage: fejkdata -data-path D [-data-path D]... [-seed N] [-repeat N] [-separator S] <path> const usage = `Usage: fejkdata [flags] <path>
-data-path D a data directory, e.g. ./data/sv_SE (repeatable; last wins on clash)
<path> a category, or a dotted path into one (person, person.last) <path> 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
Flags must come before <path>.` -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. Flags may come before or after <path>; -- ends the flags.
type stringList []string `
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)) } 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 // run returns the exit code: 0 ok, 1 runtime error, 2 misuse.
// runtime error, 2 misuse) and writes only to the given streams.
func run(args []string, stdout, stderr io.Writer) int { func run(args []string, stdout, stderr io.Writer) int {
fs := flag.NewFlagSet("fejkdata", flag.ContinueOnError) in, err := parseArgs(args)
fs.SetOutput(stderr) if err != nil {
fs.Usage = func() { fmt.Fprintln(stderr, usage) } return misuse(stderr, err)
seed := fs.Uint64("seed", 0, "seed for reproducible output") }
repeat := fs.Int("repeat", 1, "render the path this many times") if in.help {
sep := fs.String("separator", "\n", "string between repeated values") fmt.Fprint(stdout, usage)
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 0
} }
return 2 if in.version {
}
if *showVersion {
fmt.Fprintln(stdout, "fejkdata "+buildVersion()) fmt.Fprintln(stdout, "fejkdata "+buildVersion())
return 0 return 0
} }
if len(dirs) == 0 { if len(in.dirs) == 0 {
fs.Usage() return misuse(stderr, errors.New("--data-path is required"))
return 2 }
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 var opts []fejkdata.Option
fs.Visit(func(fl *flag.Flag) { if in.seeded {
if fl.Name == "seed" { opts = append(opts, fejkdata.WithSeed(in.seed))
opts = append(opts, fejkdata.WithSeed(*seed))
} }
}) f, err := fejkdata.New(in.dirs, opts...)
if *list {
f, err := fejkdata.New(dirs, opts...)
if err != nil { if err != nil {
fmt.Fprintln(stderr, err) fmt.Fprintln(stderr, err)
return 1 return 1
} }
if in.list {
for _, p := range f.List() { for _, p := range f.List() {
fmt.Fprintln(stdout, p) fmt.Fprintln(stdout, p)
} }
return 0 return 0
} }
if fs.NArg() != 1 { vals := make([]string, in.repeat)
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)
for i := range vals { 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) fmt.Fprintln(stderr, err)
return 1 return 1
} }
} }
fmt.Fprintln(stdout, strings.Join(vals, *sep)) fmt.Fprintln(stdout, strings.Join(vals, in.separator))
return 0 return 0
} }
// buildVersion reports the module version stamped into the binary by `go install` func misuse(stderr io.Writer, err error) int {
// (or "devel" for a local build), read from the build info — no version constant fmt.Fprintf(stderr, "fejkdata: %v\ntry 'fejkdata --help'\n", err)
// to bump, no extra dependency. return 2
}
// buildVersion is the module version go install stamps into the binary, or "devel".
func buildVersion() string { func buildVersion() string {
if info, ok := debug.ReadBuildInfo(); ok && info.Main.Version != "" { if info, ok := debug.ReadBuildInfo(); ok && info.Main.Version != "" {
return info.Main.Version return info.Main.Version