GNU-style CLI flags, embedded data, and a literal format grammar #3
@@ -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
@@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user