diff --git a/cmd/wand/main.go b/cmd/wand/main.go index 9f2f217..7ef9f47 100644 --- a/cmd/wand/main.go +++ b/cmd/wand/main.go @@ -12,8 +12,9 @@ func main() { man := Command("manpages", tools.Man) md := Command("markdown", tools.Markdown) exec := Command("exec", tools.Exec) - wand := Command("wand", nil, docreflect, man, md, Default(exec)) + wand := Group("wand", docreflect, man, md, Default(exec)) wand = Version(wand, version) + wand = ShortForm(wand, "f", "no-cache", "c", "clear-cache", "d", "cache-dir") conf := MergeConfig(Etc(), UserConfig()) Exec(wand, conf) } diff --git a/command.go b/command.go index 4b169bf..4bab4d6 100644 --- a/command.go +++ b/command.go @@ -10,14 +10,6 @@ import ( var commandNameExpression = regexp.MustCompile("^[a-zA-Z_][a-zA-Z_0-9]*$") -func command(name string, impl any, subcmds ...Cmd) Cmd { - return Cmd{ - name: name, - impl: impl, - subcommands: subcmds, - } -} - func wrap(impl any) Cmd { cmd, ok := impl.(Cmd) if ok { @@ -219,6 +211,10 @@ func validateCommandTree(cmd Cmd, conf Config, assignedShortForms map[string]str return fmt.Errorf("command name is not a valid symbol: '%s'", cmd.name) } + if cmd.impl == nil && !cmd.group { + return fmt.Errorf("command does not have an implementation: %s", cmd.name) + } + if cmd.impl != nil { if err := validateImpl(cmd, conf); err != nil { return fmt.Errorf("%s: %w", cmd.name, err) diff --git a/docreflect.gen.go b/docreflect.gen.go index 22e08ab..6f2f085 100644 --- a/docreflect.gen.go +++ b/docreflect.gen.go @@ -46,8 +46,8 @@ docreflect.Register("code.squareroundforest.org/arpio/wand/tools.Docreflect", "\ docreflect.Register("code.squareroundforest.org/arpio/wand/tools.Exec", "\nfunc(o, function, args)") docreflect.Register("code.squareroundforest.org/arpio/wand/tools.ExecOptions", "") docreflect.Register("code.squareroundforest.org/arpio/wand/tools.ExecOptions.CacheDir", "") +docreflect.Register("code.squareroundforest.org/arpio/wand/tools.ExecOptions.ClearCache", "") docreflect.Register("code.squareroundforest.org/arpio/wand/tools.ExecOptions.NoCache", "") -docreflect.Register("code.squareroundforest.org/arpio/wand/tools.ExecOptions.PurgeCache", "") docreflect.Register("code.squareroundforest.org/arpio/wand/tools.Man", "\nfunc(out, commandDir)") docreflect.Register("code.squareroundforest.org/arpio/wand/tools.Markdown", "\nfunc(out, commandDir)") docreflect.Register("code.squareroundforest.org/arpio/wand/tools.copyGomod", "\nfunc(mn, dst, src)") diff --git a/exec_test.go b/exec_test.go index 1ffe2bb..0386526 100644 --- a/exec_test.go +++ b/exec_test.go @@ -9,10 +9,10 @@ import ( ) type testCase struct { - impl any - stdin string - conf string - env string + impl any + stdin string + conf string + env string command string } @@ -54,7 +54,7 @@ func testExec(test testCase, err string, expect ...string) func(*testing.T) { } output := stdout.String() - if output[len(output) - 1] != '\n' { + if output[len(output)-1] != '\n' { output = output + "\n" } diff --git a/help.go b/help.go index 2afac35..862ad8a 100644 --- a/help.go +++ b/help.go @@ -14,7 +14,7 @@ type ( argumentSet struct { count int names []string - types []string + types []string variadic bool usesStdin bool usesStdout bool @@ -31,7 +31,7 @@ type ( docOption struct { name string - typ string + typ string description string shortNames []string isBool bool @@ -189,7 +189,7 @@ func constructArguments(cmd Cmd) argumentSet { return argumentSet{ count: count, names: names, - types: types, + types: types, variadic: t.IsVariadic(), usesStdin: len(ior) > 0, usesStdout: len(iow) > 0, @@ -245,7 +245,7 @@ func constructOptions(cmd Cmd, hasConfigFromOption bool) []docOption { for name, fi := range f { opt := docOption{ name: name, - typ: strings.ToLower(fmt.Sprint(fi[0].typ.Kind())), + typ: strings.ToLower(fmt.Sprint(fi[0].typ.Kind())), description: d[name], shortNames: sf[name], isBool: fi[0].typ.Kind() == reflect.Bool, diff --git a/notes.txt b/notes.txt index 4302a6b..744016e 100644 --- a/notes.txt +++ b/notes.txt @@ -1,10 +1,19 @@ speed up wand exec with smarter caching -test starting from the most referenced file to the least referenced one -run only the related test when testing a file fix notation: .build/wand net/http.Get https://squareroundforest.org | less fix wand exec: .build/wand -- 'func(a struct{Foo string}) string { return a.Foo }' --foo bar allow in wand exec: .build/wand 'regexp.MustCompile("a").MatchString' aaa +the exec command could have an --import option for the packages, for cases when it's not possible to infer considering that the possible input comes from limited sources, it may make sense to not escape just every dot in markdown +fix short form: program error: exec: exec: short form shadowing field name: no-cache +fix error reporting: program error: exec: exec: short form shadowing field name: no-cache +header file for the docreflect generated code +switch to wand docreflect for the docreflect generated code +rename wandgenerate env var to _wandgenerate +support discard in the same config file +trim input strings +improve validation messages +test starting from the most referenced file to the least referenced one +run only the related test when testing a file reflect command diff --git a/reflect.go b/reflect.go index 0fb8103..4201035 100644 --- a/reflect.go +++ b/reflect.go @@ -358,7 +358,7 @@ func acceptsMultiple(t reflect.Type) bool { } switch t.Kind() { - case reflect.Pointer, reflect.Slice: + case reflect.Pointer: return acceptsMultiple(t.Elem()) default: return false diff --git a/reflect_test.go b/reflect_test.go index 1307262..febbef3 100644 --- a/reflect_test.go +++ b/reflect_test.go @@ -1,9 +1,9 @@ package wand import ( - "testing" - "io" "bytes" + "io" + "testing" ) func TestReflect(t *testing.T) { @@ -28,7 +28,7 @@ func TestReflect(t *testing.T) { }) t.Run("struct param", func(t *testing.T) { - f := func(s struct{Bar int}) int { return s.Bar } + f := func(s struct{ Bar int }) int { return s.Bar } t.Run("basic", testExec(testCase{impl: f, command: "foo --bar 42"}, "", "42")) }) @@ -76,7 +76,7 @@ func TestReflect(t *testing.T) { t.Run("interface", func(t *testing.T) { f := func(a any) any { return a } t.Run("any", testExec(testCase{impl: f, command: "foo bar"}, "", "bar")) - type i interface{Foo()} + type i interface{ Foo() } g := func(a i) any { return a } t.Run("unscannable", testExec(testCase{impl: g, command: "foo bar"}, "non-empty interface", "")) }) diff --git a/tools/tools.go b/tools/tools.go index 95557c9..dbfd6e3 100644 --- a/tools/tools.go +++ b/tools/tools.go @@ -16,7 +16,7 @@ import ( type ExecOptions struct { NoCache bool - PurgeCache bool + ClearCache bool CacheDir string } @@ -202,7 +202,7 @@ func Exec(o ExecOptions, function string, args ...string) error { functionDir = path.Join(cacheDir, "tmp", functionHash) } - if o.NoCache || o.PurgeCache { + if o.NoCache || o.ClearCache { if err := os.RemoveAll(functionDir); err != nil { return fmt.Errorf("failed to clean cache: %w", err) } diff --git a/wand.go b/wand.go index 2335a3c..37ee5c3 100644 --- a/wand.go +++ b/wand.go @@ -17,12 +17,12 @@ type Config struct { type Cmd struct { name string impl any + group bool subcommands []Cmd isDefault bool minPositional int maxPositional int shortForms []string - description string isHelp bool version string } @@ -30,7 +30,11 @@ type Cmd struct { // name needs to be 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 command(name, impl, subcmds...) + return Cmd{name: name, impl: impl, subcommands: subcmds} +} + +func Group(name string, subcmds ...Cmd) Cmd { + return Cmd{name: name, group: true, subcommands: subcmds} } func Default(cmd Cmd) Cmd { @@ -44,10 +48,10 @@ func Args(cmd Cmd, min, max int) Cmd { return cmd } -func ShortFormOptions(cmd Cmd, f ...string) Cmd { +func ShortForm(cmd Cmd, f ...string) Cmd { cmd.shortForms = append(cmd.shortForms, f...) for i := range cmd.subcommands { - cmd.subcommands[i] = ShortFormOptions( + cmd.subcommands[i] = ShortForm( cmd.subcommands[i], f..., ) @@ -89,22 +93,20 @@ func Etc() Config { } func UserConfig() Config { - return OptionalConfig(MergeConfig( - Config{ - file: func(cmd Cmd) *file { - return fileReader( - path.Join(os.Getenv("HOME"), fmt.Sprintf(".%s", cmd.name), "config"), - ) + return OptionalConfig( + MergeConfig( + Config{ + file: func(cmd Cmd) *file { + return fileReader(path.Join(os.Getenv("HOME"), fmt.Sprintf(".%s", cmd.name), "config")) + }, }, - }, - Config{ - file: func(cmd Cmd) *file { - return fileReader( - path.Join(os.Getenv("HOME"), ".config", cmd.name, "config"), - ) + Config{ + file: func(cmd Cmd) *file { + return fileReader(path.Join(os.Getenv("HOME"), ".config", cmd.name, "config")) + }, }, - }, - )) + ), + ) } func ConfigFromOption() Config {