Replace positional CLI path with a repeatable -path flag

This commit is contained in:
lilleman
2026-06-09 00:47:01 +02:00
parent 862c5f8a23
commit 889b1fbf8b
3 changed files with 96 additions and 51 deletions
+17 -14
View File
@@ -24,24 +24,27 @@ lacked the locale coverage and format control we needed.
## CLI ## CLI
Install the `fakes` command, then point it at one or more data directories and a Install the `fakes` command, then give it one or more `-path` flags and one or
path — it prints one value to stdout. Each dot segment descends one level: more data directories — it prints one value per path to stdout. Each dot segment
folders, then the category (a JSON file), then fields inside it. descends one level: folders, then the category (a JSON file), then fields inside it.
```sh ```sh
go install github.com/Timewave-AB/fakes/cmd/fakes@latest go install github.com/Timewave-AB/fakes/cmd/fakes@latest
fakes ./data/sv_SE person # Sara Eriksson fakes -path person ./data/sv_SE # Sara Eriksson
fakes ./data/sv_SE person.last # Eriksson (dotted path into a category) fakes -path person.last ./data/sv_SE # Eriksson (dotted path into a category)
fakes ./data sv_SE.person # point at the tree; the folder is a segment fakes -path sv_SE.person ./data # point at the tree; the folder is a segment
fakes ./data/sv_SE ./mydata word # layer dirs; the last wins a name clash fakes -path person -path address ./data/sv_SE # several paths, one value each
fakes -seed 42 ./data/sv_SE address fakes -path word ./data/sv_SE ./mydata # layer dirs; the last wins a name clash
fakes -repeat 3 ./data/sv_SE person # three values, one per line fakes -seed 42 -path address ./data/sv_SE
fakes -repeat 3 -separator ', ' ./data/sv_SE word # nät, barn, sol fakes -repeat 3 -path person ./data/sv_SE # three values, one per line
fakes -repeat 3 -separator ', ' -path word ./data/sv_SE # nät, barn, sol
``` ```
`-repeat N` renders the path N times — each an independent draw — joined by `-path` is repeatable, and flags come before the data directories. `-repeat N`
`-separator` (default a newline, so values land one per line). renders each path N times — every render an independent draw — and all emitted
values (`repeat` × paths) are joined by `-separator` (default a newline, so
values land one per line).
Without installing, run it from a checkout with `go run ./cmd/fakes …`. Exit Without installing, run it from a checkout with `go run ./cmd/fakes …`. Exit
codes: `0` success, `1` runtime error (missing dir, unknown path), `2` misuse. codes: `0` success, `1` runtime error (missing dir, unknown path), `2` misuse.
@@ -69,7 +72,7 @@ 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
fakes -seed 1 ./data/sv_SE sql fakes -seed 1 -path sql ./data/sv_SE
# INSERT INTO users VALUES('zoom'),('wahoo'),('blip'); # INSERT INTO users VALUES('zoom'),('wahoo'),('blip');
``` ```
@@ -77,7 +80,7 @@ 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
fakes -repeat 100 ./data/sv_SE sql > seed.sql fakes -repeat 100 -path sql ./data/sv_SE > seed.sql
``` ```
## Library ## Library
+31 -18
View File
@@ -1,10 +1,11 @@
// Command fakes prints one fake value from one or more data directories. // Command fakes prints fake values from one or more data directories.
// //
// fakes ./data/sv_SE person # a full person // fakes -path person ./data/sv_SE # a full person
// fakes ./data/sv_SE person.last # just the surname (dotted path) // fakes -path person.last ./data/sv_SE # just the surname (dotted path)
// fakes ./data sv_SE.person # point at the tree, address by folder // fakes -path sv_SE.person ./data # point at the tree, address by folder
// fakes ./data/sv_SE ./mydata person # layer custom data; last dir wins // fakes -path person -path address ./data/sv_SE # several paths, one per line
// fakes -seed 42 ./data/sv_SE address // fakes -path person ./data/sv_SE ./mydata # layer custom data; last dir wins
// fakes -seed 42 -path address ./data/sv_SE
// //
// It is a thin CLI over the fakes library: New(dirs) then Fake(path). // It is a thin CLI over the fakes library: New(dirs) then Fake(path).
package main package main
@@ -19,13 +20,20 @@ import (
"github.com/Timewave-AB/fakes" "github.com/Timewave-AB/fakes"
) )
const usage = `Usage: fakes [-seed N] [-repeat N] [-separator S] <data-dir>... <path> const usage = `Usage: fakes -path P [-path P]... [-seed N] [-repeat N] [-separator S] <data-dir>...
-path P a category, or a dotted path into one (person, person.last); repeatable
<data-dir> one or more data directories, e.g. ./data/sv_SE (last wins on clash) <data-dir> one or more data directories, e.g. ./data/sv_SE (last wins on clash)
<path> a category, or a dotted path into one (person, person.last)
-seed N seed for reproducible output -seed N seed for reproducible output
-repeat N render the path N times (default 1) -repeat N render each path N times (default 1)
-separator S string between repeated values (default newline)` -separator S string between emitted values (default newline)`
// stringList collects a repeatable string flag, preserving order.
type stringList []string
func (s *stringList) String() string { return strings.Join(*s, ",") }
func (s *stringList) Set(v string) error { *s = append(*s, v); return 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)) }
@@ -36,12 +44,14 @@ func run(args []string, stdout, stderr io.Writer) int {
fs.SetOutput(stderr) fs.SetOutput(stderr)
fs.Usage = func() { fmt.Fprintln(stderr, usage) } fs.Usage = func() { fmt.Fprintln(stderr, usage) }
seed := fs.Uint64("seed", 0, "seed for reproducible output") seed := fs.Uint64("seed", 0, "seed for reproducible output")
repeat := fs.Int("repeat", 1, "render the path this many times") repeat := fs.Int("repeat", 1, "render each path this many times")
sep := fs.String("separator", "\n", "string between repeated values") sep := fs.String("separator", "\n", "string between emitted values")
var paths stringList
fs.Var(&paths, "path", "a category or dotted path to render (repeatable)")
if err := fs.Parse(args); err != nil { if err := fs.Parse(args); err != nil {
return 2 return 2
} }
if fs.NArg() < 2 { if len(paths) == 0 || fs.NArg() < 1 {
fs.Usage() fs.Usage()
return 2 return 2
} }
@@ -57,18 +67,21 @@ func run(args []string, stdout, stderr io.Writer) int {
} }
}) })
dirs, path := fs.Args()[:fs.NArg()-1], fs.Arg(fs.NArg()-1) f, err := fakes.New(fs.Args(), opts...)
f, err := fakes.New(dirs, opts...)
if err != nil { if err != nil {
fmt.Fprintln(stderr, err) fmt.Fprintln(stderr, err)
return 1 return 1
} }
vals := make([]string, *repeat) vals := make([]string, 0, *repeat*len(paths))
for i := range vals { for range *repeat {
if vals[i], err = f.Fake(path); err != nil { for _, p := range paths {
v, err := f.Fake(p)
if err != nil {
fmt.Fprintln(stderr, err) fmt.Fprintln(stderr, err)
return 1 return 1
} }
vals = append(vals, v)
}
} }
fmt.Fprintln(stdout, strings.Join(vals, *sep)) fmt.Fprintln(stdout, strings.Join(vals, *sep))
return 0 return 0
+46 -17
View File
@@ -19,7 +19,7 @@ func runOut(args ...string) (int, string, string) {
} }
func TestRunOutputsValue(t *testing.T) { func TestRunOutputsValue(t *testing.T) {
code, out, errb := runOut(svSE, "person") code, out, errb := runOut("-path", "person", svSE)
if code != 0 { if code != 0 {
t.Fatalf("run = %d, stderr=%q", code, errb) t.Fatalf("run = %d, stderr=%q", code, errb)
} }
@@ -33,11 +33,11 @@ func TestRunOutputsValue(t *testing.T) {
func TestRunDotPath(t *testing.T) { func TestRunDotPath(t *testing.T) {
// person.last descends to just a surname — never the full "First Last". // person.last descends to just a surname — never the full "First Last".
code, full, _ := runOut("-seed", "7", svSE, "person") code, full, _ := runOut("-seed", "7", "-path", "person", svSE)
if code != 0 { if code != 0 {
t.Fatalf("person run = %d", code) t.Fatalf("person run = %d", code)
} }
code, last, errb := runOut("-seed", "7", svSE, "person.last") code, last, errb := runOut("-seed", "7", "-path", "person.last", svSE)
if code != 0 { if code != 0 {
t.Fatalf("person.last run = %d, stderr=%q", code, errb) t.Fatalf("person.last run = %d, stderr=%q", code, errb)
} }
@@ -50,8 +50,8 @@ func TestRunDotPath(t *testing.T) {
} }
func TestRunSeedDeterministic(t *testing.T) { func TestRunSeedDeterministic(t *testing.T) {
_, a, _ := runOut("-seed", "42", svSE, "address") _, a, _ := runOut("-seed", "42", "-path", "address", svSE)
_, b, _ := runOut("-seed", "42", svSE, "address") _, b, _ := runOut("-seed", "42", "-path", "address", svSE)
if a != b { if a != b {
t.Errorf("same seed diverged: %q != %q", a, b) t.Errorf("same seed diverged: %q != %q", a, b)
} }
@@ -59,7 +59,7 @@ func TestRunSeedDeterministic(t *testing.T) {
func TestRunRepeat(t *testing.T) { func TestRunRepeat(t *testing.T) {
// -repeat N prints N values, one per line by default. // -repeat N prints N values, one per line by default.
code, out, errb := runOut("-seed", "1", "-repeat", "3", svSE, "word") code, out, errb := runOut("-seed", "1", "-repeat", "3", "-path", "word", svSE)
if code != 0 { if code != 0 {
t.Fatalf("run = %d, stderr=%q", code, errb) t.Fatalf("run = %d, stderr=%q", code, errb)
} }
@@ -70,8 +70,8 @@ func TestRunRepeat(t *testing.T) {
} }
func TestRunSeparator(t *testing.T) { func TestRunSeparator(t *testing.T) {
// -separator joins the repeated values instead of newlines. // -separator joins the emitted values instead of newlines.
code, out, errb := runOut("-repeat", "3", "-separator", ",", svSE, "word") code, out, errb := runOut("-repeat", "3", "-separator", ",", "-path", "word", svSE)
if code != 0 { if code != 0 {
t.Fatalf("run = %d, stderr=%q", code, errb) t.Fatalf("run = %d, stderr=%q", code, errb)
} }
@@ -85,7 +85,7 @@ func TestRunSeparator(t *testing.T) {
func TestRunRepeatAdvancesRNG(t *testing.T) { func TestRunRepeatAdvancesRNG(t *testing.T) {
// Each repeat is a fresh draw, not the same value N times. // Each repeat is a fresh draw, not the same value N times.
code, out, errb := runOut("-seed", "1", "-repeat", "5", svSE, "person") code, out, errb := runOut("-seed", "1", "-repeat", "5", "-path", "person", svSE)
if code != 0 { if code != 0 {
t.Fatalf("run = %d, stderr=%q", code, errb) t.Fatalf("run = %d, stderr=%q", code, errb)
} }
@@ -98,10 +98,39 @@ func TestRunRepeatAdvancesRNG(t *testing.T) {
} }
} }
func TestRunMultiplePaths(t *testing.T) {
// -path repeats: each given path renders once, in order, one per line.
code, out, errb := runOut("-path", "person", "-path", "word", svSE)
if code != 0 {
t.Fatalf("run = %d, stderr=%q", code, errb)
}
lines := strings.Split(strings.TrimRight(out, "\n"), "\n")
if len(lines) != 2 {
t.Errorf("two -path flags gave %d lines: %q", len(lines), out)
}
for _, l := range lines {
if strings.TrimSpace(l) == "" {
t.Errorf("empty value among %q", out)
}
}
}
func TestRunMultiplePathsWithRepeat(t *testing.T) {
// repeat × paths values: 2 repeats over 2 paths => 4 lines.
code, out, errb := runOut("-repeat", "2", "-path", "person", "-path", "word", svSE)
if code != 0 {
t.Fatalf("run = %d, stderr=%q", code, errb)
}
lines := strings.Split(strings.TrimRight(out, "\n"), "\n")
if len(lines) != 4 {
t.Errorf("repeat=2 × 2 paths gave %d lines: %q", len(lines), out)
}
}
func TestRunRepeatInvalid(t *testing.T) { func TestRunRepeatInvalid(t *testing.T) {
// A non-positive repeat is misuse. // A non-positive repeat is misuse.
for _, r := range []string{"0", "-1"} { for _, r := range []string{"0", "-1"} {
code, _, errb := runOut("-repeat", r, svSE, "word") code, _, errb := runOut("-repeat", r, "-path", "word", svSE)
if code != 2 { if code != 2 {
t.Errorf("repeat=%s = %d, want 2", r, code) t.Errorf("repeat=%s = %d, want 2", r, code)
} }
@@ -111,9 +140,9 @@ func TestRunRepeatInvalid(t *testing.T) {
} }
} }
func TestRunUsageOnTooFewArgs(t *testing.T) { func TestRunUsageOnMissingArgs(t *testing.T) {
// Need at least one data dir plus a path; fewer is misuse. // Need at least one -path and one data dir; fewer is misuse.
for _, args := range [][]string{{}, {svSE}} { for _, args := range [][]string{{}, {svSE}, {"-path", "person"}} {
code, _, errb := runOut(args...) code, _, errb := runOut(args...)
if code != 2 { if code != 2 {
t.Errorf("run(%v) = %d, want 2", args, code) t.Errorf("run(%v) = %d, want 2", args, code)
@@ -125,8 +154,8 @@ func TestRunUsageOnTooFewArgs(t *testing.T) {
} }
func TestRunMultipleDirs(t *testing.T) { func TestRunMultipleDirs(t *testing.T) {
// Several data dirs then a final path: all but the last arg are dirs. // Several data dirs after the path flags: all positionals are dirs.
code, out, errb := runOut(enUS, svSE, "person") code, out, errb := runOut("-path", "person", enUS, svSE)
if code != 0 { if code != 0 {
t.Fatalf("run(multi-dir) = %d, stderr=%q", code, errb) t.Fatalf("run(multi-dir) = %d, stderr=%q", code, errb)
} }
@@ -136,7 +165,7 @@ func TestRunMultipleDirs(t *testing.T) {
} }
func TestRunUnknownCategoryFails(t *testing.T) { func TestRunUnknownCategoryFails(t *testing.T) {
code, _, errb := runOut(svSE, "nope") code, _, errb := runOut("-path", "nope", svSE)
if code != 1 { if code != 1 {
t.Fatalf("run = %d, want 1", code) t.Fatalf("run = %d, want 1", code)
} }
@@ -146,7 +175,7 @@ func TestRunUnknownCategoryFails(t *testing.T) {
} }
func TestRunMissingDirFails(t *testing.T) { func TestRunMissingDirFails(t *testing.T) {
code, _, errb := runOut("../../data/nope", "person") code, _, errb := runOut("-path", "person", "../../data/nope")
if code != 1 { if code != 1 {
t.Fatalf("run = %d, want 1", code) t.Fatalf("run = %d, want 1", code)
} }