From 2d93474cb117429e624e6f5543583439e7e1745b Mon Sep 17 00:00:00 2001 From: Arpad Ryszka Date: Fri, 5 Sep 2025 03:19:00 +0200 Subject: [PATCH] config, env, options testing --- Makefile | 11 +- command.go | 15 +- commandline.go | 4 +- commandline_test.go | 841 ++++++++++++++++++++++++++++++---- config.go | 50 +- config_test.go | 167 +++++-- debug.go | 15 + docreflect_test.go | 17 + env_test.go | 91 +++- exec.go | 8 +- exec_test.go | 49 +- go.mod | 4 +- go.sum | 4 + help.go | 22 +- ini.treerack | 18 +- iniparser.gen.go | 383 ++++------------ input.go | 18 +- internal/tests/config.ini | 8 - internal/tests/testlib/lib.go | 25 + lib.go | 14 +- reflect.go | 135 +----- reflect_test.go | 51 ++- 22 files changed, 1336 insertions(+), 614 deletions(-) create mode 100644 debug.go create mode 100644 docreflect_test.go delete mode 100644 internal/tests/config.ini create mode 100644 internal/tests/testlib/lib.go diff --git a/Makefile b/Makefile index 37f610c..cb08378 100644 --- a/Makefile +++ b/Makefile @@ -8,10 +8,10 @@ lib: $(SOURCES) iniparser.gen.go docreflect.gen.go build: lib .build/wand -check: $(SOURCES) build +check: $(SOURCES) build docreflect_test.go go test -count 1 ./... -.cover: $(SOURCES) build +.cover: $(SOURCES) build docreflect_test.go go test -count 1 -coverprofile .cover ./... cover: .cover @@ -33,6 +33,13 @@ docreflect.gen.go: $(SOURCES) > docreflect.gen.go \ || rm -f docreflect.gen.go +docreflect_test.go: $(SOURCES) + go run script/docreflect/docs.go \ + wand_test \ + code.squareroundforest.org/arpio/wand/internal/tests/testlib \ + > docreflect_test.go \ + || rm -f docreflect_test.go + .build: mkdir -p .build diff --git a/command.go b/command.go index c60160e..4c276a5 100644 --- a/command.go +++ b/command.go @@ -6,6 +6,7 @@ import ( "fmt" "reflect" "regexp" + "strings" ) var commandNameExpression = regexp.MustCompile("^[a-zA-Z_][a-zA-Z_0-9]*$") @@ -119,7 +120,7 @@ func validateImpl(cmd Cmd, conf Config) error { } func validateCommandTree(cmd Cmd, conf Config) error { - if cmd.isHelp { + if cmd.helpFor != nil { return nil } @@ -231,8 +232,13 @@ func validateShortFormsTree(cmd Cmd) (map[string]string, map[string]string, erro } mf := mapFields(cmd.impl) + _, helpDefined := mf["help"] for i := 0; i < len(cmd.shortForms); i += 2 { s, l := cmd.shortForms[i], cmd.shortForms[i+1] + if s == "h" && l == "help" && !helpDefined { + continue + } + r := []rune(s) if len(r) != 1 || r[0] < 'a' || r[0] > 'z' { return nil, nil, fmt.Errorf("invalid short form: %s", s) @@ -275,7 +281,12 @@ func validateShortForms(cmd Cmd) error { } if len(um) != 0 { - return errors.New("unmapped short forms") + var umn []string + for n := range um { + umn = append(umn, n) + } + + return fmt.Errorf("unmapped short forms: %s", strings.Join(umn, ", ")) } return nil diff --git a/commandline.go b/commandline.go index 193e2d6..b096d02 100644 --- a/commandline.go +++ b/commandline.go @@ -131,9 +131,9 @@ func selectCommand(cmd Cmd, args []string) (Cmd, []string, []string) { return defaultCommand(cmd), []string{cmd.name}, args } - cmd, fullCommand, args := selectCommand(sc, args[1:]) + sc, fullCommand, args := selectCommand(sc, args[1:]) fullCommand = append([]string{cmd.name}, fullCommand...) - return cmd, fullCommand, args + return sc, fullCommand, args } func boolOption(name string, value bool) option { diff --git a/commandline_test.go b/commandline_test.go index 8319ee8..e2eae1d 100644 --- a/commandline_test.go +++ b/commandline_test.go @@ -1,6 +1,5 @@ package wand -/* import ( "fmt" "strings" @@ -39,6 +38,7 @@ func TestCommand(t *testing.T) { One, Two bool Three string } + fm := func(m m) string { return fmt.Sprintf("%t;%t;%s", m.One, m.Two, m.Three) } @@ -81,106 +81,607 @@ func TestCommand(t *testing.T) { return fmt.Sprint(d.One2) } - t.Run("no args", testExec(t, ff, "", "foo", "", "0")) + t.Run("no args", testExec(testCase{impl: ff, command: "foo"}, "", "0")) + t.Run("basic options", func(t *testing.T) { - t.Run("space", testExec(t, ff, "", "foo --one baz --second-field 42", "", "baz42")) - t.Run("eq", testExec(t, ff, "", "foo --one=baz --second-field=42", "", "baz42")) + t.Run("space", testExec(testCase{impl: ff, command: "foo --one baz --second-field 42"}, "", "baz42")) + t.Run("eq", testExec(testCase{impl: ff, command: "foo --one=baz --second-field=42"}, "", "baz42")) }) t.Run("short options combined, explicit last", func(t *testing.T) { - t.Run("bool last", testExec(t, ShortForm(fb, "one", "a", "two", "b", "three", "c"), "", "foo -abc true", "", "true;true;true;false")) - t.Run("string last", testExec(t, ShortForm(fm, "one", "a", "two", "b", "three", "c"), "", "foo -abc bar", "", "true;true;bar")) + t.Run( + "bool last", + testExec( + testCase{ + impl: ShortForm(Command("foo", fb), "a", "one", "b", "two", "c", "three"), + command: "foo -abc true", + }, + "", + "true;true;true;false", + ), + ) + + t.Run( + "string last", + testExec( + testCase{ + impl: ShortForm(Command("foo", fm), "a", "one", "b", "two", "c", "three"), + command: "foo -abc bar", + }, + "", + "true;true;bar", + ), + ) }) t.Run("multiple values", func(t *testing.T) { - t.Run("bools, short", testExec(t, ShortForm(fl, "one", "a"), "", "foo -a -a -a", "", "true,true,true;")) - t.Run("bools, short, combined", testExec(t, ShortForm(fl, "one", "a"), "", "foo -aaa", "", "true,true,true;")) - t.Run("bools, short, explicit", testExec(t, ShortForm(fl, "one", "a"), "", "foo -a true -a true -a true", "", "true,true,true;")) - t.Run("bools, short, combined, last explicit", testExec(t, ShortForm(fl, "one", "a"), "", "foo -aaa true", "", "true,true,true;")) - t.Run("bools, long", testExec(t, fl, "", "foo --one --one --one", "", "true,true,true;")) - t.Run("bools, long, explicit", testExec(t, fl, "", "foo --one true --one true --one true", "", "true,true,true;")) - t.Run("mixd, short", testExec(t, ShortForm(fl, "one", "a", "two", "b"), "", "foo -a -b bar", "", "true;bar")) - t.Run("mixed, short, combined", testExec(t, ShortForm(fl, "one", "a", "two", "b"), "", "foo -ab bar", "", "true;bar")) - t.Run("mixed, long", testExec(t, fl, "", "foo --one --two bar", "", "true;bar")) - t.Run("mixed, long, explicit", testExec(t, fl, "", "foo --one true --two bar", "", "true;bar")) + t.Run( + "bools, short", + testExec( + testCase{ + impl: ShortForm(Command("foo", fl), "a", "one"), + command: "foo -a -a -a", + }, + "", + "true,true,true;", + ), + ) + + t.Run( + "bools, short, combined", + testExec( + testCase{ + impl: ShortForm(Command("foo", fl), "a", "one"), + command: "foo -aaa", + }, + "", + "true,true,true;", + ), + ) + + t.Run( + "bools, short, explicit", + testExec( + testCase{ + impl: ShortForm(Command("foo", fl), "a", "one"), + command: "foo -a true -a true -a true", + }, + "", + "true,true,true;", + ), + ) + + t.Run( + "bools, short, combined, last explicit", + testExec( + testCase{ + impl: ShortForm(Command("foo", fl), "a", "one"), + command: "foo -aaa true", + }, + "", + "true,true,true;", + ), + ) + + t.Run( + "bools, long", + testExec( + testCase{ + impl: fl, + command: "foo --one --one --one", + }, + "", + "true,true,true;", + ), + ) + + t.Run( + "bools, long, explicit", + testExec( + testCase{ + impl: fl, + command: "foo --one true --one true --one true", + }, + "", + "true,true,true;"), + ) + + t.Run( + "mixd, short", + testExec( + testCase{ + impl: ShortForm(Command("foo", fl), "a", "one", "b", "two"), + command: "foo -a -b bar", + }, + "", + "true;bar", + ), + ) + + t.Run( + "mixed, short, combined", + testExec( + testCase{ + impl: ShortForm(Command("foo", fl), "a", "one", "b", "two"), + command: "foo -ab bar", + }, + "", + "true;bar", + ), + ) + + t.Run( + "mixed, long", + testExec( + testCase{ + impl: fl, + command: "foo --one --two bar", + }, + "", + "true;bar", + ), + ) + + t.Run( + "mixed, long, explicit", + testExec( + testCase{ + impl: fl, + command: "foo --one true --two bar", + }, + "", + "true;bar", + ), + ) }) t.Run("implicit bool option", func(t *testing.T) { - t.Run("short", testExec(t, ShortForm(fb, "one", "a"), "", "foo -a", "", "true;false;false;false")) + t.Run( + "short", + testExec( + testCase{ + impl: ShortForm(Command("foo", fb), "a", "one"), + command: "foo -a", + }, + "", + "true;false;false;false", + ), + ) + t.Run( "short, multiple", - testExec(t, ShortForm(fb, "one", "a", "two", "b", "three", "c"), "", "foo -a -b -c", "", "true;true;true;false"), + testExec( + testCase{ + impl: ShortForm(Command("foo", fb), "a", "one", "b", "two", "c", "three"), + command: "foo -a -b -c", + }, + "", + "true;true;true;false", + ), ) t.Run( "short, combined", - testExec(t, ShortForm(fb, "one", "a", "two", "b", "three", "c"), "", "foo -abc", "", "true;true;true;false"), + testExec( + testCase{ + impl: ShortForm(Command("foo", fb), "a", "one", "b", "two", "c", "three"), + command: "foo -abc", + }, + "", + "true;true;true;false", + ), ) t.Run( "short, combined, multiple", - testExec(t, ShortForm(fb, "one", "a", "two", "b", "three", "c", "four", "d"), "", "foo -ab -cd", "", "true;true;true;true"), + testExec( + testCase{ + impl: ShortForm(Command("foo", fb), "a", "one", "b", "two", "c", "three", "d", "four"), + command: "foo -ab -cd", + }, + "", + "true;true;true;true", + ), ) t.Run( "short, multiple values", - testExec(t, ShortForm(flb, "one", "a", "two", "b", "three", "c"), "", "foo -aba -cab", "", "true,true,true;true,true;true"), - ) - - t.Run("long", testExec(t, fb, "", "foo --one", "", "true;false;false;false")) - t.Run("long, multiple", testExec(t, fb, "", "foo --one --two --three", "", "true;true;true;false")) - t.Run("long, multiple values", testExec(t, flb, "", "foo --one --two --one", "", "true,true;true;")) - }) - - t.Run("explicit bool option", func(t *testing.T) { - t.Run("short, true", testExec(t, ShortForm(fb, "one", "a"), "", "foo -a true", "", "true;false;false;false")) - t.Run("short, false", testExec(t, ShortForm(fb, "one", "a"), "", "foo -a false", "", "false;false;false;false")) - t.Run("short, with eq", testExec(t, ShortForm(fb, "one", "a"), "", "foo -a=true", "", "true;false;false;false")) - t.Run("short, true variant, capital", testExec(t, ShortForm(fb, "one", "a"), "", "foo -a True", "", "true;false;false;false")) - t.Run("short, true variant, 1", testExec(t, ShortForm(fb, "one", "a"), "", "foo -a 1", "", "true;false;false;false")) - t.Run("short, false variant, 0", testExec(t, ShortForm(fb, "one", "a"), "", "foo -a 0", "", "false;false;false;false")) - t.Run("short, combined", testExec(t, ShortForm(fb, "one", "a", "two", "b"), "", "foo -ab true", "", "true;true;false;false")) - t.Run( - "short, combined, multiple", testExec( - t, - ShortForm(fb, "one", "a", "two", "b", "three", "c", "four", "d"), - "", "foo -ab true -cd true", - "", "true;true;true;true", + testCase{ + impl: ShortForm(Command("foo", flb), "a", "one", "b", "two", "c", "three"), + command: "foo -aba -cab", + }, + "", + "true,true,true;true,true;true", ), ) - t.Run("long", testExec(t, fb, "", "foo --one true", "", "true;false;false;false")) - t.Run("long, false", testExec(t, fb, "", "foo --one false", "", "false;false;false;false")) - t.Run("logn, with eq", testExec(t, fb, "", "foo --one=true", "", "true;false;false;false")) - t.Run("long, mixed, first", testExec(t, fb, "", "foo --one false --two", "", "false;true;false;false")) - t.Run("long, mixed, last", testExec(t, fb, "", "foo --one --two false", "", "true;false;false;false")) + t.Run( + "long", + testExec( + testCase{ + impl: fb, + command: "foo --one", + }, + "", + "true;false;false;false", + ), + ) + + t.Run( + "long, multiple", + testExec( + testCase{ + impl: fb, + command: "foo --one --two --three", + }, + "", + "true;true;true;false", + ), + ) + + t.Run( + "long, multiple values", + testExec( + testCase{ + impl: flb, + command: "foo --one --two --one", + }, + "", + "true,true;true;", + ), + ) + }) + + t.Run("explicit bool option", func(t *testing.T) { + t.Run( + "short, true", + testExec( + testCase{ + impl: ShortForm(Command("foo", fb), "a", "one"), + command: "foo -a true", + }, + "", + "true;false;false;false", + ), + ) + + t.Run( + "short, false", + testExec( + testCase{ + impl: ShortForm(Command("foo", fb), "a", "one"), + command: "foo -a false", + }, + "", + "false;false;false;false", + ), + ) + + t.Run( + "short, with eq", + testExec( + testCase{ + impl: ShortForm(Command("foo", fb), "a", "one"), + command: "foo -a=true", + }, + "", + "true;false;false;false", + ), + ) + + t.Run( + "short, true variant, capital", + testExec( + testCase{ + impl: ShortForm(Command("foo", fb), "a","one"), + command: "foo -a True", + }, + "", + "true;false;false;false", + ), + ) + + t.Run( + "short, true variant, 1", + testExec( + testCase{ + impl: ShortForm(Command("foo", fb), "a", "one"), + command: "foo -a 1", + }, + "", + "true;false;false;false", + ), + ) + + t.Run( + "short, false variant, 0", + testExec( + testCase{ + impl: ShortForm(Command("foo", fb), "a", "one"), + command: "foo -a 0", + }, + "", + "false;false;false;false", + ), + ) + + t.Run( + "short, combined", + testExec( + testCase{ + impl: ShortForm(Command("foo", fb), "a", "one", "b", "two"), + command: "foo -ab true", + }, + "", + "true;true;false;false", + ), + ) + + t.Run( + "short, combined, multiple", + testExec( + testCase{ + impl: ShortForm(Command("foo", fb), "a", "one", "b", "two", "c", "three", "d", "four"), + command: "foo -ab true -cd true", + }, + "", + "true;true;true;true", + ), + ) + + t.Run( + "long", + testExec( + testCase{ + impl: fb, + command: "foo --one true", + }, + "", + "true;false;false;false", + ), + ) + + t.Run( + "long, false", + testExec( + testCase{ + impl: fb, + command: "foo --one false", + }, + "", + "false;false;false;false", + ), + ) + + t.Run( + "logn, with eq", + testExec( + testCase{ + impl: fb, + command: "foo --one=true", + }, + "", + "true;false;false;false"), + ) + + t.Run( + "long, mixed, first", + testExec( + testCase{ + impl: fb, + command: "foo --one false --two", + }, + "", + "false;true;false;false", + ), + ) + + t.Run( + "long, mixed, last", + testExec( + testCase{ + impl: fb, + command: "foo --one --two false", + }, + "", + "true;false;false;false", + ), + ) }) t.Run("expected bool option", func(t *testing.T) { - t.Run("short, implicit", testExec(t, ShortForm(fb, "one", "a"), "", "foo -a", "", "true;false;false;false")) - t.Run("short, explicit", testExec(t, ShortForm(fb, "one", "a"), "", "foo -a true", "", "true;false;false;false")) - t.Run("short, automatic positional", testExec(t, ShortForm(fbp, "one", "a"), "", "foo -a bar", "", "true;false;false;false;bar")) - t.Run("short, combined", testExec(t, ShortForm(fb, "one", "a", "two", "b"), "", "foo -ab true", "", "true;true;false;false")) t.Run( - "short, combined, automatic positional", - testExec(t, ShortForm(fbp, "one", "a", "two", "b"), "", "foo -ab bar", "", "true;true;false;false;bar"), + "short, implicit", + testExec( + testCase{ + impl: ShortForm(Command("foo", fb), "a", "one"), + command: "foo -a", + }, + "", + "true;false;false;false"), ) - t.Run("long, implicit", testExec(t, fb, "", "foo --one", "", "true;false;false;false")) - t.Run("long, explicit", testExec(t, fb, "", "foo --one true", "", "true;false;false;false")) - t.Run("long, automatic positional", testExec(t, fbp, "", "foo --one bar", "", "true;false;false;false;bar")) + t.Run( + "short, explicit", + testExec( + testCase{ + impl: ShortForm(Command("foo", fb), "a", "one"), + command: "foo -a true", + }, + "", + "true;false;false;false", + ), + ) + + t.Run( + "short, automatic positional", + testExec( + testCase{ + impl: ShortForm(Command("foo", fbp), "a", "one"), + command: "foo -a bar", + }, + "", + "true;false;false;false;bar", + ), + ) + + t.Run( + "short, combined", + testExec( + testCase{ + impl: ShortForm(Command("foo", fb), "a","one", "b", "two"), + command: "foo -ab true", + }, + "", + "true;true;false;false", + ), + ) + + t.Run( + "short, combined, automatic positional", + testExec( + testCase{ + impl: ShortForm(Command("foo", fbp), "a", "one", "b", "two"), + command: "foo -ab bar", + }, + "", + "true;true;false;false;bar"), + ) + + t.Run( + "long, implicit", + testExec( + testCase{ + impl: fb, + command: "foo --one", + }, + "", + "true;false;false;false", + ), + ) + + t.Run( + "long, explicit", + testExec( + testCase{ + impl: fb, + command: "foo --one true", + }, + "", + "true;false;false;false", + ), + ) + + t.Run( + "long, automatic positional", + testExec( + testCase{ + impl: fbp, + command: "foo --one bar", + }, + "", + "true;false;false;false;bar", + ), + ) }) t.Run("positional", func(t *testing.T) { - t.Run("basic", testExec(t, fp, "", "foo bar baz", "", "0;bar,baz")) - t.Run("explicit", testExec(t, fp, "", "foo -- bar baz", "", "0;bar,baz")) - t.Run("mixed", testExec(t, fp, "", "foo bar -- baz", "", "0;bar,baz")) - t.Run("with option", testExec(t, fp, "", "foo bar --second-field 42 baz", "", "42;bar,baz")) - t.Run("with bool option at the end", testExec(t, fbp, "", "foo bar baz --one", "", "true;false;false;false;bar;baz")) - t.Run("with expected bool, implicit", testExec(t, fbp, "", "foo bar --one baz", "", "true;false;false;false;bar;baz")) - t.Run("with expected bool, explicit", testExec(t, fbp, "", "foo bar --one true baz", "", "true;false;false;false;bar;baz")) - t.Run("option format", testExec(t, fbp, "", "foo -- --one", "", "false;false;false;false;--one")) + t.Run( + "basic", + testExec( + testCase{ + impl: fp, + command: "foo bar baz", + }, + "", + "0;bar,baz", + ), + ) + + t.Run( + "explicit", + testExec( + testCase{ + impl: fp, + command: "foo -- bar baz", + }, + "", + "0;bar,baz", + ), + ) + + t.Run( + "mixed", + testExec( + testCase{ + impl: fp, + command: "foo bar -- baz", + }, + "", + "0;bar,baz", + ), + ) + + t.Run( + "with option", + testExec( + testCase{ + impl: fp, + command: "foo bar --second-field 42 baz", + }, + "", + "42;bar,baz", + ), + ) + + t.Run( + "with bool option at the end", + testExec( + testCase{ + impl: fbp, + command: "foo bar baz --one", + }, + "", + "true;false;false;false;bar;baz", + ), + ) + + t.Run( + "with expected bool, implicit", + testExec( + testCase{ + impl: fbp, + command: "foo bar --one baz", + }, + "", + "true;false;false;false;bar;baz", + ), + ) + + t.Run( + "with expected bool, explicit", + testExec( + testCase{ + impl: fbp, + command: "foo bar --one true baz", + }, + "", + "true;false;false;false;bar;baz", + ), + ) + + t.Run( + "option format", + testExec( + testCase{ + impl: fbp, + command: "foo -- --one", + }, + "", + "false;false;false;false;--one", + ), + ) }) t.Run("example", func(t *testing.T) { @@ -203,10 +704,10 @@ func TestCommand(t *testing.T) { t.Run( "full", testExec( - t, - ShortForm(fs, "foo", "a", "bar", "b"), - "", - "foo -ab --bar baz -b --qux --quux corge -- grault", + testCase{ + impl: ShortForm(Command("foo", fs), "a", "foo", "b", "bar"), + command: "foo -ab --bar baz -b --qux --quux corge -- grault", + }, "", "true;true,true,true;true;corge;baz;grault", ), @@ -214,28 +715,204 @@ func TestCommand(t *testing.T) { }) t.Run("expected or unexpected", func(t *testing.T) { - t.Run("capital letters", testExec(t, fp, "", "foo --One bar", "", "0;--One,bar")) - t.Run("digit in option name", testExec(t, fd, "", "foo --one-2", "", "true")) - t.Run("dash in option name", testExec(t, ff, "", "foo --second-field 42", "", "42")) - t.Run("unpexpected character", testExec(t, fp, "", "foo --one#", "", "0;--one#")) t.Run( - "invalid short option set", - testExec(t, ShortForm(fp, "one", "a", "one", "b", "second-field", "c"), "", "foo -aBc", "", "0;-aBc"), + "capital letters", + testExec( + testCase{ + impl: fp, + command: "foo --One bar", + }, + "", + "0;--One,bar", + ), ) - t.Run("positional separator, no value", testExec(t, fp, "", "foo --one bar --", "", "bar0;")) - t.Run("positional separator, expecting value", testExec(t, fp, "", "foo --one --", "--one", "")) - t.Run("shot flag set, expecting value", testExec(t, ShortForm(fp, "second-field", "b"), "", "foo --one -b", "--one", "")) + t.Run( + "digit in option name", + testExec( + testCase{ + impl: fd, + command: "foo --one-2", + }, + "", + "true", + ), + ) + + t.Run( + "dash in option name", + testExec( + testCase{ + impl: ff, + command: "foo --second-field 42", + }, + "", + "42", + ), + ) + + t.Run( + "unpexpected character", + testExec( + testCase{ + impl: fp, + command: "foo --one#", + }, + "", + "0;--one#", + ), + ) + + t.Run( + "invalid short option set", + testExec( + testCase{ + impl: ShortForm(Command("foo", fp), "a", "one", "b", "one", "c", "second-field"), + command: "foo -aBc", + }, + "", + "0;-aBc", + ), + ) + + t.Run( + "positional separator, no value", + testExec( + testCase{ + impl: fp, + command: "foo --one bar --", + }, + "", + "bar0;", + ), + ) + + t.Run( + "positional separator, expecting value", + testExec( + testCase{ + impl: fp, + command: "foo --one --", + }, + "--one", + "", + ), + ) + + t.Run( + "shot flag set, expecting value", + testExec( + testCase{ + impl: ShortForm(Command("foo", fp), "b", "second-field"), + command: "foo --one -b", + }, + "--one", + "", + ), + ) }) t.Run("preserve order", func(t *testing.T) { - t.Run("bools", testExec(t, fl, "", "foo --one --one false --one", "", "true,false,true;")) - t.Run("strings", testExec(t, fl, "", "foo --two 1 --two 2 --two 3", "", ";1,2,3")) + t.Run( + "bools", + testExec( + testCase{ + impl: fl, + command: "foo --one --one false --one", + }, + "", + "true,false,true;", + ), + ) + + t.Run( + "strings", + testExec( + testCase{ + impl: fl, + command: "foo --two 1 --two 2 --two 3", + }, + "", + ";1,2,3", + ), + ) }) t.Run("select subcommand", func(t *testing.T) { - t.Run("named", testExec(t, Command("", nil, Command("bar", ff), Command("baz", ff)), "", "foo baz", "", "0")) - t.Run("default", testExec(t, Command("", nil, Command("bar", ff), Default(Command("baz", ff))), "", "foo", "", "0")) + t.Run( + "named", + testExec( + testCase{ + impl: Group("foo", Command("bar", ff), Command("baz", ff)), + command: "foo baz", + }, + "", + "0", + ), + ) + + t.Run( + "default", + testExec( + testCase{ + impl: Group("foo", Command("bar", ff), Default(Command("baz", ff))), + command: "foo", + }, + "", + "0", + ), + ) + }) + + t.Run("help option", func(t *testing.T) { + t.Run( + "short form not defined", + testExec( + testCase{ + impl: fm, + command: "foo -h", + }, + "foo help", + "", + ), + ) + + t.Run( + "short form not help", + testExec( + testCase{ + impl: ShortForm(Command("foo", fm), "h", "one"), + command: "foo -h", + }, + "", + "true;false;", + ), + ) + + t.Run( + "short form", + testExec( + testCase{ + impl: ShortForm(Command("foo", fm), "h", "help"), + command: "foo -h", + contains: true, + }, + "", + "Synopsis", + ), + ) + + t.Run( + "long form", + testExec( + testCase{ + impl: fm, + command: "foo --help", + contains: true, + }, + "", + "Synopsis", + ), + ) }) } -*/ diff --git a/config.go b/config.go index 76f2930..df273b3 100644 --- a/config.go +++ b/config.go @@ -61,6 +61,39 @@ func (f *file) Close() error { return nil } +func unescapeConfig(s string) string { + var ( + u []rune + escaped bool + ) + + r := []rune(s) + for _, ri := range r { + if escaped { + u = append(u, ri) + continue + } + + if ri == '\\' { + escaped = true + continue + } + + u = append(u, ri) + } + + return string(u) +} + +func unquoteConfig(s string) string { + if len(s) < 2 { + return s + } + + s = s[1 : len(s)-1] + return unescapeConfig(s) +} + func readConfigFile(cmd Cmd, conf Config) (config, error) { var f io.ReadCloser if conf.test == "" { @@ -97,11 +130,22 @@ func readConfigFile(cmd Cmd, conf Config) (config, error) { continue } - if token.Name == "value" { - value = token.Text() - hasValue = true + if token.Name != "value" { continue } + + hasValue = true + for _, valueToken := range token.Nodes { + if valueToken.Name == "value-chars" { + value = unescapeConfig(valueToken.Text()) + break + } + + if valueToken.Name == "quoted" { + value = unquoteConfig(valueToken.Text()) + break + } + } } if c.originalNames == nil { diff --git a/config_test.go b/config_test.go index f32346a..d078570 100644 --- a/config_test.go +++ b/config_test.go @@ -1,44 +1,151 @@ package wand import ( - "bytes" - "code.squareroundforest.org/arpio/notation" + "fmt" + "strings" "testing" ) func TestConfig(t *testing.T) { - type options struct { - FooBarBaz int - Foo string - FooBar []string + type f struct{ One, SecondVar string } + ff := func(f f) string { + return f.One + f.SecondVar } - impl := func(o options) { - if o.FooBarBaz != 42 { - t.Fatal(notation.Sprintw(o)) - } - - if o.Foo != "" { - t.Fatal(notation.Sprintw(o)) - } - - if len(o.FooBar) != 2 || o.FooBar[0] != "bar" || o.FooBar[1] != "baz" { - t.Fatal(notation.Sprintw(o)) - } + type i struct{ One, SecondVar int } + fi := func(i i) string { + return fmt.Sprintf("%d;%d", i.One, i.SecondVar) } - stdin := bytes.NewBuffer(nil) - stdout := bytes.NewBuffer(nil) - stderr := bytes.NewBuffer(nil) + type m struct{ One, SecondVar []string } + fm := func(m m) string { + return strings.Join([]string{strings.Join(m.One, ","), strings.Join(m.SecondVar, ",")}, ";") + } - exec( - stdin, - stdout, - stderr, - func(int) {}, - Command("test", impl), - SystemConfig(), - nil, - []string{"test", "--config", "./internal/tests/config.ini"}, + t.Run( + "common config var casing", + testExec(testCase{impl: ff, conf: "one=bar\nsecond_var=baz", command: "foo"}, "", "barbaz"), ) + + t.Run( + "camel casing", + testExec(testCase{impl: ff, conf: "one=bar\nsecondVar=baz", command: "foo"}, "", "barbaz"), + ) + + t.Run( + "empty var", + testExec(testCase{impl: ff, conf: "one=bar\nsecondVar=", command: "foo"}, "", "bar"), + ) + + t.Run( + "discard env var", + testExec(testCase{impl: ff, conf: "one=bar\nsecondVar=baz\none", command: "foo"}, "", "baz"), + ) + + t.Run( + "eq in value", + + // this one is a good example for fixing the error reporting in the parser. Try it by removing the + // quotes: + testExec(testCase{impl: ff, conf: "one=\"bar=baz\"", command: "foo"}, "", "bar=baz"), + ) + + t.Run( + "keeps original name", + testExec(testCase{impl: fi, conf: "ONE=bar", command: "foo"}, "ONE", ""), + ) + + t.Run( + "keeps original name, last wins on conflict", + testExec(testCase{impl: fi, conf: "ONE=bar\none=baz", command: "foo"}, "one", ""), + ) + + t.Run( + "2 entries", + testExec(testCase{impl: fm, conf: "one=bar\none=baz", command: "foo"}, "", "bar,baz;"), + ) + + t.Run( + "3 entries", + testExec(testCase{impl: fm, conf: "one=bar\none=baz\none=qux", command: "foo"}, "", "bar,baz,qux;"), + ) + + t.Run( + "with empty", + testExec(testCase{impl: fm, conf: "one=bar\none=baz\none=\none=qux\none=", command: "foo"}, "", "bar,baz,,qux,;"), + ) + + t.Run( + "escape", + testExec(testCase{impl: fm, conf: "one=bar\\\nbaz", command: "foo"}, "", "bar\nbaz;"), + ) + + t.Run( + "quote", + testExec(testCase{impl: fm, conf: "one=\"bar\nbaz\"", command: "foo"}, "", "bar\nbaz;"), + ) + + t.Run( + "escape char", + testExec(testCase{impl: fm, conf: "one=bar\\\\\none=baz", command: "foo"}, "", "bar\\,baz;"), + ) + + t.Run( + "escape char last", + testExec(testCase{impl: fm, conf: "one=bar\\", command: "foo"}, "parse failed", ""), + ) + + t.Run( + "discard in same doc", + testExec(testCase{impl: fm, conf: "one=bar\nsecond-var=baz\none", command: "foo"}, "", ";baz"), + ) + + t.Run( + "discard in previous doc", + testExec( + testCase{ + impl: fm, + mergeConf: []string{"one=bar\nsecond-var=baz", "one\nsecond-var=qux"}, + command: "foo", + }, + "", + ";qux", + ), + ) + + t.Run( + "discard in previous same doc", + testExec( + testCase{ + impl: fm, + mergeConf: []string{"one=bar\nsecond-var=baz", "one\nsecond-var=qux\nsecond-var"}, + command: "foo", + }, + "", + ";", + ), + ) + + // check the syntax for tests + // - white space ignored + // - comment line + // - comment at the end of an entry + // - invalid key + // check the code for tests + + t.Run("white space ignored", testExec(testCase{impl: fm, conf: "one = bar ", command: "foo"}, "", "bar;")) + t.Run( + "comments", + testExec( + testCase{ + impl: fm, + conf: "# comment on a line\none=bar # comment after an entry", + command: "foo", + }, + "", + "bar;", + ), + ) + + t.Run("invald key", testExec(testCase{impl: fm, conf: "one two = bar", command: "foo"}, "parse failed", "")) } diff --git a/debug.go b/debug.go new file mode 100644 index 0000000..a86770c --- /dev/null +++ b/debug.go @@ -0,0 +1,15 @@ +package wand + +import ( + "code.squareroundforest.org/arpio/notation" + "os" + "testing" +) + +func debug(a ...any) { + if !testing.Testing() { + return + } + + notation.Fprintlnw(os.Stderr, a...) +} diff --git a/docreflect_test.go b/docreflect_test.go new file mode 100644 index 0000000..d665ad9 --- /dev/null +++ b/docreflect_test.go @@ -0,0 +1,17 @@ +/* +Generated with https://code.squareroundforest.org/arpio/docreflect +*/ + + +package wand_test +import "code.squareroundforest.org/arpio/docreflect" +func init() { +docreflect.Register("code.squareroundforest.org/arpio/wand/internal/tests/testlib", "") +docreflect.Register("code.squareroundforest.org/arpio/wand/internal/tests/testlib.Bar", "\nfunc(out, a, b, c)") +docreflect.Register("code.squareroundforest.org/arpio/wand/internal/tests/testlib.Baz", "\nfunc(o)") +docreflect.Register("code.squareroundforest.org/arpio/wand/internal/tests/testlib.Foo", "Foo sums three numbers.\n\nfunc(a, b, c)") +docreflect.Register("code.squareroundforest.org/arpio/wand/internal/tests/testlib.Options", "") +docreflect.Register("code.squareroundforest.org/arpio/wand/internal/tests/testlib.Options.Duration", "") +docreflect.Register("code.squareroundforest.org/arpio/wand/internal/tests/testlib.Options.Foo", "") +docreflect.Register("code.squareroundforest.org/arpio/wand/internal/tests/testlib.Options.Time", "") +} \ No newline at end of file diff --git a/env_test.go b/env_test.go index e4497b9..bce6252 100644 --- a/env_test.go +++ b/env_test.go @@ -1,6 +1,5 @@ package wand -/* import ( "fmt" "strings" @@ -23,22 +22,80 @@ func TestEnv(t *testing.T) { return strings.Join([]string{strings.Join(m.One, ","), strings.Join(m.SecondVar, ",")}, ";") } - t.Run("none match app prefix", testExec(t, ff, "SOME_VAR=foo;SOME_OTHER=bar", "baz", "", "")) - t.Run("common environment var casing", testExec(t, ff, "FOO_ONE=bar;FOO_SECOND_VAR=baz", "foo", "", "barbaz")) - t.Run("camel casing", testExec(t, ff, "fooOne=bar;fooSecondVar=baz", "foo", "", "barbaz")) - t.Run("empty env var", testExec(t, ff, "fooOne=bar;fooSecondVar=", "foo", "", "bar")) - t.Run("multipart app name", testExec(t, ff, "fooBarOne=baz;FOO_BAR_SECOND_VAR=qux", "foo-bar", "", "bazqux")) - t.Run("invalid env var", testExec(t, ff, "fooOne=bar;fooSecondVar=baz;fooQux", "foo", "", "barbaz")) - t.Run("eq in value", testExec(t, ff, "fooOne=bar=baz", "foo", "", "bar=baz")) - t.Run("keeps original name", testExec(t, fi, "FOO_ONE=bar", "foo", "FOO_ONE", "")) - t.Run("keeps original name, last wins on conflict", testExec(t, fi, "FOO_ONE=bar;fooOne=baz", "foo", "fooOne", "")) + t.Run( + "none match app prefix", + testExec(testCase{impl: ff, env: "ONE=foo;SECOND_VAR=bar", command: "baz"}, "", ""), + ) + + t.Run( + "common environment var casing", + testExec(testCase{impl: ff, env: "FOO_ONE=bar;FOO_SECOND_VAR=baz", command: "foo"}, "", "barbaz"), + ) + + t.Run( + "camel casing", + testExec(testCase{impl: ff, env: "fooOne=bar;fooSecondVar=baz", command: "foo"}, "", "barbaz"), + ) + + t.Run( + "empty env var", + testExec(testCase{impl: ff, env: "fooOne=bar;fooSecondVar=", command: "foo"}, "", "bar"), + ) + + t.Run( + "multipart app name", + testExec(testCase{impl: ff, env: "fooBarOne=baz;FOO_BAR_SECOND_VAR=qux", command: "foo-bar"}, "", "bazqux"), + ) + + t.Run( + "invalid env var", + testExec(testCase{impl: ff, env: "fooOne=bar;fooSecondVar=baz;fooQux", command: "foo"}, "", "barbaz"), + ) + + t.Run( + "eq in value", + testExec(testCase{impl: ff, env: "fooOne=bar=baz", command: "foo"}, "", "bar=baz"), + ) + + t.Run( + "keeps original name", + testExec(testCase{impl: fi, env: "FOO_ONE=bar", command: "foo"}, "FOO_ONE", ""), + ) + + t.Run( + "keeps original name, last wins on conflict", + testExec(testCase{impl: fi, env: "FOO_ONE=bar;fooOne=baz", command: "foo"}, "fooOne", ""), + ) + t.Run("multiple values", func(t *testing.T) { - t.Run("2", testExec(t, fm, "fooOne=bar:baz", "foo", "", "bar,baz;")) - t.Run("3", testExec(t, fm, "fooOne=bar:baz:qux", "foo", "", "bar,baz,qux;")) - t.Run("with empty", testExec(t, fm, "fooOne=bar:baz::qux:", "foo", "", "bar,baz,,qux,;")) - t.Run("escape", testExec(t, fm, "fooOne=bar\\:baz", "foo", "", "bar:baz;")) - t.Run("escape char", testExec(t, fm, "fooOne=bar\\\\:baz", "foo", "", "bar\\,baz;")) - t.Run("escape char last", testExec(t, fm, "fooOne=bar\\", "foo", "", "bar;")) + t.Run( + "2", + testExec(testCase{impl: fm, env: "fooOne=bar:baz", command: "foo"}, "", "bar,baz;"), + ) + + t.Run( + "3", + testExec(testCase{impl: fm, env: "fooOne=bar:baz:qux", command: "foo"}, "", "bar,baz,qux;"), + ) + + t.Run( + "with empty", + testExec(testCase{impl: fm, env: "fooOne=bar:baz::qux:", command: "foo"}, "", "bar,baz,,qux,;"), + ) + + t.Run( + "escape", + testExec(testCase{impl: fm, env: "fooOne=bar\\:baz", command: "foo"}, "", "bar:baz;"), + ) + + t.Run( + "escape char", + testExec(testCase{impl: fm, env: "fooOne=bar\\\\:baz", command: "foo"}, "", "bar\\,baz;"), + ) + + t.Run( + "escape char last", + testExec(testCase{impl: fm, env: "fooOne=bar\\", command: "foo"}, "", "bar;"), + ) }) } -*/ diff --git a/exec.go b/exec.go index 56159ad..5584794 100644 --- a/exec.go +++ b/exec.go @@ -37,9 +37,10 @@ func exec(stdin io.Reader, stdout, stderr io.Writer, exit func(int), cmd Cmd, co return } - e := readEnv(cmd.name, env) + // will need root command for the config and the env: + rootCmd := cmd cmd, fullCmd, args := selectCommand(cmd, args[1:]) - if cmd.isHelp { + if cmd.helpFor != nil { if err := showHelp(stdout, cmd, conf, fullCmd); err != nil { fmt.Fprintln(stderr, err) exit(1) @@ -79,13 +80,14 @@ func exec(stdin io.Reader, stdout, stderr io.Writer, exit func(int), cmd Cmd, co return } - c, err := readConfig(cmd, cl, conf) + c, err := readConfig(rootCmd, cl, conf) if err != nil { fmt.Fprintf(stderr, "configuration error: %v", err) exit(1) return } + e := readEnv(rootCmd.name, env) if err := validateInput(cmd, conf, c, e, cl); err != nil { fmt.Fprintln(stderr, err) suggestHelp(stderr, cmd, fullCmd) diff --git a/exec_test.go b/exec_test.go index 0386526..0044526 100644 --- a/exec_test.go +++ b/exec_test.go @@ -9,11 +9,13 @@ import ( ) type testCase struct { - impl any - stdin string - conf string - env string - command string + impl any + stdin string + conf string + mergeConf []string + env string + command string + contains bool } func testExec(test testCase, err string, expect ...string) func(*testing.T) { @@ -31,7 +33,22 @@ func testExec(test testCase, err string, expect ...string) func(*testing.T) { cmd := wrap(test.impl) e := strings.Split(test.env, ";") a := strings.Split(test.command, " ") - exec(stdinr, stdout, stderr, exit, cmd, Config{test: test.conf}, e, a) + + var conf Config + if test.conf != "" && len(test.mergeConf) > 0 { + t.Fatal("test error: conflicting test config") + } else if test.conf != "" { + conf = Config{test: test.conf} + } else if len(test.mergeConf) > 0 { + var c []Config + for _, cs := range test.mergeConf { + c = append(c, Config{test: cs}) + } + + conf = MergeConfig(c...) + } + + exec(stdinr, stdout, stderr, exit, cmd, conf, e, a) if exitCode != 0 && err == "" { t.Fatal("non-zero exit code:", stderr.String()) } @@ -58,8 +75,24 @@ func testExec(test testCase, err string, expect ...string) func(*testing.T) { output = output + "\n" } - if output != strings.Join(expstr, "\n")+"\n" { - t.Fatal("unexpected output:", stdout.String()) + checkOutput := func() bool { + return output == strings.Join(expstr, "\n")+"\n" + } + + if test.contains { + checkOutput = func() bool { + for _, e := range expstr { + if !strings.Contains(output, e) { + return false + } + } + + return true + } + } + + if !checkOutput() { + t.Fatal("unexpected output:", output) } } } diff --git a/go.mod b/go.mod index 57a8455..50c922a 100644 --- a/go.mod +++ b/go.mod @@ -3,8 +3,8 @@ module code.squareroundforest.org/arpio/wand go 1.25.0 require ( - code.squareroundforest.org/arpio/bind v0.0.0-20250903223305-8683d8ba4074 - code.squareroundforest.org/arpio/docreflect v0.0.0-20250831183400-d26ecc663a30 + code.squareroundforest.org/arpio/bind v0.0.0-20250903234821-f3e17035cd36 + code.squareroundforest.org/arpio/docreflect v0.0.0-20250904132730-afd27063724e code.squareroundforest.org/arpio/notation v0.0.0-20250826181910-5140794b16b2 code.squareroundforest.org/arpio/treerack v0.0.0-20250820014405-1d956dcc6610 github.com/iancoleman/strcase v0.3.0 diff --git a/go.sum b/go.sum index 6d506cf..07509ef 100644 --- a/go.sum +++ b/go.sum @@ -2,8 +2,12 @@ code.squareroundforest.org/arpio/bind v0.0.0-20250901011104-bcadfd8b71fc h1:nu5Y code.squareroundforest.org/arpio/bind v0.0.0-20250901011104-bcadfd8b71fc/go.mod h1:tTCmCwFABKNm3PO0Dclsp4zWhNQFTfg9+uSrgoarZFI= code.squareroundforest.org/arpio/bind v0.0.0-20250903223305-8683d8ba4074 h1:OTzn0dMou+6m2rw70g7fIylQLHUTu75noAX3lbCYMqw= code.squareroundforest.org/arpio/bind v0.0.0-20250903223305-8683d8ba4074/go.mod h1:tTCmCwFABKNm3PO0Dclsp4zWhNQFTfg9+uSrgoarZFI= +code.squareroundforest.org/arpio/bind v0.0.0-20250903234821-f3e17035cd36 h1:8TB3ABJVV0eEdnWl+dJ3Hg4lGe+BlgNPgcW5p9yZnrQ= +code.squareroundforest.org/arpio/bind v0.0.0-20250903234821-f3e17035cd36/go.mod h1:tTCmCwFABKNm3PO0Dclsp4zWhNQFTfg9+uSrgoarZFI= code.squareroundforest.org/arpio/docreflect v0.0.0-20250831183400-d26ecc663a30 h1:QUCgxUEA5/ng7GwRnzb/WezmFQXSHXl48GdLJc0KC5k= code.squareroundforest.org/arpio/docreflect v0.0.0-20250831183400-d26ecc663a30/go.mod h1:/3xQI36oJG8qLBxT2fSS61P5/+i1T64fTX9GHRh8XhA= +code.squareroundforest.org/arpio/docreflect v0.0.0-20250904132730-afd27063724e h1:f7wtGAmuTYH/VTn92sBTtKhs463q+DTtW2yKgst2kl8= +code.squareroundforest.org/arpio/docreflect v0.0.0-20250904132730-afd27063724e/go.mod h1:/3xQI36oJG8qLBxT2fSS61P5/+i1T64fTX9GHRh8XhA= code.squareroundforest.org/arpio/notation v0.0.0-20250826181910-5140794b16b2 h1:S4mjQHL70CuzFg1AGkr0o0d+4M+ZWM0sbnlYq6f0b3I= code.squareroundforest.org/arpio/notation v0.0.0-20250826181910-5140794b16b2/go.mod h1:ait4Fvg9o0+bq5hlxi9dAcPL5a+/sr33qsZPNpToMLY= code.squareroundforest.org/arpio/treerack v0.0.0-20250820014405-1d956dcc6610 h1:I0jebdyQQfqJcwq2lT/TkUPBU8secHa5xZ+VzOdYVsw= diff --git a/help.go b/help.go index 7c87639..8829b85 100644 --- a/help.go +++ b/help.go @@ -68,25 +68,27 @@ type ( } ) -func help(sf []string) Cmd { +func help(cmd Cmd) Cmd { return Cmd{ name: "help", - isHelp: true, - shortForms: sf, + helpFor: &cmd, + shortForms: cmd.shortForms, } } func insertHelp(cmd Cmd) Cmd { var hasHelpCmd bool for i, sc := range cmd.subcommands { - cmd.subcommands[i] = insertHelp(sc) - if cmd.name == "help" { + if sc.name == "help" { hasHelpCmd = true + continue } + + cmd.subcommands[i] = insertHelp(sc) } if !hasHelpCmd && cmd.version == "" { - cmd.subcommands = append(cmd.subcommands, help(cmd.shortForms)) + cmd.subcommands = append(cmd.subcommands, help(cmd)) } return cmd @@ -94,7 +96,7 @@ func insertHelp(cmd Cmd) Cmd { func hasHelpSubcommand(cmd Cmd) bool { for _, sc := range cmd.subcommands { - if sc.isHelp { + if sc.helpFor != nil { return true } } @@ -312,7 +314,7 @@ func constructDoc(cmd Cmd, conf Config, fullCommand []string) doc { description: constructDescription(cmd), hasImplementation: cmd.impl != nil, isDefault: cmd.isDefault, - isHelp: cmd.isHelp, + isHelp: cmd.helpFor != nil, isVersion: cmd.version != "", hasHelpSubcommand: hasHelpSubcommand(cmd), hasHelpOption: !hasCustomHelpOption(cmd), @@ -327,6 +329,10 @@ func constructDoc(cmd Cmd, conf Config, fullCommand []string) doc { } func showHelp(out io.Writer, cmd Cmd, conf Config, fullCommand []string) error { + if cmd.helpFor != nil { + cmd = *cmd.helpFor + } + doc := constructDoc(cmd, conf, fullCommand) return formatHelp(out, doc) } diff --git a/ini.treerack b/ini.treerack index ef65b5d..d3f9e0d 100644 --- a/ini.treerack +++ b/ini.treerack @@ -1,10 +1,8 @@ -whitespace:ws = [ \b\f\r\t\v]; -comment-line:alias = "#" [^\n]*; -comment = comment-line ("\n" comment-line)*; -quoted:alias:nows = "\"" ([^\\"] | "\\" .)* "\""; -word:alias:nows = [a-zA-Z_]([a-zA-Z_0-9\-] | "\\" .)*; -key = word | quoted; -value-chars:alias:nows = ([^\\"\n=# \b\f\r\t\v] | "\\" .)+; -value = value-chars+ | quoted; -key-val = (comment "\n")? key ("=" value?)? comment-line?; -doc:root = (key-val | comment-line | "\n")*; +whitespace:ws = [ \b\f\r\t\v]; +comment-line:alias = "#" [^\n]*; +key:nows = [a-zA-Z_][a-zA-Z_0-9\-]*; +quoted:nows = "\"" ([^\\"] | "\\" .)* "\""; +value-chars:nows = ([^\\"\n=# \b\f\r\t\v] | "\\" .)*; +value = value-chars | quoted; +key-val = key ("=" value)? comment-line?; +doc:root = "\n"* (key-val | comment-line) ("\n"+ (key-val | comment-line))* "\n"*; diff --git a/iniparser.gen.go b/iniparser.gen.go index 7023607..7777863 100644 --- a/iniparser.gen.go +++ b/iniparser.gen.go @@ -1,3 +1,4 @@ + /* This file was generated with treerack (https://code.squareroundforest.org/arpio/treerack). @@ -16,29 +17,30 @@ that the user of treerack generating this file declares for it, or it is unlicensed. */ + package wand // head import ( - "bufio" - "errors" - "fmt" - "io" "strconv" + "io" "strings" "unicode" + "fmt" + "bufio" + "errors" ) type charParser struct { - name string - id int - not bool - chars []rune - ranges [][]rune + name string + id int + not bool + chars []rune + ranges [][]rune } type charBuilder struct { - name string - id int + name string + id int } func (p *charParser) nodeName() string { @@ -88,22 +90,22 @@ func (b *charBuilder) build(c *context) ([]*node, bool) { } type sequenceParser struct { - name string - id int - commit commitType - items []parser - ranges [][]int - generalizations []int - allChars bool + name string + id int + commit commitType + items []parser + ranges [][]int + generalizations []int + allChars bool } type sequenceBuilder struct { - name string - id int - commit commitType - items []builder - ranges [][]int - generalizations []int - allChars bool + name string + id int + commit commitType + items []builder + ranges [][]int + generalizations []int + allChars bool } func (p *sequenceParser) nodeName() string { @@ -124,8 +126,8 @@ func (p *sequenceParser) parse(c *context) { c.results.markPending(c.offset, p.id) } var ( - currentCount int - parsed bool + currentCount int + parsed bool ) itemIndex := 0 from := c.offset @@ -227,9 +229,9 @@ func (b *sequenceBuilder) build(c *context) ([]*node, bool) { } } var ( - itemIndex int - currentCount int - nodes []*node + itemIndex int + currentCount int + nodes []*node ) for itemIndex < len(b.items) { itemFrom := c.offset @@ -269,18 +271,18 @@ func (b *sequenceBuilder) build(c *context) ([]*node, bool) { } type choiceParser struct { - name string - id int - commit commitType - options []parser - generalizations []int + name string + id int + commit commitType + options []parser + generalizations []int } type choiceBuilder struct { - name string - id int - commit commitType - options []builder - generalizations []int + name string + id int + commit commitType + options []builder + generalizations []int } func (p *choiceParser) nodeName() string { @@ -302,10 +304,10 @@ func (p *choiceParser) parse(c *context) { } c.results.markPending(c.offset, p.id) var ( - match bool - optionIndex int - foundMatch bool - failingParser parser + match bool + optionIndex int + foundMatch bool + failingParser parser ) from := c.offset to := c.offset @@ -454,9 +456,9 @@ func (s *idSet) has(id int) bool { } type results struct { - noMatch []*idSet - match [][]int - isPending [][]int + noMatch []*idSet + match [][]int + isPending [][]int } func ensureOffsetInts(ints [][]int, offset int) [][]int { @@ -595,19 +597,19 @@ func (r *results) unmarkPending(offset, id int) { } type context struct { - reader io.RuneReader - keywords []parser - offset int - readOffset int - consumed int - offsetLimit int - failOffset int - failingParser parser - readErr error - eof bool - results *results - tokens []rune - matchLast bool + reader io.RuneReader + keywords []parser + offset int + readOffset int + consumed int + offsetLimit int + failOffset int + failingParser parser + readErr error + eof bool + results *results + tokens []rune + matchLast bool } func newContext(r io.RuneReader, keywords []parser) *context { @@ -729,10 +731,10 @@ func (c *context) finalizeParse(root parser) error { } type node struct { - Name string - Nodes []*node - From, To int - tokens []rune + Name string + Nodes []*node + From, To int + tokens []rune } func (n *node) Tokens() []rune { @@ -748,8 +750,8 @@ func (n *node) Text() string { type commitType int const ( - none commitType = 0 - alias commitType = 1 << iota + none commitType = 0 + alias commitType = 1 << iota whitespace noWhitespace keyword @@ -762,17 +764,17 @@ const ( type formatFlags int const ( - formatNone formatFlags = 0 - formatPretty formatFlags = 1 << iota + formatNone formatFlags = 0 + formatPretty formatFlags = 1 << iota formatIncludeComments ) type parseError struct { - Input string - Offset int - Line int - Column int - Definition string + Input string + Offset int + Line int + Column int + Definition string } type parser interface { nodeName() string @@ -813,238 +815,9 @@ func parseInput(r io.Reader, p parser, b builder, kw []parser) (*node, error) { func parse(r io.Reader) (*node, error) { - var p67 = sequenceParser{id: 67, commit: 128, ranges: [][]int{{0, -1}, {1, 1}, {0, -1}}} - var p65 = choiceParser{id: 65, commit: 2} - var p64 = sequenceParser{id: 64, commit: 262, name: "whitespace", allChars: true, ranges: [][]int{{1, 1}, {1, 1}}, generalizations: []int{65}} - var p1 = charParser{id: 1, chars: []rune{32, 8, 12, 13, 9, 11}} - p64.items = []parser{&p1} - p65.options = []parser{&p64} - var p66 = sequenceParser{id: 66, commit: 258, name: "doc:wsroot", ranges: [][]int{{0, 1}}} - var p63 = sequenceParser{id: 63, commit: 2, ranges: [][]int{{1, 1}, {0, -1}}} - var p61 = choiceParser{id: 61, commit: 2} - var p58 = sequenceParser{id: 58, commit: 256, name: "key-val", ranges: [][]int{{0, 1}, {0, -1}, {1, 1}, {0, -1}, {0, 1}, {0, -1}, {0, 1}}, generalizations: []int{61}} - var p54 = sequenceParser{id: 54, commit: 2, ranges: [][]int{{1, 1}, {0, -1}, {1, 1}}} - var p14 = sequenceParser{id: 14, commit: 256, name: "comment", ranges: [][]int{{1, 1}, {0, 1}}} - var p8 = sequenceParser{id: 8, commit: 258, name: "comment-line", ranges: [][]int{{1, 1}, {0, 1}}, generalizations: []int{61}} - var p3 = sequenceParser{id: 3, commit: 10, allChars: true, ranges: [][]int{{1, 1}, {1, 1}}} - var p2 = charParser{id: 2, chars: []rune{35}} - p3.items = []parser{&p2} - var p7 = sequenceParser{id: 7, commit: 2, ranges: [][]int{{0, -1}, {1, 1}, {0, -1}}} - var p5 = sequenceParser{id: 5, commit: 2, allChars: true, ranges: [][]int{{1, 1}}} - var p4 = charParser{id: 4, not: true, chars: []rune{10}} - p5.items = []parser{&p4} - var p6 = sequenceParser{id: 6, commit: 2, ranges: [][]int{{0, -1}, {1, 1}}} - p6.items = []parser{&p65, &p5} - p7.items = []parser{&p65, &p5, &p6} - p8.items = []parser{&p3, &p7} - var p13 = sequenceParser{id: 13, commit: 2, ranges: [][]int{{0, -1}, {1, 1}, {0, -1}}} - var p11 = sequenceParser{id: 11, commit: 2, ranges: [][]int{{1, 1}, {0, -1}, {1, 1}}} - var p10 = sequenceParser{id: 10, commit: 10, allChars: true, ranges: [][]int{{1, 1}, {1, 1}}} - var p9 = charParser{id: 9, chars: []rune{10}} - p10.items = []parser{&p9} - p11.items = []parser{&p10, &p65, &p8} - var p12 = sequenceParser{id: 12, commit: 2, ranges: [][]int{{0, -1}, {1, 1}}} - p12.items = []parser{&p65, &p11} - p13.items = []parser{&p65, &p11, &p12} - p14.items = []parser{&p8, &p13} - var p53 = sequenceParser{id: 53, commit: 10, allChars: true, ranges: [][]int{{1, 1}, {1, 1}}} - var p52 = charParser{id: 52, chars: []rune{10}} - p53.items = []parser{&p52} - p54.items = []parser{&p14, &p65, &p53} - var p39 = choiceParser{id: 39, commit: 256, name: "key"} - var p38 = sequenceParser{id: 38, commit: 266, name: "word", ranges: [][]int{{1, 1}, {0, -1}, {1, 1}, {0, -1}}, generalizations: []int{39}} - var p29 = sequenceParser{id: 29, commit: 10, allChars: true, ranges: [][]int{{1, 1}, {1, 1}}} - var p28 = charParser{id: 28, chars: []rune{95}, ranges: [][]rune{{97, 122}, {65, 90}}} - p29.items = []parser{&p28} - var p37 = choiceParser{id: 37, commit: 10} - var p31 = sequenceParser{id: 31, commit: 10, allChars: true, ranges: [][]int{{1, 1}, {1, 1}}, generalizations: []int{37}} - var p30 = charParser{id: 30, chars: []rune{95, 45}, ranges: [][]rune{{97, 122}, {65, 90}, {48, 57}}} - p31.items = []parser{&p30} - var p36 = sequenceParser{id: 36, commit: 10, ranges: [][]int{{1, 1}, {1, 1}, {1, 1}, {1, 1}}, generalizations: []int{37}} - var p33 = sequenceParser{id: 33, commit: 10, allChars: true, ranges: [][]int{{1, 1}, {1, 1}}} - var p32 = charParser{id: 32, chars: []rune{92}} - p33.items = []parser{&p32} - var p35 = sequenceParser{id: 35, commit: 10, allChars: true, ranges: [][]int{{1, 1}, {1, 1}}} - var p34 = charParser{id: 34, not: true} - p35.items = []parser{&p34} - p36.items = []parser{&p33, &p35} - p37.options = []parser{&p31, &p36} - p38.items = []parser{&p29, &p37} - var p27 = sequenceParser{id: 27, commit: 266, name: "quoted", ranges: [][]int{{1, 1}, {0, -1}, {1, 1}, {1, 1}, {0, -1}, {1, 1}}, generalizations: []int{39, 51}} - var p16 = sequenceParser{id: 16, commit: 10, allChars: true, ranges: [][]int{{1, 1}, {1, 1}}} - var p15 = charParser{id: 15, chars: []rune{34}} - p16.items = []parser{&p15} - var p24 = choiceParser{id: 24, commit: 10} - var p18 = sequenceParser{id: 18, commit: 10, allChars: true, ranges: [][]int{{1, 1}, {1, 1}}, generalizations: []int{24}} - var p17 = charParser{id: 17, not: true, chars: []rune{92, 34}} - p18.items = []parser{&p17} - var p23 = sequenceParser{id: 23, commit: 10, ranges: [][]int{{1, 1}, {1, 1}, {1, 1}, {1, 1}}, generalizations: []int{24}} - var p20 = sequenceParser{id: 20, commit: 10, allChars: true, ranges: [][]int{{1, 1}, {1, 1}}} - var p19 = charParser{id: 19, chars: []rune{92}} - p20.items = []parser{&p19} - var p22 = sequenceParser{id: 22, commit: 10, allChars: true, ranges: [][]int{{1, 1}, {1, 1}}} - var p21 = charParser{id: 21, not: true} - p22.items = []parser{&p21} - p23.items = []parser{&p20, &p22} - p24.options = []parser{&p18, &p23} - var p26 = sequenceParser{id: 26, commit: 10, allChars: true, ranges: [][]int{{1, 1}, {1, 1}}} - var p25 = charParser{id: 25, chars: []rune{34}} - p26.items = []parser{&p25} - p27.items = []parser{&p16, &p24, &p26} - p39.options = []parser{&p38, &p27} - var p57 = sequenceParser{id: 57, commit: 2, ranges: [][]int{{1, 1}, {0, -1}, {0, 1}}} - var p56 = sequenceParser{id: 56, commit: 10, allChars: true, ranges: [][]int{{1, 1}, {1, 1}}} - var p55 = charParser{id: 55, chars: []rune{61}} - p56.items = []parser{&p55} - var p51 = choiceParser{id: 51, commit: 256, name: "value"} - var p50 = sequenceParser{id: 50, commit: 2, ranges: [][]int{{1, 1}, {0, -1}}, generalizations: []int{51}} - var p48 = sequenceParser{id: 48, commit: 266, name: "value-chars", ranges: [][]int{{1, -1}, {1, -1}}} - var p47 = choiceParser{id: 47, commit: 10} - var p41 = sequenceParser{id: 41, commit: 10, allChars: true, ranges: [][]int{{1, 1}, {1, 1}}, generalizations: []int{47}} - var p40 = charParser{id: 40, not: true, chars: []rune{92, 34, 10, 61, 35, 32, 8, 12, 13, 9, 11}} - p41.items = []parser{&p40} - var p46 = sequenceParser{id: 46, commit: 10, ranges: [][]int{{1, 1}, {1, 1}, {1, 1}, {1, 1}}, generalizations: []int{47}} - var p43 = sequenceParser{id: 43, commit: 10, allChars: true, ranges: [][]int{{1, 1}, {1, 1}}} - var p42 = charParser{id: 42, chars: []rune{92}} - p43.items = []parser{&p42} - var p45 = sequenceParser{id: 45, commit: 10, allChars: true, ranges: [][]int{{1, 1}, {1, 1}}} - var p44 = charParser{id: 44, not: true} - p45.items = []parser{&p44} - p46.items = []parser{&p43, &p45} - p47.options = []parser{&p41, &p46} - p48.items = []parser{&p47} - var p49 = sequenceParser{id: 49, commit: 2, ranges: [][]int{{0, -1}, {1, 1}}} - p49.items = []parser{&p65, &p48} - p50.items = []parser{&p48, &p49} - p51.options = []parser{&p50, &p27} - p57.items = []parser{&p56, &p65, &p51} - p58.items = []parser{&p54, &p65, &p39, &p65, &p57, &p65, &p8} - var p60 = sequenceParser{id: 60, commit: 10, allChars: true, ranges: [][]int{{1, 1}, {1, 1}}, generalizations: []int{61}} - var p59 = charParser{id: 59, chars: []rune{10}} - p60.items = []parser{&p59} - p61.options = []parser{&p58, &p8, &p60} - var p62 = sequenceParser{id: 62, commit: 2, ranges: [][]int{{0, -1}, {1, 1}}} - p62.items = []parser{&p65, &p61} - p63.items = []parser{&p61, &p62} - p66.items = []parser{&p63} - p67.items = []parser{&p65, &p66, &p65} - var b67 = sequenceBuilder{id: 67, commit: 128, name: "doc", ranges: [][]int{{0, -1}, {1, 1}, {0, -1}}} - var b65 = choiceBuilder{id: 65, commit: 2} - var b64 = sequenceBuilder{id: 64, commit: 262, allChars: true, ranges: [][]int{{1, 1}, {1, 1}}, generalizations: []int{65}} - var b1 = charBuilder{} - b64.items = []builder{&b1} - b65.options = []builder{&b64} - var b66 = sequenceBuilder{id: 66, commit: 258, ranges: [][]int{{0, 1}}} - var b63 = sequenceBuilder{id: 63, commit: 2, ranges: [][]int{{1, 1}, {0, -1}}} - var b61 = choiceBuilder{id: 61, commit: 2} - var b58 = sequenceBuilder{id: 58, commit: 256, name: "key-val", ranges: [][]int{{0, 1}, {0, -1}, {1, 1}, {0, -1}, {0, 1}, {0, -1}, {0, 1}}, generalizations: []int{61}} - var b54 = sequenceBuilder{id: 54, commit: 2, ranges: [][]int{{1, 1}, {0, -1}, {1, 1}}} - var b14 = sequenceBuilder{id: 14, commit: 256, name: "comment", ranges: [][]int{{1, 1}, {0, 1}}} - var b8 = sequenceBuilder{id: 8, commit: 258, ranges: [][]int{{1, 1}, {0, 1}}, generalizations: []int{61}} - var b3 = sequenceBuilder{id: 3, commit: 10, allChars: true, ranges: [][]int{{1, 1}, {1, 1}}} - var b2 = charBuilder{} - b3.items = []builder{&b2} - var b7 = sequenceBuilder{id: 7, commit: 2, ranges: [][]int{{0, -1}, {1, 1}, {0, -1}}} - var b5 = sequenceBuilder{id: 5, commit: 2, allChars: true, ranges: [][]int{{1, 1}}} - var b4 = charBuilder{} - b5.items = []builder{&b4} - var b6 = sequenceBuilder{id: 6, commit: 2, ranges: [][]int{{0, -1}, {1, 1}}} - b6.items = []builder{&b65, &b5} - b7.items = []builder{&b65, &b5, &b6} - b8.items = []builder{&b3, &b7} - var b13 = sequenceBuilder{id: 13, commit: 2, ranges: [][]int{{0, -1}, {1, 1}, {0, -1}}} - var b11 = sequenceBuilder{id: 11, commit: 2, ranges: [][]int{{1, 1}, {0, -1}, {1, 1}}} - var b10 = sequenceBuilder{id: 10, commit: 10, allChars: true, ranges: [][]int{{1, 1}, {1, 1}}} - var b9 = charBuilder{} - b10.items = []builder{&b9} - b11.items = []builder{&b10, &b65, &b8} - var b12 = sequenceBuilder{id: 12, commit: 2, ranges: [][]int{{0, -1}, {1, 1}}} - b12.items = []builder{&b65, &b11} - b13.items = []builder{&b65, &b11, &b12} - b14.items = []builder{&b8, &b13} - var b53 = sequenceBuilder{id: 53, commit: 10, allChars: true, ranges: [][]int{{1, 1}, {1, 1}}} - var b52 = charBuilder{} - b53.items = []builder{&b52} - b54.items = []builder{&b14, &b65, &b53} - var b39 = choiceBuilder{id: 39, commit: 256, name: "key"} - var b38 = sequenceBuilder{id: 38, commit: 266, ranges: [][]int{{1, 1}, {0, -1}, {1, 1}, {0, -1}}, generalizations: []int{39}} - var b29 = sequenceBuilder{id: 29, commit: 10, allChars: true, ranges: [][]int{{1, 1}, {1, 1}}} - var b28 = charBuilder{} - b29.items = []builder{&b28} - var b37 = choiceBuilder{id: 37, commit: 10} - var b31 = sequenceBuilder{id: 31, commit: 10, allChars: true, ranges: [][]int{{1, 1}, {1, 1}}, generalizations: []int{37}} - var b30 = charBuilder{} - b31.items = []builder{&b30} - var b36 = sequenceBuilder{id: 36, commit: 10, ranges: [][]int{{1, 1}, {1, 1}, {1, 1}, {1, 1}}, generalizations: []int{37}} - var b33 = sequenceBuilder{id: 33, commit: 10, allChars: true, ranges: [][]int{{1, 1}, {1, 1}}} - var b32 = charBuilder{} - b33.items = []builder{&b32} - var b35 = sequenceBuilder{id: 35, commit: 10, allChars: true, ranges: [][]int{{1, 1}, {1, 1}}} - var b34 = charBuilder{} - b35.items = []builder{&b34} - b36.items = []builder{&b33, &b35} - b37.options = []builder{&b31, &b36} - b38.items = []builder{&b29, &b37} - var b27 = sequenceBuilder{id: 27, commit: 266, ranges: [][]int{{1, 1}, {0, -1}, {1, 1}, {1, 1}, {0, -1}, {1, 1}}, generalizations: []int{39, 51}} - var b16 = sequenceBuilder{id: 16, commit: 10, allChars: true, ranges: [][]int{{1, 1}, {1, 1}}} - var b15 = charBuilder{} - b16.items = []builder{&b15} - var b24 = choiceBuilder{id: 24, commit: 10} - var b18 = sequenceBuilder{id: 18, commit: 10, allChars: true, ranges: [][]int{{1, 1}, {1, 1}}, generalizations: []int{24}} - var b17 = charBuilder{} - b18.items = []builder{&b17} - var b23 = sequenceBuilder{id: 23, commit: 10, ranges: [][]int{{1, 1}, {1, 1}, {1, 1}, {1, 1}}, generalizations: []int{24}} - var b20 = sequenceBuilder{id: 20, commit: 10, allChars: true, ranges: [][]int{{1, 1}, {1, 1}}} - var b19 = charBuilder{} - b20.items = []builder{&b19} - var b22 = sequenceBuilder{id: 22, commit: 10, allChars: true, ranges: [][]int{{1, 1}, {1, 1}}} - var b21 = charBuilder{} - b22.items = []builder{&b21} - b23.items = []builder{&b20, &b22} - b24.options = []builder{&b18, &b23} - var b26 = sequenceBuilder{id: 26, commit: 10, allChars: true, ranges: [][]int{{1, 1}, {1, 1}}} - var b25 = charBuilder{} - b26.items = []builder{&b25} - b27.items = []builder{&b16, &b24, &b26} - b39.options = []builder{&b38, &b27} - var b57 = sequenceBuilder{id: 57, commit: 2, ranges: [][]int{{1, 1}, {0, -1}, {0, 1}}} - var b56 = sequenceBuilder{id: 56, commit: 10, allChars: true, ranges: [][]int{{1, 1}, {1, 1}}} - var b55 = charBuilder{} - b56.items = []builder{&b55} - var b51 = choiceBuilder{id: 51, commit: 256, name: "value"} - var b50 = sequenceBuilder{id: 50, commit: 2, ranges: [][]int{{1, 1}, {0, -1}}, generalizations: []int{51}} - var b48 = sequenceBuilder{id: 48, commit: 266, ranges: [][]int{{1, -1}, {1, -1}}} - var b47 = choiceBuilder{id: 47, commit: 10} - var b41 = sequenceBuilder{id: 41, commit: 10, allChars: true, ranges: [][]int{{1, 1}, {1, 1}}, generalizations: []int{47}} - var b40 = charBuilder{} - b41.items = []builder{&b40} - var b46 = sequenceBuilder{id: 46, commit: 10, ranges: [][]int{{1, 1}, {1, 1}, {1, 1}, {1, 1}}, generalizations: []int{47}} - var b43 = sequenceBuilder{id: 43, commit: 10, allChars: true, ranges: [][]int{{1, 1}, {1, 1}}} - var b42 = charBuilder{} - b43.items = []builder{&b42} - var b45 = sequenceBuilder{id: 45, commit: 10, allChars: true, ranges: [][]int{{1, 1}, {1, 1}}} - var b44 = charBuilder{} - b45.items = []builder{&b44} - b46.items = []builder{&b43, &b45} - b47.options = []builder{&b41, &b46} - b48.items = []builder{&b47} - var b49 = sequenceBuilder{id: 49, commit: 2, ranges: [][]int{{0, -1}, {1, 1}}} - b49.items = []builder{&b65, &b48} - b50.items = []builder{&b48, &b49} - b51.options = []builder{&b50, &b27} - b57.items = []builder{&b56, &b65, &b51} - b58.items = []builder{&b54, &b65, &b39, &b65, &b57, &b65, &b8} - var b60 = sequenceBuilder{id: 60, commit: 10, allChars: true, ranges: [][]int{{1, 1}, {1, 1}}, generalizations: []int{61}} - var b59 = charBuilder{} - b60.items = []builder{&b59} - b61.options = []builder{&b58, &b8, &b60} - var b62 = sequenceBuilder{id: 62, commit: 2, ranges: [][]int{{0, -1}, {1, 1}}} - b62.items = []builder{&b65, &b61} - b63.items = []builder{&b61, &b62} - b66.items = []builder{&b63} - b67.items = []builder{&b65, &b66, &b65} +var p60 = sequenceParser{id: 60, commit: 128,ranges: [][]int{{0, -1},{1, 1},{0, -1},},};var p58 = choiceParser{id: 58, commit: 2,};var p57 = sequenceParser{id: 57, commit: 262,name: "whitespace",allChars: true,ranges: [][]int{{1, 1},{1, 1},},generalizations: []int{58,},};var p1 = charParser{id: 1,chars: []rune{32,8,12,13,9,11,},};p57.items = []parser{&p1,};p58.options = []parser{&p57,};var p59 = sequenceParser{id: 59, commit: 258,name: "doc:wsroot",ranges: [][]int{{0, 1},{0, -1},{1, 1},{0, 1},{0, 1},},};var p52 = sequenceParser{id: 52, commit: 2,ranges: [][]int{{1, 1},{0, -1},},};var p42 = sequenceParser{id: 42, commit: 10,allChars: true,ranges: [][]int{{1, 1},{1, 1},},};var p41 = charParser{id: 41,chars: []rune{10,},};p42.items = []parser{&p41,};var p51 = sequenceParser{id: 51, commit: 2,ranges: [][]int{{0, -1},{1, 1},},};p51.items = []parser{&p58,&p42,};p52.items = []parser{&p42,&p51,};var p43 = choiceParser{id: 43, commit: 2,};var p40 = sequenceParser{id: 40, commit: 256,name: "key-val",ranges: [][]int{{1, 1},{0, -1},{0, 1},{0, -1},{0, 1},},generalizations: []int{43,46,},};var p13 = sequenceParser{id: 13, commit: 264,name: "key",ranges: [][]int{{1, 1},{0, -1},{1, 1},{0, -1},},};var p10 = sequenceParser{id: 10, commit: 10,allChars: true,ranges: [][]int{{1, 1},{1, 1},},};var p9 = charParser{id: 9,chars: []rune{95,},ranges: [][]rune{{97, 122},{65, 90},},};p10.items = []parser{&p9,};var p12 = sequenceParser{id: 12, commit: 10,allChars: true,ranges: [][]int{{1, 1},{1, 1},},};var p11 = charParser{id: 11,chars: []rune{95,45,},ranges: [][]rune{{97, 122},{65, 90},{48, 57},},};p12.items = []parser{&p11,};p13.items = []parser{&p10,&p12,};var p39 = sequenceParser{id: 39, commit: 2,ranges: [][]int{{1, 1},{0, -1},{1, 1},},};var p38 = sequenceParser{id: 38, commit: 10,allChars: true,ranges: [][]int{{1, 1},{1, 1},},};var p37 = charParser{id: 37,chars: []rune{61,},};p38.items = []parser{&p37,};var p36 = choiceParser{id: 36, commit: 256,name: "value",};var p35 = sequenceParser{id: 35, commit: 264,name: "value-chars",ranges: [][]int{{0, -1},{0, -1},},generalizations: []int{36,},};var p34 = choiceParser{id: 34, commit: 10,};var p28 = sequenceParser{id: 28, commit: 10,allChars: true,ranges: [][]int{{1, 1},{1, 1},},generalizations: []int{34,},};var p27 = charParser{id: 27,not: true,chars: []rune{92,34,10,61,35,32,8,12,13,9,11,},};p28.items = []parser{&p27,};var p33 = sequenceParser{id: 33, commit: 10,ranges: [][]int{{1, 1},{1, 1},{1, 1},{1, 1},},generalizations: []int{34,},};var p30 = sequenceParser{id: 30, commit: 10,allChars: true,ranges: [][]int{{1, 1},{1, 1},},};var p29 = charParser{id: 29,chars: []rune{92,},};p30.items = []parser{&p29,};var p32 = sequenceParser{id: 32, commit: 10,allChars: true,ranges: [][]int{{1, 1},{1, 1},},};var p31 = charParser{id: 31,not: true,};p32.items = []parser{&p31,};p33.items = []parser{&p30,&p32,};p34.options = []parser{&p28,&p33,};p35.items = []parser{&p34,};var p26 = sequenceParser{id: 26, commit: 264,name: "quoted",ranges: [][]int{{1, 1},{0, -1},{1, 1},{1, 1},{0, -1},{1, 1},},generalizations: []int{36,},};var p15 = sequenceParser{id: 15, commit: 10,allChars: true,ranges: [][]int{{1, 1},{1, 1},},};var p14 = charParser{id: 14,chars: []rune{34,},};p15.items = []parser{&p14,};var p23 = choiceParser{id: 23, commit: 10,};var p17 = sequenceParser{id: 17, commit: 10,allChars: true,ranges: [][]int{{1, 1},{1, 1},},generalizations: []int{23,},};var p16 = charParser{id: 16,not: true,chars: []rune{92,34,},};p17.items = []parser{&p16,};var p22 = sequenceParser{id: 22, commit: 10,ranges: [][]int{{1, 1},{1, 1},{1, 1},{1, 1},},generalizations: []int{23,},};var p19 = sequenceParser{id: 19, commit: 10,allChars: true,ranges: [][]int{{1, 1},{1, 1},},};var p18 = charParser{id: 18,chars: []rune{92,},};p19.items = []parser{&p18,};var p21 = sequenceParser{id: 21, commit: 10,allChars: true,ranges: [][]int{{1, 1},{1, 1},},};var p20 = charParser{id: 20,not: true,};p21.items = []parser{&p20,};p22.items = []parser{&p19,&p21,};p23.options = []parser{&p17,&p22,};var p25 = sequenceParser{id: 25, commit: 10,allChars: true,ranges: [][]int{{1, 1},{1, 1},},};var p24 = charParser{id: 24,chars: []rune{34,},};p25.items = []parser{&p24,};p26.items = []parser{&p15,&p23,&p25,};p36.options = []parser{&p35,&p26,};p39.items = []parser{&p38,&p58,&p36,};var p8 = sequenceParser{id: 8, commit: 258,name: "comment-line",ranges: [][]int{{1, 1},{0, 1},},generalizations: []int{43,46,},};var p3 = sequenceParser{id: 3, commit: 10,allChars: true,ranges: [][]int{{1, 1},{1, 1},},};var p2 = charParser{id: 2,chars: []rune{35,},};p3.items = []parser{&p2,};var p7 = sequenceParser{id: 7, commit: 2,ranges: [][]int{{0, -1},{1, 1},{0, -1},},};var p5 = sequenceParser{id: 5, commit: 2,allChars: true,ranges: [][]int{{1, 1},},};var p4 = charParser{id: 4,not: true,chars: []rune{10,},};p5.items = []parser{&p4,};var p6 = sequenceParser{id: 6, commit: 2,ranges: [][]int{{0, -1},{1, 1},},};p6.items = []parser{&p58,&p5,};p7.items = []parser{&p58,&p5,&p6,};p8.items = []parser{&p3,&p7,};p40.items = []parser{&p13,&p58,&p39,&p58,&p8,};p43.options = []parser{&p40,&p8,};var p54 = sequenceParser{id: 54, commit: 2,ranges: [][]int{{0, -1},{1, 1},{0, -1},},};var p48 = sequenceParser{id: 48, commit: 2,ranges: [][]int{{1, 1},{0, -1},{0, -1},{1, 1},},};var p45 = sequenceParser{id: 45, commit: 10,allChars: true,ranges: [][]int{{1, 1},{1, 1},},};var p44 = charParser{id: 44,chars: []rune{10,},};p45.items = []parser{&p44,};var p47 = sequenceParser{id: 47, commit: 2,ranges: [][]int{{0, -1},{1, 1},},};p47.items = []parser{&p58,&p45,};var p46 = choiceParser{id: 46, commit: 2,};p46.options = []parser{&p40,&p8,};p48.items = []parser{&p45,&p47,&p58,&p46,};var p53 = sequenceParser{id: 53, commit: 2,ranges: [][]int{{0, -1},{1, 1},},};p53.items = []parser{&p58,&p48,};p54.items = []parser{&p58,&p48,&p53,};var p56 = sequenceParser{id: 56, commit: 2,ranges: [][]int{{0, -1},{1, 1},{0, -1},},};var p50 = sequenceParser{id: 50, commit: 10,allChars: true,ranges: [][]int{{1, 1},{1, 1},},};var p49 = charParser{id: 49,chars: []rune{10,},};p50.items = []parser{&p49,};var p55 = sequenceParser{id: 55, commit: 2,ranges: [][]int{{0, -1},{1, 1},},};p55.items = []parser{&p58,&p50,};p56.items = []parser{&p58,&p50,&p55,};p59.items = []parser{&p52,&p58,&p43,&p54,&p56,};p60.items = []parser{&p58,&p59,&p58,};var b60 = sequenceBuilder{id: 60, commit: 128,name: "doc",ranges: [][]int{{0, -1},{1, 1},{0, -1},},};var b58 = choiceBuilder{id: 58, commit: 2,};var b57 = sequenceBuilder{id: 57, commit: 262,allChars: true,ranges: [][]int{{1, 1},{1, 1},},generalizations: []int{58,},};var b1 = charBuilder{};b57.items = []builder{&b1,};b58.options = []builder{&b57,};var b59 = sequenceBuilder{id: 59, commit: 258,ranges: [][]int{{0, 1},{0, -1},{1, 1},{0, 1},{0, 1},},};var b52 = sequenceBuilder{id: 52, commit: 2,ranges: [][]int{{1, 1},{0, -1},},};var b42 = sequenceBuilder{id: 42, commit: 10,allChars: true,ranges: [][]int{{1, 1},{1, 1},},};var b41 = charBuilder{};b42.items = []builder{&b41,};var b51 = sequenceBuilder{id: 51, commit: 2,ranges: [][]int{{0, -1},{1, 1},},};b51.items = []builder{&b58,&b42,};b52.items = []builder{&b42,&b51,};var b43 = choiceBuilder{id: 43, commit: 2,};var b40 = sequenceBuilder{id: 40, commit: 256,name: "key-val",ranges: [][]int{{1, 1},{0, -1},{0, 1},{0, -1},{0, 1},},generalizations: []int{43,46,},};var b13 = sequenceBuilder{id: 13, commit: 264,name: "key",ranges: [][]int{{1, 1},{0, -1},{1, 1},{0, -1},},};var b10 = sequenceBuilder{id: 10, commit: 10,allChars: true,ranges: [][]int{{1, 1},{1, 1},},};var b9 = charBuilder{};b10.items = []builder{&b9,};var b12 = sequenceBuilder{id: 12, commit: 10,allChars: true,ranges: [][]int{{1, 1},{1, 1},},};var b11 = charBuilder{};b12.items = []builder{&b11,};b13.items = []builder{&b10,&b12,};var b39 = sequenceBuilder{id: 39, commit: 2,ranges: [][]int{{1, 1},{0, -1},{1, 1},},};var b38 = sequenceBuilder{id: 38, commit: 10,allChars: true,ranges: [][]int{{1, 1},{1, 1},},};var b37 = charBuilder{};b38.items = []builder{&b37,};var b36 = choiceBuilder{id: 36, commit: 256,name: "value",};var b35 = sequenceBuilder{id: 35, commit: 264,name: "value-chars",ranges: [][]int{{0, -1},{0, -1},},generalizations: []int{36,},};var b34 = choiceBuilder{id: 34, commit: 10,};var b28 = sequenceBuilder{id: 28, commit: 10,allChars: true,ranges: [][]int{{1, 1},{1, 1},},generalizations: []int{34,},};var b27 = charBuilder{};b28.items = []builder{&b27,};var b33 = sequenceBuilder{id: 33, commit: 10,ranges: [][]int{{1, 1},{1, 1},{1, 1},{1, 1},},generalizations: []int{34,},};var b30 = sequenceBuilder{id: 30, commit: 10,allChars: true,ranges: [][]int{{1, 1},{1, 1},},};var b29 = charBuilder{};b30.items = []builder{&b29,};var b32 = sequenceBuilder{id: 32, commit: 10,allChars: true,ranges: [][]int{{1, 1},{1, 1},},};var b31 = charBuilder{};b32.items = []builder{&b31,};b33.items = []builder{&b30,&b32,};b34.options = []builder{&b28,&b33,};b35.items = []builder{&b34,};var b26 = sequenceBuilder{id: 26, commit: 264,name: "quoted",ranges: [][]int{{1, 1},{0, -1},{1, 1},{1, 1},{0, -1},{1, 1},},generalizations: []int{36,},};var b15 = sequenceBuilder{id: 15, commit: 10,allChars: true,ranges: [][]int{{1, 1},{1, 1},},};var b14 = charBuilder{};b15.items = []builder{&b14,};var b23 = choiceBuilder{id: 23, commit: 10,};var b17 = sequenceBuilder{id: 17, commit: 10,allChars: true,ranges: [][]int{{1, 1},{1, 1},},generalizations: []int{23,},};var b16 = charBuilder{};b17.items = []builder{&b16,};var b22 = sequenceBuilder{id: 22, commit: 10,ranges: [][]int{{1, 1},{1, 1},{1, 1},{1, 1},},generalizations: []int{23,},};var b19 = sequenceBuilder{id: 19, commit: 10,allChars: true,ranges: [][]int{{1, 1},{1, 1},},};var b18 = charBuilder{};b19.items = []builder{&b18,};var b21 = sequenceBuilder{id: 21, commit: 10,allChars: true,ranges: [][]int{{1, 1},{1, 1},},};var b20 = charBuilder{};b21.items = []builder{&b20,};b22.items = []builder{&b19,&b21,};b23.options = []builder{&b17,&b22,};var b25 = sequenceBuilder{id: 25, commit: 10,allChars: true,ranges: [][]int{{1, 1},{1, 1},},};var b24 = charBuilder{};b25.items = []builder{&b24,};b26.items = []builder{&b15,&b23,&b25,};b36.options = []builder{&b35,&b26,};b39.items = []builder{&b38,&b58,&b36,};var b8 = sequenceBuilder{id: 8, commit: 258,ranges: [][]int{{1, 1},{0, 1},},generalizations: []int{43,46,},};var b3 = sequenceBuilder{id: 3, commit: 10,allChars: true,ranges: [][]int{{1, 1},{1, 1},},};var b2 = charBuilder{};b3.items = []builder{&b2,};var b7 = sequenceBuilder{id: 7, commit: 2,ranges: [][]int{{0, -1},{1, 1},{0, -1},},};var b5 = sequenceBuilder{id: 5, commit: 2,allChars: true,ranges: [][]int{{1, 1},},};var b4 = charBuilder{};b5.items = []builder{&b4,};var b6 = sequenceBuilder{id: 6, commit: 2,ranges: [][]int{{0, -1},{1, 1},},};b6.items = []builder{&b58,&b5,};b7.items = []builder{&b58,&b5,&b6,};b8.items = []builder{&b3,&b7,};b40.items = []builder{&b13,&b58,&b39,&b58,&b8,};b43.options = []builder{&b40,&b8,};var b54 = sequenceBuilder{id: 54, commit: 2,ranges: [][]int{{0, -1},{1, 1},{0, -1},},};var b48 = sequenceBuilder{id: 48, commit: 2,ranges: [][]int{{1, 1},{0, -1},{0, -1},{1, 1},},};var b45 = sequenceBuilder{id: 45, commit: 10,allChars: true,ranges: [][]int{{1, 1},{1, 1},},};var b44 = charBuilder{};b45.items = []builder{&b44,};var b47 = sequenceBuilder{id: 47, commit: 2,ranges: [][]int{{0, -1},{1, 1},},};b47.items = []builder{&b58,&b45,};var b46 = choiceBuilder{id: 46, commit: 2,};b46.options = []builder{&b40,&b8,};b48.items = []builder{&b45,&b47,&b58,&b46,};var b53 = sequenceBuilder{id: 53, commit: 2,ranges: [][]int{{0, -1},{1, 1},},};b53.items = []builder{&b58,&b48,};b54.items = []builder{&b58,&b48,&b53,};var b56 = sequenceBuilder{id: 56, commit: 2,ranges: [][]int{{0, -1},{1, 1},{0, -1},},};var b50 = sequenceBuilder{id: 50, commit: 10,allChars: true,ranges: [][]int{{1, 1},{1, 1},},};var b49 = charBuilder{};b50.items = []builder{&b49,};var b55 = sequenceBuilder{id: 55, commit: 2,ranges: [][]int{{0, -1},{1, 1},},};b55.items = []builder{&b58,&b50,};b56.items = []builder{&b58,&b50,&b55,};b59.items = []builder{&b52,&b58,&b43,&b54,&b56,};b60.items = []builder{&b58,&b59,&b58,}; - var keywords = []parser{} +var keywords = []parser{} - return parseInput(r, &p67, &b67, keywords) +return parseInput(r, &p60, &b60, keywords) } diff --git a/input.go b/input.go index 78299cd..e81becd 100644 --- a/input.go +++ b/input.go @@ -51,27 +51,35 @@ func validateEnv(cmd Cmd, e env) error { } func validateOptions(cmd Cmd, o []option, conf Config) error { - ml := make(map[string]string) ms := make(map[string]string) + ml := make(map[string]string) for i := 0; i < len(cmd.shortForms); i += 2 { - l, s := cmd.shortForms[i], cmd.shortForms[i+1] - ml[l] = s + s, l := cmd.shortForms[i], cmd.shortForms[i+1] ms[s] = l + ml[l] = s } + var names []string mo := make(map[string][]option) for _, oi := range o { n := oi.name - if ln, ok := ms[n]; ok && oi.shortForm { + if oi.shortForm { + ln, ok := ms[n] + if !ok { + return fmt.Errorf("option not supported: -%s", n) + } + n = ln } + names = append(names, n) mo[n] = append(mo[n], oi) } hasConfigOption := hasConfigFromOption(conf) mf := mapFields(cmd.impl) - for n, os := range mo { + for _, n := range names { + os := mo[n] en := "--" + n if sn, ok := ml[n]; ok { en += ", -" + sn diff --git a/internal/tests/config.ini b/internal/tests/config.ini deleted file mode 100644 index 2d5dc5a..0000000 --- a/internal/tests/config.ini +++ /dev/null @@ -1,8 +0,0 @@ -# test config - -# another comment -foo_bar_baz = 42 # a comment -foo -foo_bar = bar - -foo_bar = baz diff --git a/internal/tests/testlib/lib.go b/internal/tests/testlib/lib.go new file mode 100644 index 0000000..23f2086 --- /dev/null +++ b/internal/tests/testlib/lib.go @@ -0,0 +1,25 @@ +package testlib + +import ( + "io" + "time" +) + +type Options struct { + Foo int + Duration time.Duration + Time time.Time +} + +// Foo sums three numbers. +func Foo(a, b, c int) int { + return a + b + c +} + +func Bar(out io.Writer, a, b, c int) int { + return a + b + c +} + +func Baz(o Options) int { + return o.Foo +} diff --git a/lib.go b/lib.go index 5b7f08d..5ca6157 100644 --- a/lib.go +++ b/lib.go @@ -8,6 +8,7 @@ import ( "path" ) +// the parsing errors might be tricky type Config struct { file func(Cmd) *file merge []Config @@ -25,11 +26,11 @@ type Cmd struct { minPositional int maxPositional int shortForms []string - isHelp bool + helpFor *Cmd version string } -// name needs to be valid symbol. The application name should also be a valid symbol, +// name needs to be a valid symbol. The application name should also be a valid symbol, // though not mandatory. If it is not, the environment variables may not work properly. func Command(name string, impl any, subcmds ...Cmd) Cmd { return Cmd{name: name, impl: impl, subcommands: subcmds} @@ -90,6 +91,13 @@ func OptionalConfig(conf Config) Config { return conf } +func ConfigFile(name string) Config { + return Config{ + file: func(Cmd) *file { return fileReader(name) }, + } +} + +// the config will consider the command name as in the arguments func Etc() Config { return OptionalConfig(Config{ file: func(cmd Cmd) *file { @@ -98,6 +106,7 @@ func Etc() Config { }) } +// the config will consider the command name as in the arguments func UserConfig() Config { return OptionalConfig( MergeConfig( @@ -123,6 +132,7 @@ func SystemConfig() Config { return MergeConfig(Etc(), UserConfig(), ConfigFromOption()) } +// the env will consider the command name as in the arguments func Exec(impl any, conf ...Config) { exec(os.Stdin, os.Stdout, os.Stderr, os.Exit, wrap(impl), MergeConfig(conf...), os.Environ(), os.Args) } diff --git a/reflect.go b/reflect.go index 8d3efdc..28fdbf0 100644 --- a/reflect.go +++ b/reflect.go @@ -49,45 +49,6 @@ func or[T any](p ...func(T) bool) func(T) bool { } } -func circularChecked(visited map[reflect.Type]bool, t reflect.Type) bool { - if t == nil { - return false - } - - if visited[t] { - return true - } - - if visited == nil { - visited = make(map[reflect.Type]bool) - } - - visited[t] = true - switch t.Kind() { - case reflect.Pointer, reflect.Slice: - return circularChecked(visited, t.Elem()) - case reflect.Struct: - for i := 0; i < t.NumField(); i++ { - svisited := make(map[reflect.Type]bool) - for t := range visited { - svisited[t] = true - } - - if circularChecked(svisited, t.Field(i).Type) { - return true - } - } - - return false - default: - return false - } -} - -func circular(t reflect.Type) bool { - return circularChecked(nil, t) -} - func unpackType(t reflect.Type) reflect.Type { if t == nil { return nil @@ -237,10 +198,7 @@ func mapFields(f any) map[string][]bind.Field { } func boolFields(f []bind.Field) []bind.Field { - return filter( - fields(f), - func(f bind.Field) bool { return f.Type() == bind.Bool }, - ) + return filter(f, func(f bind.Field) bool { return f.Type() == bind.Bool }) } func positional(f any) ([]reflect.Type, bool) { @@ -282,93 +240,28 @@ func ioParameters(f any) ([]reflect.Type, []reflect.Type) { } func bindable(t reflect.Type) bool { - if t == nil { - return false - } - - if circular(t) { - return false - } - - t = unpackType(t) - if isTime(t) { - return true - } - - if isStruct(t) { - return true - } - - switch t.Kind() { - case reflect.Bool, - reflect.Int, - reflect.Int8, - reflect.Int16, - reflect.Int32, - reflect.Int64, - reflect.Uint, - reflect.Uint8, - reflect.Uint16, - reflect.Uint32, - reflect.Uint64, - reflect.Float32, - reflect.Float64, - reflect.String: - return true - case reflect.Interface: - return t.NumMethod() == 0 - case reflect.Slice: - return bindable(t.Elem()) - default: - return false - } + return bind.BindableType(t) } func scalarTypeString(t bind.FieldType) string { - r := reflect.TypeOf(t) - p := strings.Split(r.Name(), ".") - n := p[len(p)-1] - n = strings.ToLower(n) - return n + switch t { + case bind.Duration: + return "duration" + case bind.Time: + return "time" + default: + return strings.ToLower(reflect.Kind(t).String()) + } } func canScan(t bind.FieldType, v any) bool { - switch t { - case bind.Any: - return true - case bind.Bool: - _, ok := bind.CreateAndBindScalar[bool](v) - return ok - case bind.Int, bind.Int8, bind.Int16, bind.Int32, bind.Int64: - _, ok := bind.CreateAndBindScalar[int](v) - return ok - case bind.Uint, bind.Uint8, bind.Uint16, bind.Uint32, bind.Uint64: - _, ok := bind.CreateAndBindScalar[uint](v) - return ok - case bind.Float32, bind.Float64: - _, ok := bind.CreateAndBindScalar[float64](v) - return ok - case bind.String: - _, ok := bind.CreateAndBindScalar[string](v) - return ok - case bind.Duration: - _, ok := bind.CreateAndBindScalar[time.Duration](v) - return ok - case bind.Time: - _, ok := bind.CreateAndBindScalar[time.Time](v) - return ok - default: - return false - } + _, ok := bind.CreateAndBindScalarFieldType(t, v) + return ok } func canScanType(t reflect.Type, v any) bool { - if t == nil { - return false - } - - r := allocate(reflect.PointerTo(t)) - return bind.BindScalar(r.Interface(), v) + _, ok := bind.CreateAndBindScalarFor(t, v) + return ok } func allocate(t reflect.Type) reflect.Value { diff --git a/reflect_test.go b/reflect_test.go index 163611a..74916f1 100644 --- a/reflect_test.go +++ b/reflect_test.go @@ -2,8 +2,10 @@ package wand import ( "bytes" + "code.squareroundforest.org/arpio/wand/internal/tests/testlib" "io" "testing" + "time" ) func TestReflect(t *testing.T) { @@ -12,7 +14,10 @@ func TestReflect(t *testing.T) { t.Run("slice", testExec(testCase{impl: f, command: "foo 42"}, "", "42")) ps := func(a *[]*[]int) int { return (*((*a)[0]))[0] } t.Run("pointer and slice", testExec(testCase{impl: ps, command: "foo 42"}, "", "42")) - type s struct{Foo int; Bar *s} + type s struct { + Foo int + Bar *s + } c := func(v s) int { return v.Foo } t.Run("circular type", testExec(testCase{impl: c, command: "foo --foo 42"}, "unsupported parameter type", "")) fp := func(a int) int { return a } @@ -89,14 +94,52 @@ func TestReflect(t *testing.T) { }) t.Run("compatible types", func(t *testing.T) { - type s0 struct{FooBar int; Foo struct {Bar string}} - type s1 struct{FooBar int; Foo struct {Bar int}} + type s0 struct { + FooBar int + Foo struct{ Bar string } + } + type s1 struct { + FooBar int + Foo struct{ Bar int } + } f := func(a s0) int { return a.FooBar + len(a.Foo.Bar) } g := func(a s1) int { return a.FooBar + a.Foo.Bar } t.Run("incompatible", testExec(testCase{impl: f, command: "foo --foo-bar 42"}, "duplicate fields with different types", "")) t.Run("compatible", testExec(testCase{impl: g, command: "foo --foo-bar 42"}, "", "84")) - type s2 struct{FooBar any; Foo struct {Bar int}} + type s2 struct { + FooBar any + Foo struct{ Bar int } + } h := func(a s2) int { return len(a.FooBar.(string)) + a.Foo.Bar } t.Run("any interface", testExec(testCase{impl: h, command: "foo --foo-bar 42"}, "", "44")) }) + + t.Run("bind failure", func(t *testing.T) { + f := func(s struct{ Foo time.Duration }) time.Duration { return s.Foo } + t.Run("no parse", testExec(testCase{impl: f, conf: "bar=baz", command: "foo"}, "", "0s")) + }) + + t.Run("indices of positional parameters", func(t *testing.T) { + t.Run( + "show function params", + testExec(testCase{impl: testlib.Foo, command: "foo help", contains: true}, "", "foo help"), + ) + + t.Run( + "skip non-positional params", + testExec(testCase{impl: testlib.Bar, command: "bar help", contains: true}, "", "bar help"), + ) + }) + + t.Run("scalar type string", func(t *testing.T) { + t.Run( + "show help with options", + testExec( + testCase{impl: testlib.Baz, command: "baz help", contains: true}, + "", + "baz help", + "--foo int", + ), + ) + }) }