One CLI classifier settling the argument shape before load; JSON strings are templates and a quote is reserved in names
This commit is contained in:
+51
-36
@@ -27,11 +27,11 @@ const usage = `Usage: fejkdata [flags] <path|template>
|
|||||||
<template> a format string or JSON value to render inline, e.g.
|
<template> a format string or JSON value to render inline, e.g.
|
||||||
'name: {/sv_SE.person.last}' or '{"format":"{x}","x":["bosse","lina"]}'
|
'name: {/sv_SE.person.last}' or '{"format":"{x}","x":["bosse","lina"]}'
|
||||||
|
|
||||||
An argument containing a { token, or a JSON object or array, is a template; any
|
An argument containing a { token, or a JSON object, array or string, is a
|
||||||
other argument is a path (a path never contains a brace). Templates reach the data
|
template; any other argument is a path (a path never contains a brace, a bracket
|
||||||
with a reference: {/sv_SE.person.first} from the root, whether the data is shipped
|
or a quote). Templates reach the data by reference from the root —
|
||||||
or layered with --data-path. A [ that is not valid JSON names no template and no
|
{/sv_SE.person.first} — whether the data is shipped or layered with --data-path. A
|
||||||
path.
|
bracket that is not valid JSON names no template and no path.
|
||||||
|
|
||||||
-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
|
||||||
@@ -195,18 +195,22 @@ func parseArgs(argv []string) (invocation, error) {
|
|||||||
return in, nil
|
return in, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// check rejects a flag combination that cannot run.
|
// check rejects a flag combination or an argument that cannot run, and reports
|
||||||
func (in invocation) check() error {
|
// what the argument names, so its shape is settled before any data is read.
|
||||||
|
func (in invocation) check() (argKind, error) {
|
||||||
if in.list && len(in.paths) > 0 {
|
if in.list && len(in.paths) > 0 {
|
||||||
return errors.New("--list takes no path")
|
return argPath, errors.New("--list takes no path")
|
||||||
}
|
}
|
||||||
if in.list && (in.repeatSet || in.separatorSet) {
|
if in.list && (in.repeatSet || in.separatorSet) {
|
||||||
return errors.New("--list takes no --repeat or --separator")
|
return argPath, errors.New("--list takes no --repeat or --separator")
|
||||||
}
|
}
|
||||||
if !in.list && len(in.paths) != 1 {
|
if in.list {
|
||||||
return fmt.Errorf("expected one path, got %d", len(in.paths))
|
return argPath, nil
|
||||||
}
|
}
|
||||||
return nil
|
if len(in.paths) != 1 {
|
||||||
|
return argPath, fmt.Errorf("expected one path, got %d", len(in.paths))
|
||||||
|
}
|
||||||
|
return classify(in.paths[0])
|
||||||
}
|
}
|
||||||
|
|
||||||
func (in invocation) options() []fejkdata.Option {
|
func (in invocation) options() []fejkdata.Option {
|
||||||
@@ -223,24 +227,19 @@ func (in invocation) options() []fejkdata.Option {
|
|||||||
return opts
|
return opts
|
||||||
}
|
}
|
||||||
|
|
||||||
// write streams the path's renders to w, repeat of them joined by the separator
|
// write streams the argument's renders to w, repeat of them joined by the
|
||||||
// and ended by a newline. A path that renders once renders every time, so a Fake
|
// separator and ended by a newline. A value that renders once renders every time,
|
||||||
// failure comes before anything is written; a write failure surfaces from Flush,
|
// so a render failure comes before anything is written; a write failure surfaces
|
||||||
// bufio keeping the first one.
|
// from Flush, bufio keeping the first one.
|
||||||
func (in invocation) write(f *fejkdata.Generator, w io.Writer) error {
|
func (in invocation) write(f *fejkdata.Generator, kind argKind, w io.Writer) error {
|
||||||
arg := in.paths[0]
|
arg := in.paths[0]
|
||||||
var draw func() (string, error)
|
draw := func() (string, error) { return f.Fake(arg) }
|
||||||
switch {
|
if kind == argTemplate {
|
||||||
case isTemplate(arg):
|
|
||||||
t, err := f.NewTemplate(arg)
|
t, err := f.NewTemplate(arg)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return templateError{err}
|
return templateError{err}
|
||||||
}
|
}
|
||||||
draw = func() (string, error) { return t.Fake(), nil }
|
draw = func() (string, error) { return t.Fake(), nil }
|
||||||
case strings.HasPrefix(arg, "["):
|
|
||||||
return templateError{fmt.Errorf("%q starts with [ but is not valid JSON, so it names no template and no path", arg)}
|
|
||||||
default:
|
|
||||||
draw = func() (string, error) { return f.Fake(arg) }
|
|
||||||
}
|
}
|
||||||
out := bufio.NewWriter(w)
|
out := bufio.NewWriter(w)
|
||||||
for i := 0; i < in.repeat; i++ {
|
for i := 0; i < in.repeat; i++ {
|
||||||
@@ -264,16 +263,30 @@ type templateError struct{ error }
|
|||||||
|
|
||||||
func (e templateError) Unwrap() error { return e.error }
|
func (e templateError) Unwrap() error { return e.error }
|
||||||
|
|
||||||
// isTemplate reports whether an argument is an inline template rather than a
|
type argKind int
|
||||||
// path: a format string carrying a { token, or a JSON object or array. A name may
|
|
||||||
// not contain a brace or bracket, so both spellings collide with no path; the [
|
const (
|
||||||
// gate is valid-JSON so a [ alone never swallows an argument that merely began
|
argPath argKind = iota
|
||||||
// with a copied bracket.
|
argTemplate
|
||||||
func isTemplate(arg string) bool {
|
)
|
||||||
if strings.ContainsRune(arg, '{') {
|
|
||||||
return true
|
// classify reads what a positional argument names by its shape: a format string
|
||||||
|
// carrying a { token, or a JSON object, array or string, is an inline template;
|
||||||
|
// anything else is a path. A name may hold neither a brace nor a bracket nor a
|
||||||
|
// quote, so no path collides with any of those spellings, and the JSON gate is
|
||||||
|
// valid-JSON so a copied bracket names nothing rather than swallowing an argument.
|
||||||
|
func classify(arg string) (argKind, error) {
|
||||||
|
if strings.ContainsRune(arg, '{') || (isJSONStart(arg) && json.Valid([]byte(arg))) {
|
||||||
|
return argTemplate, nil
|
||||||
}
|
}
|
||||||
return strings.HasPrefix(arg, "[") && json.Valid([]byte(arg))
|
if i := strings.IndexAny(arg, "[]"); i >= 0 {
|
||||||
|
return argPath, fmt.Errorf("%q holds a %q, which no path may, and it is not valid JSON, so it names no template either", arg, arg[i:i+1])
|
||||||
|
}
|
||||||
|
return argPath, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func isJSONStart(arg string) bool {
|
||||||
|
return strings.HasPrefix(arg, "[") || strings.HasPrefix(arg, `"`)
|
||||||
}
|
}
|
||||||
|
|
||||||
func main() { os.Exit(run(os.Args[1:], os.Stdout, os.Stderr)) }
|
func main() { os.Exit(run(os.Args[1:], os.Stdout, os.Stderr)) }
|
||||||
@@ -292,7 +305,8 @@ func run(args []string, stdout, stderr io.Writer) int {
|
|||||||
fmt.Fprintln(stdout, "fejkdata "+buildVersion())
|
fmt.Fprintln(stdout, "fejkdata "+buildVersion())
|
||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
if err := in.check(); err != nil {
|
kind, err := in.check()
|
||||||
|
if err != nil {
|
||||||
return misuse(stderr, err)
|
return misuse(stderr, err)
|
||||||
}
|
}
|
||||||
f, err := fejkdata.New(in.options()...)
|
f, err := fejkdata.New(in.options()...)
|
||||||
@@ -309,7 +323,7 @@ func run(args []string, stdout, stderr io.Writer) int {
|
|||||||
}
|
}
|
||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
if err := in.write(f, stdout); err != nil {
|
if err := in.write(f, kind, stdout); err != nil {
|
||||||
var te templateError
|
var te templateError
|
||||||
if errors.As(err, &te) {
|
if errors.As(err, &te) {
|
||||||
return misuse(stderr, te.error)
|
return misuse(stderr, te.error)
|
||||||
@@ -321,7 +335,8 @@ func run(args []string, stdout, stderr io.Writer) int {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func misuse(stderr io.Writer, err error) int {
|
func misuse(stderr io.Writer, err error) int {
|
||||||
fmt.Fprintf(stderr, "fejkdata: %v\ntry 'fejkdata --help'\n", err)
|
// A library error already names the program, so the prefix is not doubled.
|
||||||
|
fmt.Fprintf(stderr, "fejkdata: %s\ntry 'fejkdata --help'\n", strings.TrimPrefix(err.Error(), "fejkdata: "))
|
||||||
return 2
|
return 2
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -140,12 +140,17 @@ func (t *template) compileFormat() error {
|
|||||||
// when the input is not JSON. A JSON string literal and a bare string compile
|
// when the input is not JSON. A JSON string literal and a bare string compile
|
||||||
// alike (both are a template with no fields); the other JSON scalars — a number,
|
// alike (both are a template with no fields); the other JSON scalars — a number,
|
||||||
// bool or null — are no template, so they are rejected here, as a data file that
|
// bool or null — are no template, so they are rejected here, as a data file that
|
||||||
// was one would be at load.
|
// was one would be at load. Padding is where the two readings would disagree — a
|
||||||
|
// format string renders it, JSON drops it — so a padded JSON value is rejected
|
||||||
|
// naming the one that renders.
|
||||||
func compileInput(input string) (node, error) {
|
func compileInput(input string) (node, error) {
|
||||||
var raw any
|
var raw any
|
||||||
if err := json.Unmarshal([]byte(input), &raw); err != nil {
|
if err := json.Unmarshal([]byte(input), &raw); err != nil {
|
||||||
return compile(input)
|
return compile(input)
|
||||||
}
|
}
|
||||||
|
if trimmed := strings.TrimSpace(input); trimmed != input {
|
||||||
|
return nil, fmt.Errorf("a JSON template may not be padded with spaces, which a format string would render; write %s", trimmed)
|
||||||
|
}
|
||||||
return compile(raw)
|
return compile(raw)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -350,15 +355,15 @@ func weightOf(raw any) (float64, error) {
|
|||||||
|
|
||||||
// reservedInName is what a category, folder or field name may not contain: a dot
|
// reservedInName is what a category, folder or field name may not contain: a dot
|
||||||
// separates the segments of a path, '|' the arms of a token, '(' opens a function
|
// separates the segments of a path, '|' the arms of a token, '(' opens a function
|
||||||
// call, braces delimit the token, '/' starts a reference, and a bracket would be
|
// call, braces delimit the token, '/' starts a reference, and brackets and a quote
|
||||||
// misread by the command line as a JSON array. A name carrying one is rejected
|
// open a JSON value. A name carrying one is rejected where it is authored rather
|
||||||
// where it is authored rather than where it would be unreachable.
|
// than where it would be unreachable.
|
||||||
const reservedInName = ".|({}/[]"
|
const reservedInName = ".|({}/[]\""
|
||||||
|
|
||||||
// reservedList spells reservedInName for an error message, so the two cannot drift.
|
// reservedList spells reservedInName for an error message, so the two cannot drift.
|
||||||
var reservedList = strings.Join(strings.Split(reservedInName, ""), " ")
|
var reservedList = strings.Join(strings.Split(reservedInName, ""), " ")
|
||||||
|
|
||||||
// checkName rejects a name the dot path, {token} and CLI grammars cannot spell.
|
// checkName rejects a name the dot path, {token} and JSON grammars cannot spell.
|
||||||
// Both a category or folder and a field go through it, so there is one answer to
|
// Both a category or folder and a field go through it, so there is one answer to
|
||||||
// what a name may contain.
|
// what a name may contain.
|
||||||
func checkName(name string) error {
|
func checkName(name string) error {
|
||||||
@@ -366,7 +371,7 @@ func checkName(name string) error {
|
|||||||
return fmt.Errorf("%q is empty, which is not a path segment, so List never offers it", name)
|
return fmt.Errorf("%q is empty, which is not a path segment, so List never offers it", name)
|
||||||
}
|
}
|
||||||
if i := strings.IndexAny(name, reservedInName); i >= 0 {
|
if i := strings.IndexAny(name, reservedInName); i >= 0 {
|
||||||
return fmt.Errorf("%q contains %q; a name may not use %s, which the dot path, {token} and CLI grammars reserve",
|
return fmt.Errorf("%q contains %q; a name may not use %s, which the dot path, {token} and JSON grammars reserve",
|
||||||
name, name[i:i+1], reservedList)
|
name, name[i:i+1], reservedList)
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
|
|||||||
Reference in New Issue
Block a user