diff --git a/README.md b/README.md index 9849ef3..0ac460c 100644 --- a/README.md +++ b/README.md @@ -44,7 +44,7 @@ level — folders, then the category (a JSON file), then fields. | `-d`, `--data-path D` | a directory to layer over the shipped data; repeatable, the last wins a name clash | | `--no-shipped-data` | load only the `--data-path` directories | | `-s`, `--seed N` | reproducible output | -| `-n`, `--repeat N` | render the path N times, each an independent draw | +| `-n`, `--repeat N` | render the path N times (up to 1048576), each an independent draw, streamed | | `--separator S` | between repeated values (default a newline) | | `--list` | print every path, then exit | | `--version`, `-h`, `--help` | print, then exit | diff --git a/builtins.go b/builtins.go index dfb6ce6..c121eb7 100644 --- a/builtins.go +++ b/builtins.go @@ -14,7 +14,7 @@ import ( // fat-fingered or overflowing argument fails at New instead of trying to allocate // gigabytes — or panicking — at render. const ( - maxLen = 1 << 20 // 1,048,576 chars/bytes + maxLen = MaxRepeat maxDecimals = 1024 ) diff --git a/cmd/fejkdata/main.go b/cmd/fejkdata/main.go index 0b335a1..a5514c0 100644 --- a/cmd/fejkdata/main.go +++ b/cmd/fejkdata/main.go @@ -8,6 +8,7 @@ package main import ( + "bufio" "errors" "fmt" "io" @@ -27,7 +28,7 @@ const usage = `Usage: fejkdata [flags] -h, --help print this help, then exit --list list the paths the data offers, then exit --no-shipped-data load only the --data-path directories - -n, --repeat N render the path N times (default 1) + -n, --repeat N render the path N times, 1..1048576 (default 1) -s, --seed N seed for reproducible output --separator S string between repeated values (default newline) --version print the version, then exit @@ -37,16 +38,18 @@ attaches or follows (-n3, -n 3); short flags bundle (-hn 3). ` type invocation struct { - dirs []string - help bool - list bool - noShipped bool - paths []string - repeat int - seed uint64 - seeded bool - separator string - version bool + dirs []string + help bool + list bool + noShipped bool + paths []string + repeat int + repeatSet bool + seed uint64 + seeded bool + separator string + separatorSet bool + version bool } type flagDef struct { @@ -63,10 +66,10 @@ var flagDefs = []flagDef{ {"no-shipped-data", "", false, func(in *invocation, _ string) error { in.noShipped = 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) + if err != nil || n < 1 || n > fejkdata.MaxRepeat { + return fmt.Errorf("--repeat needs an integer in 1..%d, got %q", fejkdata.MaxRepeat, v) } - in.repeat = n + in.repeat, in.repeatSet = n, true return nil }}, {"seed", "s", true, func(in *invocation, v string) error { @@ -77,7 +80,7 @@ var flagDefs = []flagDef{ in.seed, in.seeded = n, true return nil }}, - {"separator", "", true, func(in *invocation, v string) error { in.separator = v; return nil }}, + {"separator", "", true, func(in *invocation, v string) error { in.separator, in.separatorSet = v, true; return nil }}, {"version", "", false, func(in *invocation, _ string) error { in.version = true; return nil }}, } @@ -124,8 +127,8 @@ func splitFlags(arg string) ([]flagArg, error) { } var flags []flagArg letters := arg[1:] - for i := 0; i < len(letters); i++ { - letter := letters[i : i+1] + for i, r := range letters { + letter := string(r) def := flagByShort(letter) if def == nil { return nil, fmt.Errorf("unknown flag -%s", letter) @@ -134,7 +137,7 @@ func splitFlags(arg string) ([]flagArg, error) { flags = append(flags, flagArg{def: def}) continue } - rest := letters[i+1:] + rest := letters[i+len(letter):] 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:]) @@ -188,6 +191,9 @@ func (in invocation) check() error { if in.list && len(in.paths) > 0 { return errors.New("--list takes no path") } + if in.list && (in.repeatSet || in.separatorSet) { + return errors.New("--list takes no --repeat or --separator") + } if !in.list && len(in.paths) != 1 { return fmt.Errorf("expected one path, got %d", len(in.paths)) } @@ -208,17 +214,23 @@ func (in invocation) options() []fejkdata.Option { 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 { +// write streams the path's renders to w, repeat of them joined by the separator +// and ended by a newline. A path that renders once renders every time, so the only +// failure comes before anything is written. +func (in invocation) write(f *fejkdata.Generator, w io.Writer) error { + out := bufio.NewWriter(w) + for i := 0; i < in.repeat; i++ { v, err := f.Fake(in.paths[0]) if err != nil { - return "", err + return err } - vals[i] = v + if i > 0 { + out.WriteString(in.separator) + } + out.WriteString(v) } - return strings.Join(vals, in.separator), nil + out.WriteString("\n") + return out.Flush() } func main() { os.Exit(run(os.Args[1:], os.Stdout, os.Stderr)) } @@ -254,12 +266,10 @@ func run(args []string, stdout, stderr io.Writer) int { } return 0 } - out, err := in.values(f) - if err != nil { + if err := in.write(f, stdout); err != nil { fmt.Fprintln(stderr, err) return 1 } - fmt.Fprintln(stdout, out) return 0 } diff --git a/fejkdata.go b/fejkdata.go index ff7c052..0af610a 100644 --- a/fejkdata.go +++ b/fejkdata.go @@ -28,6 +28,9 @@ import ( //go:embed data var shippedFS embed.FS +// MaxRepeat caps a repeat, and the renders nested repeats multiply to along any path. +const MaxRepeat = 1 << 20 + // ErrNoData is returned by New when no source is loaded at all. var ErrNoData = errors.New("no data: WithoutShippedData needs at least one WithDataPath or WithDataFS")