This commit is contained in:
Arpad Ryszka 2025-08-26 14:12:18 +02:00
parent 7e5e7e97f8
commit 3ff038c9d3
10 changed files with 54 additions and 46 deletions

View File

@ -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)
}

View File

@ -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)

View File

@ -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)")

View File

@ -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"
}

View File

@ -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,

View File

@ -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

View File

@ -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

View File

@ -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", ""))
})

View File

@ -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)
}

38
wand.go
View File

@ -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 {