apply bind
This commit is contained in:
parent
72083c1f33
commit
6d2d51d211
1
.gitignore
vendored
1
.gitignore
vendored
@ -1 +1,2 @@
|
|||||||
.build
|
.build
|
||||||
|
.cover
|
||||||
|
|||||||
2
Makefile
2
Makefile
@ -29,7 +29,6 @@ iniparser.gen.go: ini.treerack
|
|||||||
docreflect.gen.go: $(SOURCES)
|
docreflect.gen.go: $(SOURCES)
|
||||||
go run script/docreflect/docs.go \
|
go run script/docreflect/docs.go \
|
||||||
wand \
|
wand \
|
||||||
code.squareroundforest.org/arpio/docreflect/generate \
|
|
||||||
code.squareroundforest.org/arpio/wand/tools \
|
code.squareroundforest.org/arpio/wand/tools \
|
||||||
> docreflect.gen.go \
|
> docreflect.gen.go \
|
||||||
|| rm -f docreflect.gen.go
|
|| rm -f docreflect.gen.go
|
||||||
@ -44,6 +43,7 @@ install: .build/wand
|
|||||||
cp .build/wand ~/bin
|
cp .build/wand ~/bin
|
||||||
|
|
||||||
clean:
|
clean:
|
||||||
|
go clean ./...
|
||||||
rm -rf .build
|
rm -rf .build
|
||||||
rm -f docreflect.gen.go
|
rm -f docreflect.gen.go
|
||||||
rm -f iniparser.gen.go
|
rm -f iniparser.gen.go
|
||||||
|
|||||||
193
apply.go
193
apply.go
@ -1,188 +1,61 @@
|
|||||||
package wand
|
package wand
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/iancoleman/strcase"
|
|
||||||
"io"
|
"io"
|
||||||
"reflect"
|
"reflect"
|
||||||
"strings"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func ensurePointerAllocation(p reflect.Value, n int) {
|
func bindKeyVals(receiver reflect.Value, keyVals map[string][]string) bool {
|
||||||
if p.IsNil() {
|
v := make(map[string][]any)
|
||||||
p.Set(reflect.New(p.Type().Elem()))
|
for name, values := range keyVals {
|
||||||
}
|
for _, vi := range values {
|
||||||
|
v[name] = append(v[name], vi)
|
||||||
ensureAllocation(p.Elem(), n)
|
|
||||||
}
|
|
||||||
|
|
||||||
func ensureSliceAllocation(s reflect.Value, n int) {
|
|
||||||
if s.Len() < n {
|
|
||||||
a := reflect.MakeSlice(s.Type(), n-s.Len(), n-s.Len())
|
|
||||||
a = reflect.AppendSlice(s, a)
|
|
||||||
s.Set(a)
|
|
||||||
}
|
|
||||||
|
|
||||||
if s.Len() > n {
|
|
||||||
a := s.Slice(0, n)
|
|
||||||
s.Set(a)
|
|
||||||
}
|
|
||||||
|
|
||||||
for i := 0; i < s.Len(); i++ {
|
|
||||||
ensureAllocation(s.Index(i), 1)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func ensureAllocation(v reflect.Value, n int) {
|
u := bindFields(receiver, v)
|
||||||
switch v.Type().Kind() {
|
return len(v) > 0 && len(u) < len(v)
|
||||||
case reflect.Pointer:
|
|
||||||
ensurePointerAllocation(v, n)
|
|
||||||
case reflect.Slice:
|
|
||||||
ensureSliceAllocation(v, n)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func setPointerValue(p reflect.Value, v []value) {
|
func bindOptions(receiver reflect.Value, shortForms []string, o []option) bool {
|
||||||
setFieldValue(p.Elem(), v)
|
ms := make(map[string]string)
|
||||||
|
for i := 0; i < len(shortForms); i += 2 {
|
||||||
|
ms[shortForms[i]] = shortForms[i + 1]
|
||||||
}
|
}
|
||||||
|
|
||||||
func setSliceValue(s reflect.Value, v []value) {
|
v := make(map[string][]any)
|
||||||
for i := 0; i < s.Len(); i++ {
|
for _, oi := range o {
|
||||||
setFieldValue(s.Index(i), v[i:i+1])
|
n := oi.name
|
||||||
}
|
if oi.shortForm {
|
||||||
|
n = ms[n]
|
||||||
}
|
}
|
||||||
|
|
||||||
func setValue(f reflect.Value, v value) {
|
var val any
|
||||||
if v.isBool {
|
if oi.value.isBool {
|
||||||
f.Set(reflect.ValueOf(v.boolean))
|
val = oi.value.boolean
|
||||||
return
|
} else {
|
||||||
|
val = oi.value.str
|
||||||
}
|
}
|
||||||
|
|
||||||
f.Set(reflect.ValueOf(scan(f.Type(), v.str)))
|
v[n] = append(v[n], val)
|
||||||
}
|
}
|
||||||
|
|
||||||
func setFieldValue(field reflect.Value, v []value) {
|
u := bindFields(receiver, v)
|
||||||
switch field.Kind() {
|
return len(v) > 0 && len(u) < len(v)
|
||||||
case reflect.Pointer:
|
|
||||||
setPointerValue(field, v)
|
|
||||||
case reflect.Slice:
|
|
||||||
setSliceValue(field, v)
|
|
||||||
default:
|
|
||||||
setValue(field, v[0])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func setField(s reflect.Value, name string, v []value) {
|
|
||||||
for i := 0; i < s.Type().NumField(); i++ {
|
|
||||||
fs := s.Type().Field(i)
|
|
||||||
fname := strcase.ToKebab(fs.Name)
|
|
||||||
ft := fs.Type
|
|
||||||
ftup := unpack(ft)
|
|
||||||
fv := s.Field(i)
|
|
||||||
switch {
|
|
||||||
case !fs.Anonymous && fname == name:
|
|
||||||
ensureAllocation(fv, len(v))
|
|
||||||
setFieldValue(fv, v)
|
|
||||||
case !fs.Anonymous && ftup.Kind() == reflect.Struct:
|
|
||||||
prefix := fname + "-"
|
|
||||||
if strings.HasPrefix(name, prefix) {
|
|
||||||
ensureAllocation(fv, len(v))
|
|
||||||
setField(unpack(fv), name[len(prefix):], v)
|
|
||||||
}
|
|
||||||
case fs.Anonymous:
|
|
||||||
ensureAllocation(fv, 1)
|
|
||||||
setField(unpack(fv), name, v)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func createStructArg(t reflect.Type, shortForms []string, c config, e env, o []option) (reflect.Value, bool) {
|
func createStructArg(t reflect.Type, shortForms []string, c config, e env, o []option) (reflect.Value, bool) {
|
||||||
tup := unpack(t)
|
r := allocate(t)
|
||||||
f := fields(tup)
|
hasConfigMatches := bindKeyVals(r, c.values)
|
||||||
fn := make(map[string]bool)
|
hasEnvMatches := bindKeyVals(r, e.values)
|
||||||
for _, fi := range f {
|
hasOptionMatches := bindOptions(r, shortForms, o)
|
||||||
fn[fi.name] = true
|
return r, hasConfigMatches || hasEnvMatches || hasOptionMatches
|
||||||
}
|
|
||||||
|
|
||||||
ms := make(map[string]string)
|
|
||||||
for i := 0; i < len(shortForms); i += 2 {
|
|
||||||
l, s := shortForms[i], shortForms[i+1]
|
|
||||||
ms[s] = l
|
|
||||||
}
|
|
||||||
|
|
||||||
om := make(map[string][]option)
|
|
||||||
for _, oi := range o {
|
|
||||||
n := oi.name
|
|
||||||
if l, ok := ms[n]; ok && oi.shortForm {
|
|
||||||
n = l
|
|
||||||
}
|
|
||||||
|
|
||||||
om[n] = append(om[n], oi)
|
|
||||||
}
|
|
||||||
|
|
||||||
var foundConfig []string
|
|
||||||
for n := range c.values {
|
|
||||||
if fn[n] {
|
|
||||||
foundConfig = append(foundConfig, n)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var foundEnv []string
|
|
||||||
for n := range e.values {
|
|
||||||
if fn[n] {
|
|
||||||
foundEnv = append(foundEnv, n)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var foundOptions []string
|
|
||||||
for n := range om {
|
|
||||||
if fn[n] {
|
|
||||||
foundOptions = append(foundOptions, n)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(foundConfig) == 0 && len(foundEnv) == 0 && len(foundOptions) == 0 {
|
|
||||||
return reflect.Zero(t), false
|
|
||||||
}
|
|
||||||
|
|
||||||
p := reflect.New(tup)
|
|
||||||
for _, n := range foundConfig {
|
|
||||||
var v []value
|
|
||||||
for _, vi := range c.values[n] {
|
|
||||||
v = append(v, stringValue(vi))
|
|
||||||
}
|
|
||||||
|
|
||||||
setField(p.Elem(), n, v)
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, n := range foundEnv {
|
|
||||||
var v []value
|
|
||||||
for _, vi := range e.values[n] {
|
|
||||||
v = append(v, stringValue(vi))
|
|
||||||
}
|
|
||||||
|
|
||||||
setField(p.Elem(), n, v)
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, n := range foundOptions {
|
|
||||||
var v []value
|
|
||||||
for _, oi := range om[n] {
|
|
||||||
v = append(v, oi.value)
|
|
||||||
}
|
|
||||||
|
|
||||||
setField(p.Elem(), n, v)
|
|
||||||
}
|
|
||||||
|
|
||||||
return pack(p.Elem(), t), true
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func createPositional(t reflect.Type, v string) reflect.Value {
|
func createPositional(t reflect.Type, v string) reflect.Value {
|
||||||
if t.Kind() == reflect.Interface {
|
r := allocate(t)
|
||||||
return reflect.ValueOf(v)
|
bindScalar(r, v)
|
||||||
}
|
return r
|
||||||
|
|
||||||
tup := unpack(t)
|
|
||||||
sv := reflect.ValueOf(scan(tup, v))
|
|
||||||
return pack(sv, t)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func createArgs(stdin io.Reader, stdout io.Writer, t reflect.Type, shortForms []string, c config, e env, cl commandLine) []reflect.Value {
|
func createArgs(stdin io.Reader, stdout io.Writer, t reflect.Type, shortForms []string, c config, e env, cl commandLine) []reflect.Value {
|
||||||
@ -246,7 +119,7 @@ func processResults(t reflect.Type, out []reflect.Value) ([]any, error) {
|
|||||||
|
|
||||||
func apply(stdin io.Reader, stdout io.Writer, cmd Cmd, c config, e env, cl commandLine) ([]any, error) {
|
func apply(stdin io.Reader, stdout io.Writer, cmd Cmd, c config, e env, cl commandLine) ([]any, error) {
|
||||||
v := reflect.ValueOf(cmd.impl)
|
v := reflect.ValueOf(cmd.impl)
|
||||||
v = unpack(v)
|
v = unpackValue(v)
|
||||||
t := v.Type()
|
t := v.Type()
|
||||||
args := createArgs(stdin, stdout, t, cmd.shortForms, c, e, cl)
|
args := createArgs(stdin, stdout, t, cmd.shortForms, c, e, cl)
|
||||||
out := v.Call(args)
|
out := v.Call(args)
|
||||||
|
|||||||
352
command.go
352
command.go
@ -5,7 +5,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"reflect"
|
"reflect"
|
||||||
"regexp"
|
"regexp"
|
||||||
"slices"
|
"code.squareroundforest.org/arpio/bind"
|
||||||
)
|
)
|
||||||
|
|
||||||
var commandNameExpression = regexp.MustCompile("^[a-zA-Z_][a-zA-Z_0-9]*$")
|
var commandNameExpression = regexp.MustCompile("^[a-zA-Z_][a-zA-Z_0-9]*$")
|
||||||
@ -19,103 +19,41 @@ func wrap(impl any) Cmd {
|
|||||||
return Command("", impl)
|
return Command("", impl)
|
||||||
}
|
}
|
||||||
|
|
||||||
func validateFields(f []field, conf Config) error {
|
func validateFields(f []bind.Field, conf Config) error {
|
||||||
hasConfigFromOption := hasConfigFromOption(conf)
|
hasConfigFromOption := hasConfigFromOption(conf)
|
||||||
mf := make(map[string]field)
|
mf := make(map[string]bind.Field)
|
||||||
for _, fi := range f {
|
for _, fi := range f {
|
||||||
if ef, ok := mf[fi.name]; ok && !compatibleTypes(fi.typ, ef.typ) {
|
if ef, ok := mf[fi.Name()]; ok && !compatibleTypes(fi.Type(), ef.Type()) {
|
||||||
return fmt.Errorf("duplicate fields with different types: %s", fi.name)
|
return fmt.Errorf("duplicate fields with different types: %s", fi.Name())
|
||||||
}
|
}
|
||||||
|
|
||||||
if hasConfigFromOption && fi.name == "config" {
|
if hasConfigFromOption && fi.Name() == "config" {
|
||||||
return errors.New("option reserved for config file shadowed by struct field")
|
return errors.New("option reserved for config file shadowed by struct field")
|
||||||
}
|
}
|
||||||
|
|
||||||
mf[fi.name] = fi
|
mf[fi.Name()] = fi
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func validateParameter(visited map[reflect.Type]bool, t reflect.Type) error {
|
func validatePositional(p []reflect.Type, variadic bool, min, max int) error {
|
||||||
switch t.Kind() {
|
fixedPositional := len(p)
|
||||||
case reflect.Bool,
|
if variadic {
|
||||||
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 nil
|
|
||||||
case reflect.Pointer,
|
|
||||||
reflect.Slice:
|
|
||||||
if visited[t] {
|
|
||||||
return fmt.Errorf("circular type definitions not supported: %s", t.Name())
|
|
||||||
}
|
|
||||||
|
|
||||||
if visited == nil {
|
|
||||||
visited = make(map[reflect.Type]bool)
|
|
||||||
}
|
|
||||||
|
|
||||||
visited[t] = true
|
|
||||||
t = unpack(t)
|
|
||||||
return validateParameter(visited, t)
|
|
||||||
case reflect.Interface:
|
|
||||||
if t.NumMethod() > 0 {
|
|
||||||
return errors.New("non-empty interface parameter")
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
default:
|
|
||||||
return fmt.Errorf("unsupported parameter type: %v", t)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func validatePositional(t reflect.Type, min, max int) error {
|
|
||||||
p := positionalParameters(t)
|
|
||||||
ior, iow := ioParameters(p)
|
|
||||||
if len(ior) > 1 || len(iow) > 1 {
|
|
||||||
return errors.New("only zero or one reader and zero or one writer parameters is supported")
|
|
||||||
}
|
|
||||||
|
|
||||||
for i, pi := range p {
|
|
||||||
if slices.Contains(ior, i) || slices.Contains(iow, i) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := validateParameter(nil, pi); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
last := t.NumIn() - 1
|
|
||||||
lastVariadic := t.IsVariadic() &&
|
|
||||||
!isStruct(t.In(last)) &&
|
|
||||||
!slices.Contains(ior, last) &&
|
|
||||||
!slices.Contains(iow, last)
|
|
||||||
fixedPositional := len(p) - len(ior) - len(iow)
|
|
||||||
if lastVariadic {
|
|
||||||
fixedPositional--
|
fixedPositional--
|
||||||
}
|
}
|
||||||
|
|
||||||
if min > 0 && min < fixedPositional {
|
if min > 0 && min < fixedPositional {
|
||||||
return fmt.Errorf(
|
return fmt.Errorf(
|
||||||
"minimum positional defined as %d but the implementation expects minimum %d fixed parameters",
|
"minimum positional arguments defined as %d but the implementation expects minimum %d fixed parameters",
|
||||||
min,
|
min,
|
||||||
fixedPositional,
|
fixedPositional,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
if min > 0 && min > fixedPositional && !lastVariadic {
|
if min > 0 && min > fixedPositional && !variadic {
|
||||||
return fmt.Errorf(
|
return fmt.Errorf(
|
||||||
"minimum positional defined as %d but the implementation has only %d fixed parameters and no variadic parameter",
|
"minimum positional arguments defined as %d but the implementation has only %d fixed parameters and no variadic parameter",
|
||||||
min,
|
min,
|
||||||
fixedPositional,
|
fixedPositional,
|
||||||
)
|
)
|
||||||
@ -123,7 +61,7 @@ func validatePositional(t reflect.Type, min, max int) error {
|
|||||||
|
|
||||||
if max > 0 && max < fixedPositional {
|
if max > 0 && max < fixedPositional {
|
||||||
return fmt.Errorf(
|
return fmt.Errorf(
|
||||||
"maximum positional defined as %d but the implementation expects minimum %d fixed parameters",
|
"maximum positional arguments defined as %d but the implementation expects minimum %d fixed parameters",
|
||||||
max,
|
max,
|
||||||
fixedPositional,
|
fixedPositional,
|
||||||
)
|
)
|
||||||
@ -131,7 +69,7 @@ func validatePositional(t reflect.Type, min, max int) error {
|
|||||||
|
|
||||||
if min > 0 && max > 0 && min > max {
|
if min > 0 && max > 0 && min > max {
|
||||||
return fmt.Errorf(
|
return fmt.Errorf(
|
||||||
"minimum positional defined as larger then the maxmimum positional: %d > %d",
|
"minimum positional arguments defined as larger then the maxmimum positional: %d > %d",
|
||||||
min,
|
min,
|
||||||
max,
|
max,
|
||||||
)
|
)
|
||||||
@ -140,65 +78,45 @@ func validatePositional(t reflect.Type, min, max int) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func validateIOParameters(ior, iow []reflect.Type) error {
|
||||||
|
if len(ior) > 1 || len(iow) > 1 {
|
||||||
|
return errors.New("only zero or one reader and zero or one writer parameter is supported")
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func validateImpl(cmd Cmd, conf Config) error {
|
func validateImpl(cmd Cmd, conf Config) error {
|
||||||
v := reflect.ValueOf(cmd.impl)
|
if !isFunc(cmd.impl) {
|
||||||
v = unpack(v)
|
return errors.New("command implementation must be a function or a pointer to a function")
|
||||||
t := v.Type()
|
|
||||||
if t.Kind() != reflect.Func {
|
|
||||||
return errors.New("command implementation not a function")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
s := structParameters(t)
|
p := parameters(cmd.impl)
|
||||||
f, err := fieldsChecked(nil, s...)
|
for _, pi := range p {
|
||||||
if err != nil {
|
if !isReader(pi) && !isWriter(pi) && !bindable(pi) {
|
||||||
return err
|
return fmt.Errorf("unsupported parameter type: %s", pi.Name())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
f := fields(cmd.impl)
|
||||||
if err := validateFields(f, conf); err != nil {
|
if err := validateFields(f, conf); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := validatePositional(t, cmd.minPositional, cmd.maxPositional); err != nil {
|
pos, variadic := positional(cmd.impl)
|
||||||
|
if err := validatePositional(pos, variadic, cmd.minPositional, cmd.maxPositional); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
ior, iow := ioParameters(cmd.impl)
|
||||||
|
if err := validateIOParameters(ior, iow); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func validateShortForms(cmd Cmd, assignedShortForms map[string]string) error {
|
func validateCommandTree(cmd Cmd, conf Config) error {
|
||||||
mf := mapFields(cmd.impl)
|
|
||||||
if len(cmd.shortForms)%2 != 0 {
|
|
||||||
return fmt.Errorf(
|
|
||||||
"undefined option short form: %s", cmd.shortForms[len(cmd.shortForms)-1],
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
for i := 0; i < len(cmd.shortForms); i += 2 {
|
|
||||||
fn := cmd.shortForms[i]
|
|
||||||
sf := cmd.shortForms[i+1]
|
|
||||||
if len(sf) != 1 && (sf[0] < 'a' || sf[0] > 'z') {
|
|
||||||
return fmt.Errorf("invalid short form: %s", sf)
|
|
||||||
}
|
|
||||||
|
|
||||||
if _, ok := mf[sf]; ok {
|
|
||||||
return fmt.Errorf("short form shadowing field name: %s", sf)
|
|
||||||
}
|
|
||||||
|
|
||||||
if _, ok := mf[fn]; !ok {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
if lf, ok := assignedShortForms[sf]; ok && lf != fn {
|
|
||||||
return fmt.Errorf("ambigous short form: %s", sf)
|
|
||||||
}
|
|
||||||
|
|
||||||
assignedShortForms[sf] = fn
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func validateCommandTree(cmd Cmd, conf Config, assignedShortForms map[string]string, root bool) error {
|
|
||||||
if cmd.isHelp {
|
if cmd.isHelp {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@ -207,35 +125,25 @@ func validateCommandTree(cmd Cmd, conf Config, assignedShortForms map[string]str
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
if !root && !commandNameExpression.MatchString(cmd.name) {
|
|
||||||
return fmt.Errorf("command name is not a valid symbol: '%s'", cmd.name)
|
|
||||||
}
|
|
||||||
|
|
||||||
if cmd.impl == nil && !cmd.group {
|
if cmd.impl == nil && !cmd.group {
|
||||||
return fmt.Errorf("command does not have an implementation: %s", cmd.name)
|
return fmt.Errorf("command does not have an implementation: %s", cmd.name)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if cmd.impl == nil && len(cmd.subcommands) == 0 {
|
||||||
|
return fmt.Errorf("empty command category: %s", cmd.name)
|
||||||
|
}
|
||||||
|
|
||||||
if cmd.impl != nil {
|
if cmd.impl != nil {
|
||||||
if err := validateImpl(cmd, conf); err != nil {
|
if err := validateImpl(cmd, conf); err != nil {
|
||||||
return fmt.Errorf("%s: %w", cmd.name, err)
|
return fmt.Errorf("%s: %w", cmd.name, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if cmd.impl == nil && len(cmd.subcommands) == 0 {
|
|
||||||
return fmt.Errorf("empty command category: %s", cmd.name)
|
|
||||||
}
|
|
||||||
|
|
||||||
if cmd.impl != nil {
|
|
||||||
if err := validateShortForms(cmd, assignedShortForms); err != nil {
|
|
||||||
return fmt.Errorf("%s: %w", cmd.name, err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var hasDefault bool
|
var hasDefault bool
|
||||||
names := make(map[string]bool)
|
names := make(map[string]bool)
|
||||||
for _, s := range cmd.subcommands {
|
for _, s := range cmd.subcommands {
|
||||||
if s.name == "" {
|
if !commandNameExpression.MatchString(s.name) {
|
||||||
return fmt.Errorf("unnamed subcommand of: %s", cmd.name)
|
return fmt.Errorf("command name is not a valid symbol: '%s'", cmd.name)
|
||||||
}
|
}
|
||||||
|
|
||||||
if names[s.name] {
|
if names[s.name] {
|
||||||
@ -243,13 +151,9 @@ func validateCommandTree(cmd Cmd, conf Config, assignedShortForms map[string]str
|
|||||||
}
|
}
|
||||||
|
|
||||||
names[s.name] = true
|
names[s.name] = true
|
||||||
if err := validateCommandTree(s, conf, assignedShortForms, false); err != nil {
|
|
||||||
return fmt.Errorf("%s: %w", s.name, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if s.isDefault && cmd.impl != nil {
|
if s.isDefault && cmd.impl != nil {
|
||||||
return fmt.Errorf(
|
return fmt.Errorf(
|
||||||
"default subcommand defined for a command with explicit implementation: %s, %s",
|
"default subcommand defined for a command that has an explicit implementation: %s, %s",
|
||||||
cmd.name,
|
cmd.name,
|
||||||
s.name,
|
s.name,
|
||||||
)
|
)
|
||||||
@ -262,36 +166,174 @@ func validateCommandTree(cmd Cmd, conf Config, assignedShortForms map[string]str
|
|||||||
if s.isDefault {
|
if s.isDefault {
|
||||||
hasDefault = true
|
hasDefault = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if err := validateCommandTree(s, conf); err != nil {
|
||||||
|
return fmt.Errorf("%s: %w", s.name, err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func allShortForms(cmd Cmd) []string {
|
func checkShortFormDefinition(existing map[string]string, short, long string) error {
|
||||||
var sf []string
|
e, ok := existing[short]
|
||||||
|
if !ok {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if e == long {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return fmt.Errorf(
|
||||||
|
"using the same short form for different options is not allowed: %s->%s, %s->%s",
|
||||||
|
short, long, short, e,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func collectMappedShortForms(to, from map[string]string) (map[string]string, error) {
|
||||||
|
for s, l := range from {
|
||||||
|
if err := checkShortFormDefinition(to, s, l); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if to == nil {
|
||||||
|
to = make(map[string]string)
|
||||||
|
}
|
||||||
|
|
||||||
|
to[s] = l
|
||||||
|
}
|
||||||
|
|
||||||
|
return to, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func validateShortFormsTree(cmd Cmd) (map[string]string, map[string]string, error) {
|
||||||
|
var mapped, unmapped map[string]string
|
||||||
for _, sc := range cmd.subcommands {
|
for _, sc := range cmd.subcommands {
|
||||||
sf = append(sf, allShortForms(sc)...)
|
m, um, err := validateShortFormsTree(sc)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if mapped, err = collectMappedShortForms(mapped, m); err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if unmapped, err = collectMappedShortForms(unmapped, um); err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(cmd.shortForms) % 2 != 0 {
|
||||||
|
return nil, nil, fmt.Errorf("unassigned short form: %s", cmd.shortForms[len(cmd.shortForms) - 1])
|
||||||
|
}
|
||||||
|
|
||||||
|
mf := mapFields(cmd.impl)
|
||||||
for i := 0; i < len(cmd.shortForms); i += 2 {
|
for i := 0; i < len(cmd.shortForms); i += 2 {
|
||||||
sf = append(sf, cmd.shortForms[i])
|
s, l := cmd.shortForms[i], cmd.shortForms[i + 1]
|
||||||
|
r := []rune(s)
|
||||||
|
if len(r) != 1 || r[0] < 'a' || r[0] > 'z' {
|
||||||
|
return nil, nil, fmt.Errorf("invalid short form: %s", s)
|
||||||
}
|
}
|
||||||
|
|
||||||
return sf
|
if err := checkShortFormDefinition(mapped, s, l); err != nil {
|
||||||
|
return nil, nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
func validateCommand(cmd Cmd, conf Config) error {
|
if err := checkShortFormDefinition(unmapped, s, l); err != nil {
|
||||||
assignedShortForms := make(map[string]string)
|
return nil, nil, err
|
||||||
if err := validateCommandTree(cmd, conf, assignedShortForms, true); err != nil {
|
}
|
||||||
|
|
||||||
|
_, hasField := mf[l]
|
||||||
|
_, isMapped := mapped[s]
|
||||||
|
if !hasField && !isMapped {
|
||||||
|
if unmapped == nil {
|
||||||
|
unmapped = make(map[string]string)
|
||||||
|
}
|
||||||
|
|
||||||
|
unmapped[s] = l
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if mapped == nil {
|
||||||
|
mapped = make(map[string]string)
|
||||||
|
}
|
||||||
|
|
||||||
|
delete(unmapped, s)
|
||||||
|
mapped[s] = l
|
||||||
|
}
|
||||||
|
|
||||||
|
return mapped, unmapped, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func validateShortForms(cmd Cmd) error {
|
||||||
|
_, um, err := validateShortFormsTree(cmd)
|
||||||
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
asf := allShortForms(cmd)
|
if len(um) != 0 {
|
||||||
for _, sf := range asf {
|
return errors.New("unmapped short forms")
|
||||||
if _, ok := assignedShortForms[sf]; !ok {
|
|
||||||
return fmt.Errorf("unassigned option short form: %s", sf)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func validateCommand(cmd Cmd, conf Config) error {
|
||||||
|
if err := validateCommandTree(cmd, conf); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := validateShortForms(cmd); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func insertHelpOption(names []string) []string {
|
||||||
|
for _, n := range names {
|
||||||
|
if n == "help" {
|
||||||
|
return names
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return append(names, "help")
|
||||||
|
}
|
||||||
|
|
||||||
|
func insertHelpShortForm(shortForms []string) []string {
|
||||||
|
for _, sf := range shortForms {
|
||||||
|
if sf == "h" {
|
||||||
|
return shortForms
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return append(shortForms, "h")
|
||||||
|
}
|
||||||
|
|
||||||
|
func boolOptions(cmd Cmd) []string {
|
||||||
|
f := fields(cmd.impl)
|
||||||
|
b := boolFields(f)
|
||||||
|
|
||||||
|
var n []string
|
||||||
|
for _, fi := range b {
|
||||||
|
n = append(n, fi.Name())
|
||||||
|
}
|
||||||
|
|
||||||
|
n = insertHelpOption(n)
|
||||||
|
sfm := make(map[string][]string)
|
||||||
|
for i := 0; i < len(cmd.shortForms); i += 2 {
|
||||||
|
s, l := cmd.shortForms[i], cmd.shortForms[i+1]
|
||||||
|
sfm[l] = append(sfm[l], s)
|
||||||
|
}
|
||||||
|
|
||||||
|
var sf []string
|
||||||
|
for _, ni := range n {
|
||||||
|
if sn, ok := sfm[ni]; ok {
|
||||||
|
sf = append(sf, sn...)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
sf = insertHelpShortForm(sf)
|
||||||
|
return append(n, sf...)
|
||||||
|
}
|
||||||
|
|||||||
@ -1,11 +1,11 @@
|
|||||||
package wand
|
package wand
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"reflect"
|
|
||||||
"slices"
|
"slices"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"unicode"
|
"unicode"
|
||||||
|
"code.squareroundforest.org/arpio/bind"
|
||||||
)
|
)
|
||||||
|
|
||||||
type value struct {
|
type value struct {
|
||||||
@ -33,57 +33,6 @@ func stringValue(s string) value {
|
|||||||
return value{str: s}
|
return value{str: s}
|
||||||
}
|
}
|
||||||
|
|
||||||
func insertHelpOption(names []string) []string {
|
|
||||||
for _, n := range names {
|
|
||||||
if n == "help" {
|
|
||||||
return names
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return append(names, "help")
|
|
||||||
}
|
|
||||||
|
|
||||||
func insertHelpShortForm(shortForms []string) []string {
|
|
||||||
for _, sf := range shortForms {
|
|
||||||
if sf == "h" {
|
|
||||||
return shortForms
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return append(shortForms, "h")
|
|
||||||
}
|
|
||||||
|
|
||||||
func boolOptions(cmd Cmd) []string {
|
|
||||||
v := reflect.ValueOf(cmd.impl)
|
|
||||||
v = unpack(v)
|
|
||||||
t := v.Type()
|
|
||||||
s := structParameters(t)
|
|
||||||
f := fields(s...)
|
|
||||||
b := boolFields(f)
|
|
||||||
|
|
||||||
var n []string
|
|
||||||
for _, fi := range b {
|
|
||||||
n = append(n, fi.name)
|
|
||||||
}
|
|
||||||
|
|
||||||
n = insertHelpOption(n)
|
|
||||||
sfm := make(map[string][]string)
|
|
||||||
for i := 0; i < len(cmd.shortForms); i += 2 {
|
|
||||||
l, s := cmd.shortForms[i], cmd.shortForms[i+1]
|
|
||||||
sfm[l] = append(sfm[l], s)
|
|
||||||
}
|
|
||||||
|
|
||||||
var sf []string
|
|
||||||
for _, ni := range n {
|
|
||||||
if sn, ok := sfm[ni]; ok {
|
|
||||||
sf = append(sf, sn...)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
sf = insertHelpShortForm(sf)
|
|
||||||
return append(n, sf...)
|
|
||||||
}
|
|
||||||
|
|
||||||
func isOption(arg string) bool {
|
func isOption(arg string) bool {
|
||||||
a := []rune(arg)
|
a := []rune(arg)
|
||||||
if len(a) <= 2 {
|
if len(a) <= 2 {
|
||||||
@ -325,15 +274,14 @@ func readArgs(boolOptions, args []string) commandLine {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func hasHelpOption(cmd Cmd, o []option) bool {
|
func hasHelpOption(cmd Cmd, o []option) bool {
|
||||||
var mf map[string][]field
|
var mf map[string][]bind.Field
|
||||||
if cmd.impl != nil {
|
if cmd.impl != nil {
|
||||||
mf = mapFields(cmd.impl)
|
mf = mapFields(cmd.impl)
|
||||||
}
|
}
|
||||||
|
|
||||||
sf := make(map[string]bool)
|
sf := make(map[string]string)
|
||||||
for i := 0; i < len(cmd.shortForms); i += 2 {
|
for i := 0; i < len(cmd.shortForms); i += 2 {
|
||||||
s := cmd.shortForms[i+1]
|
sf[cmd.shortForms[i]] = cmd.shortForms[i + 1]
|
||||||
sf[s] = true
|
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, oi := range o {
|
for _, oi := range o {
|
||||||
@ -341,14 +289,22 @@ func hasHelpOption(cmd Cmd, o []option) bool {
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
if oi.name == "help" {
|
n := oi.name
|
||||||
if _, ok := mf["help"]; !ok {
|
if oi.shortForm && n == "h" {
|
||||||
return true
|
l, ok := sf["h"]
|
||||||
}
|
if !ok {
|
||||||
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
if oi.shortForm && oi.name == "h" {
|
if l != "help" {
|
||||||
if !sf["h"] {
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
n = "help"
|
||||||
|
}
|
||||||
|
|
||||||
|
if n == "help" {
|
||||||
|
if _, ok := mf["help"]; !ok {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -111,6 +111,7 @@ func readConfigFile(cmd Cmd, conf Config) (config, error) {
|
|||||||
name := strcase.ToKebab(key)
|
name := strcase.ToKebab(key)
|
||||||
c.originalNames[name] = key
|
c.originalNames[name] = key
|
||||||
if !hasValue {
|
if !hasValue {
|
||||||
|
delete(c.values, name)
|
||||||
c.discard = append(c.discard, name)
|
c.discard = append(c.discard, name)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,62 +1,31 @@
|
|||||||
|
/*
|
||||||
|
Generated with https://code.squareroundforest.org/arpio/docreflect
|
||||||
|
*/
|
||||||
|
|
||||||
|
|
||||||
package wand
|
package wand
|
||||||
import "code.squareroundforest.org/arpio/docreflect"
|
import "code.squareroundforest.org/arpio/docreflect"
|
||||||
func init() {
|
func init() {
|
||||||
docreflect.Register("code.squareroundforest.org/arpio/docreflect/generate", "Package generate provides a generator to generate go code from go docs that registers doc entries\nfor use with the docreflect package.\n")
|
|
||||||
docreflect.Register("code.squareroundforest.org/arpio/docreflect/generate.GenerateRegistry", "GenerateRegistry generates a Go code file to the output, including a package init function that\nwill register the documentation of the declarations specified by their gopath.\n\nThe gopath argument accepts any number of package, package level symbol, of struct field paths.\nIt is recommended to use package paths unless special circumstances.\n\nSome important gotchas to keep in mind, GenerateRegistry does not resolve type references like\ntype aliases, or type definitions based on named types, and it doesn't follow import paths.\n\nfunc(w, outputPackageName, gopath)")
|
|
||||||
docreflect.Register("code.squareroundforest.org/arpio/docreflect/generate.cleanPaths", "\nfunc(gopath)")
|
|
||||||
docreflect.Register("code.squareroundforest.org/arpio/docreflect/generate.collectGoDirs", "\nfunc(o)")
|
|
||||||
docreflect.Register("code.squareroundforest.org/arpio/docreflect/generate.findFieldDocs", "\nfunc(str, fieldPath)")
|
|
||||||
docreflect.Register("code.squareroundforest.org/arpio/docreflect/generate.findGoMod", "\nfunc(dir)")
|
|
||||||
docreflect.Register("code.squareroundforest.org/arpio/docreflect/generate.fixDocPackage", "\nfunc(p)")
|
|
||||||
docreflect.Register("code.squareroundforest.org/arpio/docreflect/generate.format", "\nfunc(w, pname, docs)")
|
|
||||||
docreflect.Register("code.squareroundforest.org/arpio/docreflect/generate.funcDocs", "\nfunc(f)")
|
|
||||||
docreflect.Register("code.squareroundforest.org/arpio/docreflect/generate.funcParams", "\nfunc(f)")
|
|
||||||
docreflect.Register("code.squareroundforest.org/arpio/docreflect/generate.generate", "\nfunc(o, gopaths)")
|
|
||||||
docreflect.Register("code.squareroundforest.org/arpio/docreflect/generate.getGoroot", "\nfunc()")
|
|
||||||
docreflect.Register("code.squareroundforest.org/arpio/docreflect/generate.importPackages", "\nfunc(o, godirs, paths)")
|
|
||||||
docreflect.Register("code.squareroundforest.org/arpio/docreflect/generate.initOptions", "\nfunc()")
|
|
||||||
docreflect.Register("code.squareroundforest.org/arpio/docreflect/generate.merge", "\nfunc(m)")
|
|
||||||
docreflect.Register("code.squareroundforest.org/arpio/docreflect/generate.methodDocs", "\nfunc(importPath, t)")
|
|
||||||
docreflect.Register("code.squareroundforest.org/arpio/docreflect/generate.modCache", "\nfunc()")
|
|
||||||
docreflect.Register("code.squareroundforest.org/arpio/docreflect/generate.options", "")
|
|
||||||
docreflect.Register("code.squareroundforest.org/arpio/docreflect/generate.options.gomod", "")
|
|
||||||
docreflect.Register("code.squareroundforest.org/arpio/docreflect/generate.options.goroot", "")
|
|
||||||
docreflect.Register("code.squareroundforest.org/arpio/docreflect/generate.options.modules", "")
|
|
||||||
docreflect.Register("code.squareroundforest.org/arpio/docreflect/generate.options.wd", "")
|
|
||||||
docreflect.Register("code.squareroundforest.org/arpio/docreflect/generate.packageDocs", "\nfunc(pkg)")
|
|
||||||
docreflect.Register("code.squareroundforest.org/arpio/docreflect/generate.packageFuncDocs", "\nfunc(importPath, funcs)")
|
|
||||||
docreflect.Register("code.squareroundforest.org/arpio/docreflect/generate.packagePaths", "\nfunc(p)")
|
|
||||||
docreflect.Register("code.squareroundforest.org/arpio/docreflect/generate.parsePackages", "\nfunc(pkgs)")
|
|
||||||
docreflect.Register("code.squareroundforest.org/arpio/docreflect/generate.parserInclude", "\nfunc(pkg)")
|
|
||||||
docreflect.Register("code.squareroundforest.org/arpio/docreflect/generate.readGomod", "\nfunc(wd)")
|
|
||||||
docreflect.Register("code.squareroundforest.org/arpio/docreflect/generate.set", "\nfunc(m, key, value)")
|
|
||||||
docreflect.Register("code.squareroundforest.org/arpio/docreflect/generate.splitGopath", "\nfunc(p)")
|
|
||||||
docreflect.Register("code.squareroundforest.org/arpio/docreflect/generate.structFieldDocs", "\nfunc(t, fieldPath)")
|
|
||||||
docreflect.Register("code.squareroundforest.org/arpio/docreflect/generate.structFieldsDocs", "\nfunc(importPath, t)")
|
|
||||||
docreflect.Register("code.squareroundforest.org/arpio/docreflect/generate.symbolDocs", "\nfunc(pkg, gopath)")
|
|
||||||
docreflect.Register("code.squareroundforest.org/arpio/docreflect/generate.symbolPath", "\nfunc(packagePath, name)")
|
|
||||||
docreflect.Register("code.squareroundforest.org/arpio/docreflect/generate.takeDocs", "\nfunc(pkgs, gopaths)")
|
|
||||||
docreflect.Register("code.squareroundforest.org/arpio/docreflect/generate.takeFieldDocs", "\nfunc(packagePath, prefix, f)")
|
|
||||||
docreflect.Register("code.squareroundforest.org/arpio/docreflect/generate.typeDocs", "\nfunc(importPath, types)")
|
|
||||||
docreflect.Register("code.squareroundforest.org/arpio/docreflect/generate.typeMethodDocs", "\nfunc(t, name)")
|
|
||||||
docreflect.Register("code.squareroundforest.org/arpio/docreflect/generate.unpack", "\nfunc(e)")
|
|
||||||
docreflect.Register("code.squareroundforest.org/arpio/docreflect/generate.valueDocs", "\nfunc(packagePath, v)")
|
|
||||||
docreflect.Register("code.squareroundforest.org/arpio/wand/tools", "")
|
docreflect.Register("code.squareroundforest.org/arpio/wand/tools", "")
|
||||||
docreflect.Register("code.squareroundforest.org/arpio/wand/tools.Docreflect", "\nfunc(out, packageName, gopaths)")
|
docreflect.Register("code.squareroundforest.org/arpio/wand/tools.Docreflect", "\nfunc(out, packageName, gopaths)")
|
||||||
docreflect.Register("code.squareroundforest.org/arpio/wand/tools.Exec", "\nfunc(o, function, args)")
|
docreflect.Register("code.squareroundforest.org/arpio/wand/tools.Exec", "\nfunc(o, stdin, args)")
|
||||||
docreflect.Register("code.squareroundforest.org/arpio/wand/tools.ExecOptions", "")
|
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.CacheDir", "")
|
||||||
docreflect.Register("code.squareroundforest.org/arpio/wand/tools.ExecOptions.ClearCache", "")
|
docreflect.Register("code.squareroundforest.org/arpio/wand/tools.ExecOptions.ClearCache", "")
|
||||||
|
docreflect.Register("code.squareroundforest.org/arpio/wand/tools.ExecOptions.Import", "")
|
||||||
|
docreflect.Register("code.squareroundforest.org/arpio/wand/tools.ExecOptions.InlineImport", "")
|
||||||
docreflect.Register("code.squareroundforest.org/arpio/wand/tools.ExecOptions.NoCache", "")
|
docreflect.Register("code.squareroundforest.org/arpio/wand/tools.ExecOptions.NoCache", "")
|
||||||
docreflect.Register("code.squareroundforest.org/arpio/wand/tools.Man", "\nfunc(out, commandDir)")
|
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.Markdown", "\nfunc(out, o, commandDir)")
|
||||||
docreflect.Register("code.squareroundforest.org/arpio/wand/tools.copyGomod", "\nfunc(mn, dst, src)")
|
docreflect.Register("code.squareroundforest.org/arpio/wand/tools.MarkdownOptions", "")
|
||||||
|
docreflect.Register("code.squareroundforest.org/arpio/wand/tools.MarkdownOptions.Level", "")
|
||||||
|
docreflect.Register("code.squareroundforest.org/arpio/wand/tools.commandReader", "\nfunc(in)")
|
||||||
docreflect.Register("code.squareroundforest.org/arpio/wand/tools.execCommandDir", "\nfunc(out, commandDir, env)")
|
docreflect.Register("code.squareroundforest.org/arpio/wand/tools.execCommandDir", "\nfunc(out, commandDir, env)")
|
||||||
docreflect.Register("code.squareroundforest.org/arpio/wand/tools.execInternal", "\nfunc(command, args)")
|
docreflect.Register("code.squareroundforest.org/arpio/wand/tools.execInternal", "\nfunc(command, args)")
|
||||||
docreflect.Register("code.squareroundforest.org/arpio/wand/tools.execTransparent", "\nfunc(command, args)")
|
docreflect.Register("code.squareroundforest.org/arpio/wand/tools.execTransparent", "\nfunc(command, args)")
|
||||||
|
docreflect.Register("code.squareroundforest.org/arpio/wand/tools.execWand", "\nfunc(o, args)")
|
||||||
docreflect.Register("code.squareroundforest.org/arpio/wand/tools.execc", "\nfunc(stdin, stdout, stderr, command, args, env)")
|
docreflect.Register("code.squareroundforest.org/arpio/wand/tools.execc", "\nfunc(stdin, stdout, stderr, command, args, env)")
|
||||||
docreflect.Register("code.squareroundforest.org/arpio/wand/tools.findGomod", "\nfunc(wd)")
|
docreflect.Register("code.squareroundforest.org/arpio/wand/tools.hash", "\nfunc(expression, imports, inlineImports)")
|
||||||
docreflect.Register("code.squareroundforest.org/arpio/wand/tools.functionHash", "\nfunc(function)")
|
docreflect.Register("code.squareroundforest.org/arpio/wand/tools.printGoFile", "\nfunc(fn, expression, imports, inlineImports)")
|
||||||
docreflect.Register("code.squareroundforest.org/arpio/wand/tools.printFile", "\nfunc(fn, pkg, expression)")
|
docreflect.Register("code.squareroundforest.org/arpio/wand/tools.readExec", "\nfunc(o, stdin)")
|
||||||
docreflect.Register("code.squareroundforest.org/arpio/wand/tools.splitFunction", "\nfunc(function)")
|
|
||||||
}
|
}
|
||||||
5
docs.go
Normal file
5
docs.go
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
/*
|
||||||
|
Wand provides utilities for constructing command line applications from functions, with automatic parameter
|
||||||
|
binding from command line arguments, environment variables and configuration files.
|
||||||
|
*/
|
||||||
|
package wand
|
||||||
6
exec.go
6
exec.go
@ -18,7 +18,7 @@ func exec(stdin io.Reader, stdout, stderr io.Writer, exit func(int), cmd Cmd, co
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if os.Getenv("wandgenerate") == "man" {
|
if os.Getenv("_wandgenerate") == "man" {
|
||||||
if err := generateMan(stdout, cmd, conf); err != nil {
|
if err := generateMan(stdout, cmd, conf); err != nil {
|
||||||
fmt.Fprintln(stderr, err)
|
fmt.Fprintln(stderr, err)
|
||||||
exit(1)
|
exit(1)
|
||||||
@ -27,8 +27,8 @@ func exec(stdin io.Reader, stdout, stderr io.Writer, exit func(int), cmd Cmd, co
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if os.Getenv("wandgenerate") == "markdown" {
|
if os.Getenv("_wandgenerate") == "markdown" {
|
||||||
level, _ := strconv.Atoi(os.Getenv("wandmarkdownlevel"))
|
level, _ := strconv.Atoi(os.Getenv("_wandmarkdownlevel"))
|
||||||
if err := generateMarkdown(stdout, cmd, conf, level); err != nil {
|
if err := generateMarkdown(stdout, cmd, conf, level); err != nil {
|
||||||
fmt.Fprintln(stderr, err)
|
fmt.Fprintln(stderr, err)
|
||||||
exit(1)
|
exit(1)
|
||||||
|
|||||||
33
format.go
33
format.go
@ -70,21 +70,6 @@ func lines(s string) string {
|
|||||||
return strings.Join(pp, "\n")
|
return strings.Join(pp, "\n")
|
||||||
}
|
}
|
||||||
|
|
||||||
func escapeTeletype(s string) string {
|
|
||||||
r := []rune(s)
|
|
||||||
for i := range r {
|
|
||||||
if r[i] >= 0x00 && r[i] <= 0x1f && r[i] != '\n' && r[i] != '\t' {
|
|
||||||
r[i] = 0xb7
|
|
||||||
}
|
|
||||||
|
|
||||||
if r[i] >= 0x7f && r[i] <= 0x9f {
|
|
||||||
r[i] = 0xb7
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return string(r)
|
|
||||||
}
|
|
||||||
|
|
||||||
func manParagraphs(s string) string {
|
func manParagraphs(s string) string {
|
||||||
p := paragraphs(s)
|
p := paragraphs(s)
|
||||||
pp := strings.Split(p, "\n\n")
|
pp := strings.Split(p, "\n\n")
|
||||||
@ -105,6 +90,21 @@ func manLines(s string) string {
|
|||||||
return strings.Join(ll, "\n")
|
return strings.Join(ll, "\n")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func escapeTeletype(s string) string {
|
||||||
|
r := []rune(s)
|
||||||
|
for i := range r {
|
||||||
|
if r[i] >= 0x00 && r[i] <= 0x1f && r[i] != '\n' && r[i] != '\t' {
|
||||||
|
r[i] = 0xb7
|
||||||
|
}
|
||||||
|
|
||||||
|
if r[i] >= 0x7f && r[i] <= 0x9f {
|
||||||
|
r[i] = 0xb7
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return string(r)
|
||||||
|
}
|
||||||
|
|
||||||
func escapeRoff(s string) string {
|
func escapeRoff(s string) string {
|
||||||
var (
|
var (
|
||||||
rr []rune
|
rr []rune
|
||||||
@ -163,8 +163,9 @@ func escapeMD(s string) string {
|
|||||||
rr = append(rr, ri)
|
rr = append(rr, ri)
|
||||||
default:
|
default:
|
||||||
rr = append(rr, ri)
|
rr = append(rr, ri)
|
||||||
lastDigit = ri >= 0 && ri <= 9
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
lastDigit = ri >= 0 && ri <= 9
|
||||||
}
|
}
|
||||||
|
|
||||||
return string(rr)
|
return string(rr)
|
||||||
|
|||||||
11
go.mod
11
go.mod
@ -1,12 +1,15 @@
|
|||||||
module code.squareroundforest.org/arpio/wand
|
module code.squareroundforest.org/arpio/wand
|
||||||
|
|
||||||
go 1.24.6
|
go 1.25.0
|
||||||
|
|
||||||
require (
|
require (
|
||||||
code.squareroundforest.org/arpio/docreflect v0.0.0-20250823192303-755a103f3788
|
code.squareroundforest.org/arpio/docreflect v0.0.0-20250831183400-d26ecc663a30
|
||||||
code.squareroundforest.org/arpio/notation v0.0.0-20241225183158-af3bd591a174
|
code.squareroundforest.org/arpio/notation v0.0.0-20250826181910-5140794b16b2
|
||||||
code.squareroundforest.org/arpio/treerack v0.0.0-20250820014405-1d956dcc6610
|
code.squareroundforest.org/arpio/treerack v0.0.0-20250820014405-1d956dcc6610
|
||||||
github.com/iancoleman/strcase v0.3.0
|
github.com/iancoleman/strcase v0.3.0
|
||||||
)
|
)
|
||||||
|
|
||||||
require golang.org/x/mod v0.27.0 // indirect
|
require (
|
||||||
|
code.squareroundforest.org/arpio/bind v0.0.0-20250831235903-9a6db08a25d0 // indirect
|
||||||
|
golang.org/x/mod v0.27.0 // indirect
|
||||||
|
)
|
||||||
|
|||||||
12
go.sum
12
go.sum
@ -1,7 +1,19 @@
|
|||||||
|
code.squareroundforest.org/arpio/bind v0.0.0-20250831151900-af0bbca22e99 h1:1p3wtLY/USO+niU9d7yNqk/sbRluZ3/Xoo7L7gEF+ew=
|
||||||
|
code.squareroundforest.org/arpio/bind v0.0.0-20250831151900-af0bbca22e99/go.mod h1:tTCmCwFABKNm3PO0Dclsp4zWhNQFTfg9+uSrgoarZFI=
|
||||||
|
code.squareroundforest.org/arpio/bind v0.0.0-20250831235903-9a6db08a25d0 h1:dpekVQNpmH39MDNig+hA2IFF6J7TXPQNc8hTuENAn7A=
|
||||||
|
code.squareroundforest.org/arpio/bind v0.0.0-20250831235903-9a6db08a25d0/go.mod h1:tTCmCwFABKNm3PO0Dclsp4zWhNQFTfg9+uSrgoarZFI=
|
||||||
code.squareroundforest.org/arpio/docreflect v0.0.0-20250823192303-755a103f3788 h1:jJoq0FdasFFDX1uJowXD8iyX/2G3gjwxtVEDyXtfeuw=
|
code.squareroundforest.org/arpio/docreflect v0.0.0-20250823192303-755a103f3788 h1:jJoq0FdasFFDX1uJowXD8iyX/2G3gjwxtVEDyXtfeuw=
|
||||||
code.squareroundforest.org/arpio/docreflect v0.0.0-20250823192303-755a103f3788/go.mod h1:/3xQI36oJG8qLBxT2fSS61P5/+i1T64fTX9GHRh8XhA=
|
code.squareroundforest.org/arpio/docreflect v0.0.0-20250823192303-755a103f3788/go.mod h1:/3xQI36oJG8qLBxT2fSS61P5/+i1T64fTX9GHRh8XhA=
|
||||||
|
code.squareroundforest.org/arpio/docreflect v0.0.0-20250826190210-b092c9cb4c2e h1:FJ9rP44KGmiDZb+gGRH0ty209R05x2vR3wNPUG3HB4I=
|
||||||
|
code.squareroundforest.org/arpio/docreflect v0.0.0-20250826190210-b092c9cb4c2e/go.mod h1:/3xQI36oJG8qLBxT2fSS61P5/+i1T64fTX9GHRh8XhA=
|
||||||
|
code.squareroundforest.org/arpio/docreflect v0.0.0-20250826190339-b00034d8ca42 h1:w9JPDwsnPvDC70helP9RNL2lnRj+ab2eVgv9fK56kIg=
|
||||||
|
code.squareroundforest.org/arpio/docreflect v0.0.0-20250826190339-b00034d8ca42/go.mod h1:/3xQI36oJG8qLBxT2fSS61P5/+i1T64fTX9GHRh8XhA=
|
||||||
|
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/notation v0.0.0-20241225183158-af3bd591a174 h1:DKMSagVY3uyRhJ4ohiwQzNnR6CWdVKLkg97A8eQGxQU=
|
code.squareroundforest.org/arpio/notation v0.0.0-20241225183158-af3bd591a174 h1:DKMSagVY3uyRhJ4ohiwQzNnR6CWdVKLkg97A8eQGxQU=
|
||||||
code.squareroundforest.org/arpio/notation v0.0.0-20241225183158-af3bd591a174/go.mod h1:ait4Fvg9o0+bq5hlxi9dAcPL5a+/sr33qsZPNpToMLY=
|
code.squareroundforest.org/arpio/notation v0.0.0-20241225183158-af3bd591a174/go.mod h1:ait4Fvg9o0+bq5hlxi9dAcPL5a+/sr33qsZPNpToMLY=
|
||||||
|
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=
|
code.squareroundforest.org/arpio/treerack v0.0.0-20250820014405-1d956dcc6610 h1:I0jebdyQQfqJcwq2lT/TkUPBU8secHa5xZ+VzOdYVsw=
|
||||||
code.squareroundforest.org/arpio/treerack v0.0.0-20250820014405-1d956dcc6610/go.mod h1:9XhPcVt1Y1M609z02lHvEcp00dwPD9NUCoVxS2TpcH8=
|
code.squareroundforest.org/arpio/treerack v0.0.0-20250820014405-1d956dcc6610/go.mod h1:9XhPcVt1Y1M609z02lHvEcp00dwPD9NUCoVxS2TpcH8=
|
||||||
github.com/aryszka/notation v0.0.0-20230129164653-172017dde5e4 h1:JzqT9RArcw2sD4QPAyTss/sHaCZvCv+91DDJPZOrShw=
|
github.com/aryszka/notation v0.0.0-20230129164653-172017dde5e4 h1:JzqT9RArcw2sD4QPAyTss/sHaCZvCv+91DDJPZOrShw=
|
||||||
|
|||||||
67
help.go
67
help.go
@ -2,6 +2,7 @@ package wand
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"code.squareroundforest.org/arpio/docreflect"
|
"code.squareroundforest.org/arpio/docreflect"
|
||||||
|
"code.squareroundforest.org/arpio/bind"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"reflect"
|
"reflect"
|
||||||
@ -67,10 +68,11 @@ type (
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
func help() Cmd {
|
func help(sf []string) Cmd {
|
||||||
return Cmd{
|
return Cmd{
|
||||||
name: "help",
|
name: "help",
|
||||||
isHelp: true,
|
isHelp: true,
|
||||||
|
shortForms: sf,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -84,7 +86,7 @@ func insertHelp(cmd Cmd) Cmd {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if !hasHelpCmd && cmd.version == "" {
|
if !hasHelpCmd && cmd.version == "" {
|
||||||
cmd.subcommands = append(cmd.subcommands, help())
|
cmd.subcommands = append(cmd.subcommands, help(cmd.shortForms))
|
||||||
}
|
}
|
||||||
|
|
||||||
return cmd
|
return cmd
|
||||||
@ -127,11 +129,7 @@ func hasOptions(cmd Cmd) bool {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
v := reflect.ValueOf(cmd.impl)
|
return len(fields(cmd.impl)) > 0
|
||||||
t := v.Type()
|
|
||||||
t = unpack(t)
|
|
||||||
s := structParameters(t)
|
|
||||||
return len(fields(s...)) > 0
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func allCommands(cmd doc) []doc {
|
func allCommands(cmd doc) []doc {
|
||||||
@ -147,17 +145,17 @@ func allCommands(cmd doc) []doc {
|
|||||||
return commands
|
return commands
|
||||||
}
|
}
|
||||||
|
|
||||||
func functionParams(v reflect.Value, skip []int) ([]string, []string) {
|
func functionParams(v any, indices []int) ([]string, []string) {
|
||||||
names := docreflect.FunctionParams(v)
|
var names []string
|
||||||
|
r := reflect.ValueOf(v)
|
||||||
var types []reflect.Kind
|
allNames := docreflect.FunctionParams(r)
|
||||||
for i := 0; i < v.Type().NumIn(); i++ {
|
for _, i := range indices {
|
||||||
types = append(types, v.Type().In(i).Kind())
|
names = append(names, allNames[i])
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, i := range skip {
|
var types []reflect.Kind
|
||||||
names = append(names[:i], names[i+1:]...)
|
for _, i := range indices {
|
||||||
types = append(types[:i], types[i+1:]...)
|
types = append(types, r.Type().In(i).Kind())
|
||||||
}
|
}
|
||||||
|
|
||||||
var stypes []string
|
var stypes []string
|
||||||
@ -173,24 +171,22 @@ func constructArguments(cmd Cmd) argumentSet {
|
|||||||
return argumentSet{}
|
return argumentSet{}
|
||||||
}
|
}
|
||||||
|
|
||||||
v := unpack(reflect.ValueOf(cmd.impl))
|
p, variadic := positional(cmd.impl)
|
||||||
t := v.Type()
|
pi := positionalIndices(cmd.impl)
|
||||||
p := positionalParameters(t)
|
ior, iow := ioParameters(cmd.impl)
|
||||||
ior, iow := ioParameters(p)
|
names, types := functionParams(cmd.impl, pi)
|
||||||
count := len(p) - len(ior) - len(iow)
|
if len(names) < len(p) {
|
||||||
names, types := functionParams(v, append(ior, iow...))
|
|
||||||
if len(names) < count {
|
|
||||||
names = nil
|
names = nil
|
||||||
for i := 0; i < count; i++ {
|
for i := 0; i < len(p); i++ {
|
||||||
names = append(names, fmt.Sprintf("arg%d", i))
|
names = append(names, fmt.Sprintf("arg%d", i))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return argumentSet{
|
return argumentSet{
|
||||||
count: count,
|
count: len(p),
|
||||||
names: names,
|
names: names,
|
||||||
types: types,
|
types: types,
|
||||||
variadic: t.IsVariadic(),
|
variadic: variadic,
|
||||||
usesStdin: len(ior) > 0,
|
usesStdin: len(ior) > 0,
|
||||||
usesStdout: len(iow) > 0,
|
usesStdout: len(iow) > 0,
|
||||||
minPositional: cmd.minPositional,
|
minPositional: cmd.minPositional,
|
||||||
@ -230,13 +226,12 @@ func constructOptions(cmd Cmd, hasConfigFromOption bool) []docOption {
|
|||||||
sf[l] = append(sf[l], s)
|
sf[l] = append(sf[l], s)
|
||||||
}
|
}
|
||||||
|
|
||||||
t := unpack(reflect.ValueOf(cmd.impl).Type())
|
s := structParameters(cmd.impl)
|
||||||
s := structParameters(t)
|
|
||||||
d := make(map[string]string)
|
d := make(map[string]string)
|
||||||
for _, si := range s {
|
for _, si := range s {
|
||||||
f := fields(si)
|
f := structFields(si)
|
||||||
for _, fi := range f {
|
for _, fi := range f {
|
||||||
d[fi.name] = docreflect.Field(si, fi.path...)
|
d[fi.Name()] = docreflect.Field(si, fi.Path()...)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -245,14 +240,14 @@ func constructOptions(cmd Cmd, hasConfigFromOption bool) []docOption {
|
|||||||
for name, fi := range f {
|
for name, fi := range f {
|
||||||
opt := docOption{
|
opt := docOption{
|
||||||
name: name,
|
name: name,
|
||||||
typ: strings.ToLower(fmt.Sprint(fi[0].typ.Kind())),
|
typ: scalarTypeString(fi[0].Type()),
|
||||||
description: d[name],
|
description: d[name],
|
||||||
shortNames: sf[name],
|
shortNames: sf[name],
|
||||||
isBool: fi[0].typ.Kind() == reflect.Bool,
|
isBool: fi[0].Type() == bind.Bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, fii := range fi {
|
for _, fii := range fi {
|
||||||
if fii.acceptsMultiple {
|
if fii.List() {
|
||||||
opt.acceptsMultiple = true
|
opt.acceptsMultiple = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -292,15 +287,13 @@ func constructConfigDocs(cmd Cmd, conf Config) []docConfig {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func constructDoc(cmd Cmd, conf Config, fullCommand []string) doc {
|
func constructDoc(cmd Cmd, conf Config, fullCommand []string) doc {
|
||||||
hasConfigFromOption := hasConfigFromOption(conf)
|
|
||||||
|
|
||||||
var subcommands []doc
|
var subcommands []doc
|
||||||
for _, sc := range cmd.subcommands {
|
for _, sc := range cmd.subcommands {
|
||||||
subcommands = append(subcommands, constructDoc(sc, conf, append(fullCommand, sc.name)))
|
subcommands = append(subcommands, constructDoc(sc, conf, append(fullCommand, sc.name)))
|
||||||
}
|
}
|
||||||
|
|
||||||
var hasBoolOptions, hasListOptions bool
|
var hasBoolOptions, hasListOptions bool
|
||||||
options := constructOptions(cmd, hasConfigFromOption)
|
options := constructOptions(cmd, hasConfigFromOption(conf))
|
||||||
for _, o := range options {
|
for _, o := range options {
|
||||||
if o.isBool {
|
if o.isBool {
|
||||||
hasBoolOptions = true
|
hasBoolOptions = true
|
||||||
@ -322,7 +315,7 @@ func constructDoc(cmd Cmd, conf Config, fullCommand []string) doc {
|
|||||||
isHelp: cmd.isHelp,
|
isHelp: cmd.isHelp,
|
||||||
isVersion: cmd.version != "",
|
isVersion: cmd.version != "",
|
||||||
hasHelpSubcommand: hasHelpSubcommand(cmd),
|
hasHelpSubcommand: hasHelpSubcommand(cmd),
|
||||||
hasHelpOption: hasCustomHelpOption(cmd),
|
hasHelpOption: !hasCustomHelpOption(cmd),
|
||||||
options: options,
|
options: options,
|
||||||
hasBoolOptions: hasBoolOptions,
|
hasBoolOptions: hasBoolOptions,
|
||||||
hasListOptions: hasListOptions,
|
hasListOptions: hasListOptions,
|
||||||
|
|||||||
241
iniparser.gen.go
241
iniparser.gen.go
File diff suppressed because one or more lines are too long
79
input.go
79
input.go
@ -3,7 +3,7 @@ package wand
|
|||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"reflect"
|
"reflect"
|
||||||
"slices"
|
"code.squareroundforest.org/arpio/bind"
|
||||||
)
|
)
|
||||||
|
|
||||||
func validateKeyValues(cmd Cmd, keyValues map[string][]string, originalNames map[string]string) error {
|
func validateKeyValues(cmd Cmd, keyValues map[string][]string, originalNames map[string]string) error {
|
||||||
@ -15,20 +15,17 @@ func validateKeyValues(cmd Cmd, keyValues map[string][]string, originalNames map
|
|||||||
}
|
}
|
||||||
|
|
||||||
for _, fi := range f {
|
for _, fi := range f {
|
||||||
if len(values) > 1 && !fi.acceptsMultiple {
|
if len(values) > 1 && !fi.List() {
|
||||||
return fmt.Errorf(
|
return fmt.Errorf(
|
||||||
"expected only one value, received %d, as environment value, %s",
|
"expected only one value, received %d, for %s",
|
||||||
len(values),
|
len(values),
|
||||||
originalNames[name],
|
originalNames[name],
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, v := range values {
|
for _, v := range values {
|
||||||
if !canScan(fi.typ, v) {
|
if !canScan(fi.Type(), v) {
|
||||||
return fmt.Errorf(
|
return fmt.Errorf("type mismatch: %s", originalNames[name])
|
||||||
"environment variable cannot be applied, type mismatch: %s",
|
|
||||||
originalNames[name],
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -38,11 +35,19 @@ func validateKeyValues(cmd Cmd, keyValues map[string][]string, originalNames map
|
|||||||
}
|
}
|
||||||
|
|
||||||
func validateConfig(cmd Cmd, c config) error {
|
func validateConfig(cmd Cmd, c config) error {
|
||||||
return validateKeyValues(cmd, c.values, c.originalNames)
|
if err := validateKeyValues(cmd, c.values, c.originalNames); err != nil {
|
||||||
|
return fmt.Errorf("config: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func validateEnv(cmd Cmd, e env) error {
|
func validateEnv(cmd Cmd, e env) error {
|
||||||
return validateKeyValues(cmd, e.values, e.originalNames)
|
if err := validateKeyValues(cmd, e.values, e.originalNames); err != nil {
|
||||||
|
return fmt.Errorf("environment: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func validateOptions(cmd Cmd, o []option, conf Config) error {
|
func validateOptions(cmd Cmd, o []option, conf Config) error {
|
||||||
@ -64,27 +69,25 @@ func validateOptions(cmd Cmd, o []option, conf Config) error {
|
|||||||
mo[n] = append(mo[n], oi)
|
mo[n] = append(mo[n], oi)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
hasConfigOption := hasConfigFromOption(conf)
|
||||||
mf := mapFields(cmd.impl)
|
mf := mapFields(cmd.impl)
|
||||||
if hasConfigFromOption(conf) {
|
|
||||||
mf["config"] = []field{{
|
|
||||||
acceptsMultiple: true,
|
|
||||||
typ: reflect.TypeFor[string](),
|
|
||||||
}}
|
|
||||||
}
|
|
||||||
|
|
||||||
for n, os := range mo {
|
for n, os := range mo {
|
||||||
en := "--" + n
|
en := "--" + n
|
||||||
if sn, ok := ml[n]; ok {
|
if sn, ok := ml[n]; ok {
|
||||||
en += ", -" + sn
|
en += ", -" + sn
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if hasConfigOption && n == "config" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
f := mf[n]
|
f := mf[n]
|
||||||
if len(f) == 0 {
|
if len(f) == 0 {
|
||||||
return fmt.Errorf("option not supported: %s", en)
|
return fmt.Errorf("option not supported: %s", en)
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, fi := range f {
|
for _, fi := range f {
|
||||||
if len(os) > 1 && !fi.acceptsMultiple {
|
if len(os) > 1 && !fi.List() {
|
||||||
return fmt.Errorf(
|
return fmt.Errorf(
|
||||||
"expected only one value, received %d, as option, %s",
|
"expected only one value, received %d, as option, %s",
|
||||||
len(os),
|
len(os),
|
||||||
@ -93,14 +96,14 @@ func validateOptions(cmd Cmd, o []option, conf Config) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
for _, oi := range os {
|
for _, oi := range os {
|
||||||
if oi.value.isBool && fi.typ.Kind() != reflect.Bool {
|
if oi.value.isBool && fi.Type() != bind.Bool {
|
||||||
return fmt.Errorf(
|
return fmt.Errorf(
|
||||||
"received boolean value for field that does not accept it: %s",
|
"received boolean value for field that does not accept it: %s",
|
||||||
en,
|
en,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
if !oi.value.isBool && !canScan(fi.typ, oi.value.str) {
|
if !oi.value.isBool && !canScan(fi.Type(), oi.value.str) {
|
||||||
return fmt.Errorf(
|
return fmt.Errorf(
|
||||||
"option cannot be applied, type mismatch: %s",
|
"option cannot be applied, type mismatch: %s",
|
||||||
en,
|
en,
|
||||||
@ -114,20 +117,10 @@ func validateOptions(cmd Cmd, o []option, conf Config) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func validatePositionalArgs(cmd Cmd, a []string) error {
|
func validatePositionalArgs(cmd Cmd, a []string) error {
|
||||||
v := reflect.ValueOf(cmd.impl)
|
p, variadic := positional(cmd.impl)
|
||||||
v = unpack(v)
|
min := len(p)
|
||||||
t := v.Type()
|
max := len(p)
|
||||||
p := positionalParameters(t)
|
if variadic {
|
||||||
ior, iow := ioParameters(p)
|
|
||||||
last := t.NumIn() - 1
|
|
||||||
lastVariadic := t.IsVariadic() &&
|
|
||||||
!isStruct(t.In(last)) &&
|
|
||||||
!slices.Contains(ior, last) &&
|
|
||||||
!slices.Contains(iow, last)
|
|
||||||
length := len(p) - len(ior) - len(iow)
|
|
||||||
min := length
|
|
||||||
max := length
|
|
||||||
if lastVariadic {
|
|
||||||
min--
|
min--
|
||||||
max = -1
|
max = -1
|
||||||
}
|
}
|
||||||
@ -149,26 +142,18 @@ func validatePositionalArgs(cmd Cmd, a []string) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
for i, ai := range a {
|
for i, ai := range a {
|
||||||
if slices.Contains(ior, i) || slices.Contains(iow, i) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
var pi reflect.Type
|
var pi reflect.Type
|
||||||
if i >= length {
|
if i >= len(p) {
|
||||||
pi = p[length-1]
|
pi = p[len(p)-1]
|
||||||
} else {
|
} else {
|
||||||
pi = p[i]
|
pi = p[i]
|
||||||
}
|
}
|
||||||
|
|
||||||
if pi.Kind() == reflect.Interface {
|
if !canScanType(pi, ai) {
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
if !canScan(pi, ai) {
|
|
||||||
return fmt.Errorf(
|
return fmt.Errorf(
|
||||||
"cannot apply positional argument at index %d, expecting %v",
|
"cannot apply positional argument at index %d, expecting %s",
|
||||||
i,
|
i,
|
||||||
pi,
|
fmt.Sprint(pi.Kind()),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -49,6 +49,10 @@ func Args(cmd Cmd, min, max int) Cmd {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func ShortForm(cmd Cmd, f ...string) Cmd {
|
func ShortForm(cmd Cmd, f ...string) Cmd {
|
||||||
|
if len(f)%2 != 0 {
|
||||||
|
f = append(f, "")
|
||||||
|
}
|
||||||
|
|
||||||
cmd.shortForms = append(cmd.shortForms, f...)
|
cmd.shortForms = append(cmd.shortForms, f...)
|
||||||
for i := range cmd.subcommands {
|
for i := range cmd.subcommands {
|
||||||
cmd.subcommands[i] = ShortForm(
|
cmd.subcommands[i] = ShortForm(
|
||||||
13
notes.txt
Normal file
13
notes.txt
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
review if any symbols unused
|
||||||
|
test:
|
||||||
|
- nil return values
|
||||||
|
- options in variadic
|
||||||
|
- options in pointer
|
||||||
|
- options in slice
|
||||||
|
- options in slice and pointer
|
||||||
|
- parameters in pointers
|
||||||
|
- parameters in slices
|
||||||
|
- parameters in slices and pointers
|
||||||
|
- implementation in pointer
|
||||||
|
- implementation in slice => not accepted
|
||||||
|
- implementation in pointer and slice => not accepted
|
||||||
57
output.go
57
output.go
@ -7,32 +7,33 @@ import (
|
|||||||
"reflect"
|
"reflect"
|
||||||
)
|
)
|
||||||
|
|
||||||
func printOutput(w io.Writer, o []any) error {
|
func fprintOne(out io.Writer, v any) error {
|
||||||
wraperr := func(err error) error {
|
reader, ok := v.(io.Reader)
|
||||||
return fmt.Errorf("error copying output: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, oi := range o {
|
|
||||||
r, ok := oi.(io.Reader)
|
|
||||||
if ok {
|
if ok {
|
||||||
if _, err := io.Copy(w, r); err != nil {
|
_, err := io.Copy(out, reader)
|
||||||
return wraperr(err)
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
continue
|
r := reflect.ValueOf(v)
|
||||||
}
|
if r.IsValid() {
|
||||||
|
t := r.Type()
|
||||||
t := reflect.TypeOf(oi)
|
|
||||||
if t.Implements(reflect.TypeFor[fmt.Stringer]()) {
|
if t.Implements(reflect.TypeFor[fmt.Stringer]()) {
|
||||||
if _, err := fmt.Fprintln(w, oi); err != nil {
|
_, err := fmt.Fprintln(out, r.Interface())
|
||||||
return wraperr(err)
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
continue
|
if t.Kind() == reflect.Slice {
|
||||||
|
for i := 0; i < r.Len(); i++ {
|
||||||
|
if err := fprintOne(out, r.Index(i).Interface()); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
t = unpack(t, reflect.Pointer)
|
return nil
|
||||||
switch t.Kind() {
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
switch r.Kind() {
|
||||||
case reflect.Bool,
|
case reflect.Bool,
|
||||||
reflect.Int,
|
reflect.Int,
|
||||||
reflect.Int8,
|
reflect.Int8,
|
||||||
@ -47,19 +48,19 @@ func printOutput(w io.Writer, o []any) error {
|
|||||||
reflect.Uintptr,
|
reflect.Uintptr,
|
||||||
reflect.Float32,
|
reflect.Float32,
|
||||||
reflect.Float64,
|
reflect.Float64,
|
||||||
reflect.String,
|
reflect.String:
|
||||||
reflect.UnsafePointer:
|
_, err := fmt.Fprintln(out, v)
|
||||||
if _, err := fmt.Fprintln(w, oi); err != nil {
|
return err
|
||||||
return wraperr(err)
|
|
||||||
}
|
|
||||||
default:
|
default:
|
||||||
if _, err := notation.Fprintwt(w, oi); err != nil {
|
_, err := notation.Fprintlnwt(out, v)
|
||||||
return wraperr(err)
|
return err
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if _, err := fmt.Fprintln(w); err != nil {
|
func printOutput(out io.Writer, o []any) error {
|
||||||
return wraperr(err)
|
for _, oi := range o {
|
||||||
}
|
if err := fprintOne(out, oi); err != nil {
|
||||||
|
return fmt.Errorf("error displaying output: %w", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
637
reflect.go
637
reflect.go
@ -1,164 +1,274 @@
|
|||||||
package wand
|
package wand
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
|
||||||
"github.com/iancoleman/strcase"
|
|
||||||
"io"
|
|
||||||
"reflect"
|
"reflect"
|
||||||
"slices"
|
"code.squareroundforest.org/arpio/bind"
|
||||||
"strconv"
|
"io"
|
||||||
|
"time"
|
||||||
"strings"
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
type packedKind[T any] interface {
|
func filter[T any](list []T, predicate func(T) bool) []T {
|
||||||
Kind() reflect.Kind
|
var filtered []T
|
||||||
Elem() T
|
for _, item := range list {
|
||||||
}
|
if predicate(item) {
|
||||||
|
filtered = append(filtered, item)
|
||||||
type field struct {
|
|
||||||
name string
|
|
||||||
path []string
|
|
||||||
typ reflect.Type
|
|
||||||
acceptsMultiple bool
|
|
||||||
}
|
|
||||||
|
|
||||||
var (
|
|
||||||
readerType = reflect.TypeFor[io.Reader]()
|
|
||||||
writerType = reflect.TypeFor[io.Writer]()
|
|
||||||
)
|
|
||||||
|
|
||||||
func pack(v reflect.Value, t reflect.Type) reflect.Value {
|
|
||||||
if v.Type() == t {
|
|
||||||
return v
|
|
||||||
}
|
|
||||||
|
|
||||||
if t.Kind() == reflect.Pointer {
|
|
||||||
pv := pack(v, t.Elem())
|
|
||||||
p := reflect.New(t.Elem())
|
|
||||||
p.Elem().Set(pv)
|
|
||||||
return p
|
|
||||||
}
|
|
||||||
|
|
||||||
iv := pack(v, t.Elem())
|
|
||||||
s := reflect.MakeSlice(t, 1, 1)
|
|
||||||
s.Index(0).Set(iv)
|
|
||||||
return s
|
|
||||||
}
|
|
||||||
|
|
||||||
func unpack[T packedKind[T]](p T, kinds ...reflect.Kind) T {
|
|
||||||
if len(kinds) == 0 {
|
|
||||||
kinds = []reflect.Kind{reflect.Pointer, reflect.Slice}
|
|
||||||
}
|
|
||||||
|
|
||||||
if slices.Contains(kinds, p.Kind()) {
|
|
||||||
return unpack(p.Elem(), kinds...)
|
|
||||||
}
|
|
||||||
|
|
||||||
return p
|
|
||||||
}
|
|
||||||
|
|
||||||
func isReader(t reflect.Type) bool {
|
|
||||||
return unpack(t) == readerType
|
|
||||||
}
|
|
||||||
|
|
||||||
func isWriter(t reflect.Type) bool {
|
|
||||||
return unpack(t) == writerType
|
|
||||||
}
|
|
||||||
|
|
||||||
func isStruct(t reflect.Type) bool {
|
|
||||||
t = unpack(t)
|
|
||||||
return t.Kind() == reflect.Struct
|
|
||||||
}
|
|
||||||
|
|
||||||
func parseInt(s string, byteSize int) (int64, error) {
|
|
||||||
bitSize := byteSize * 8
|
|
||||||
switch {
|
|
||||||
case strings.HasPrefix(s, "0b"):
|
|
||||||
return strconv.ParseInt(s[2:], 2, bitSize)
|
|
||||||
case strings.HasPrefix(s, "0x"):
|
|
||||||
return strconv.ParseInt(s[2:], 16, bitSize)
|
|
||||||
case strings.HasPrefix(s, "0"):
|
|
||||||
return strconv.ParseInt(s[1:], 8, bitSize)
|
|
||||||
default:
|
|
||||||
return strconv.ParseInt(s, 10, bitSize)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func parseUint(s string, byteSize int) (uint64, error) {
|
return filtered
|
||||||
bitSize := byteSize * 8
|
}
|
||||||
switch {
|
|
||||||
case strings.HasPrefix(s, "0b"):
|
func not[T any](p func(T) bool) func(T) bool {
|
||||||
return strconv.ParseUint(s[2:], 2, bitSize)
|
return func(v T) bool {
|
||||||
case strings.HasPrefix(s, "0x"):
|
return !p(v)
|
||||||
return strconv.ParseUint(s[2:], 16, bitSize)
|
|
||||||
case strings.HasPrefix(s, "0"):
|
|
||||||
return strconv.ParseUint(s[1:], 8, bitSize)
|
|
||||||
default:
|
|
||||||
return strconv.ParseUint(s, 10, bitSize)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func canScan(t reflect.Type, s string) bool {
|
func and[T any](p ...func(T) bool) func(T) bool {
|
||||||
switch t.Kind() {
|
return func(v T) bool {
|
||||||
case reflect.Bool:
|
for _, pi := range p {
|
||||||
_, err := strconv.ParseBool(s)
|
if !pi(v) {
|
||||||
return err == nil
|
|
||||||
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
|
|
||||||
_, err := parseInt(s, int(t.Size()))
|
|
||||||
return err == nil
|
|
||||||
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
|
|
||||||
_, err := parseUint(s, int(t.Size()))
|
|
||||||
return err == nil
|
|
||||||
case reflect.Float32, reflect.Float64:
|
|
||||||
_, err := strconv.ParseFloat(s, int(t.Size())*8)
|
|
||||||
return err == nil
|
|
||||||
case reflect.String:
|
|
||||||
return true
|
|
||||||
default:
|
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func scan(t reflect.Type, s string) any {
|
return true
|
||||||
p := reflect.New(t)
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func or[T any](p ...func(T) bool) func(T) bool {
|
||||||
|
return func(v T) bool {
|
||||||
|
for _, pi := range p {
|
||||||
|
if pi(v) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func unpackTypeChecked(visited map[reflect.Type]bool, t reflect.Type) reflect.Type {
|
||||||
|
if t == nil {
|
||||||
|
return t
|
||||||
|
}
|
||||||
|
|
||||||
|
if visited[t] {
|
||||||
|
return t
|
||||||
|
}
|
||||||
|
|
||||||
|
if visited == nil {
|
||||||
|
visited = make(map[reflect.Type]bool)
|
||||||
|
}
|
||||||
|
|
||||||
|
visited[t] = true
|
||||||
switch t.Kind() {
|
switch t.Kind() {
|
||||||
case reflect.Bool:
|
case reflect.Pointer, reflect.Slice:
|
||||||
v, _ := strconv.ParseBool(s)
|
return unpackTypeChecked(visited, t.Elem())
|
||||||
p.Elem().Set(reflect.ValueOf(v).Convert(t))
|
|
||||||
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
|
|
||||||
v, _ := parseInt(s, int(t.Size()))
|
|
||||||
p.Elem().Set(reflect.ValueOf(v).Convert(t))
|
|
||||||
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
|
|
||||||
v, _ := parseUint(s, int(t.Size()))
|
|
||||||
p.Elem().Set(reflect.ValueOf(v).Convert(t))
|
|
||||||
case reflect.Float32, reflect.Float64:
|
|
||||||
v, _ := strconv.ParseFloat(s, int(t.Size())*8)
|
|
||||||
p.Elem().Set(reflect.ValueOf(v).Convert(t))
|
|
||||||
default:
|
default:
|
||||||
p.Elem().Set(reflect.ValueOf(s).Convert(t))
|
return t
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return p.Elem().Interface()
|
func unpackType(t reflect.Type) reflect.Type {
|
||||||
|
return unpackTypeChecked(nil, t)
|
||||||
}
|
}
|
||||||
|
|
||||||
func fieldsChecked(visited map[reflect.Type]bool, s ...reflect.Type) ([]field, error) {
|
func unpackValueChecked(visited map[uintptr]bool, v reflect.Value) reflect.Value {
|
||||||
if len(s) == 0 {
|
if !v.IsValid() {
|
||||||
return nil, nil
|
return v
|
||||||
}
|
}
|
||||||
|
|
||||||
var (
|
switch v.Kind() {
|
||||||
anonFields []field
|
case reflect.Pointer:
|
||||||
plainFields []field
|
p := v.Pointer()
|
||||||
|
if visited[p] {
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
|
||||||
|
if visited == nil {
|
||||||
|
visited = make(map[uintptr]bool)
|
||||||
|
}
|
||||||
|
|
||||||
|
visited[p] = true
|
||||||
|
return unpackValueChecked(visited, v.Elem())
|
||||||
|
case reflect.Interface:
|
||||||
|
if v.IsNil() {
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
|
||||||
|
return unpackValueChecked(visited, v.Elem())
|
||||||
|
default:
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func unpackValue(v reflect.Value) reflect.Value {
|
||||||
|
return unpackValueChecked(nil, v)
|
||||||
|
}
|
||||||
|
|
||||||
|
func isFunc(v any) bool {
|
||||||
|
r := reflect.ValueOf(v)
|
||||||
|
r = unpackValue(r)
|
||||||
|
return r.Kind() == reflect.Func
|
||||||
|
}
|
||||||
|
|
||||||
|
func isTime(t reflect.Type) bool {
|
||||||
|
if t == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return t.ConvertibleTo(reflect.TypeFor[time.Time]())
|
||||||
|
}
|
||||||
|
|
||||||
|
func isStruct(t reflect.Type) bool {
|
||||||
|
if t == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
t = unpackType(t)
|
||||||
|
return !isTime(t) && t.Kind() == reflect.Struct
|
||||||
|
}
|
||||||
|
|
||||||
|
func isReader(t reflect.Type) bool {
|
||||||
|
if t == nil || t.Kind() != reflect.Interface {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return t.NumMethod() == 1 && t.Implements(reflect.TypeFor[io.Reader]())
|
||||||
|
}
|
||||||
|
|
||||||
|
func isWriter(t reflect.Type) bool {
|
||||||
|
if t == nil || t.Kind() != reflect.Interface {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return t.NumMethod() == 1 && t.Implements(reflect.TypeFor[io.Writer]())
|
||||||
|
}
|
||||||
|
|
||||||
|
func compatibleTypes(t ...bind.Scalar) bool {
|
||||||
|
if len(t) == 0 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(t) == 1 {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
switch t[0] {
|
||||||
|
case bind.Any:
|
||||||
|
return compatibleTypes(t[1:]...)
|
||||||
|
default:
|
||||||
|
return t[0] == t[1] && compatibleTypes(t[1:]...)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func parameters(f any) []reflect.Type {
|
||||||
|
r := reflect.ValueOf(f)
|
||||||
|
r = unpackValue(r)
|
||||||
|
if r.Kind() != reflect.Func {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var p []reflect.Type
|
||||||
|
t := r.Type()
|
||||||
|
for i := 0; i < t.NumIn(); i++ {
|
||||||
|
p = append(p, t.In(i))
|
||||||
|
}
|
||||||
|
|
||||||
|
return p
|
||||||
|
}
|
||||||
|
|
||||||
|
func structParameters(f any) []reflect.Type {
|
||||||
|
return filter(parameters(f), isStruct)
|
||||||
|
}
|
||||||
|
|
||||||
|
func structFields(s reflect.Type) []bind.Field {
|
||||||
|
s = unpackType(s)
|
||||||
|
v := reflect.Zero(s)
|
||||||
|
return bind.FieldValues(v)
|
||||||
|
}
|
||||||
|
|
||||||
|
func fields(f any) []bind.Field {
|
||||||
|
var fields []bind.Field
|
||||||
|
s := structParameters(fields)
|
||||||
|
for _, si := range s {
|
||||||
|
fields = append(fields, structFields(si)...)
|
||||||
|
}
|
||||||
|
|
||||||
|
return fields
|
||||||
|
}
|
||||||
|
|
||||||
|
func mapFields(f any) map[string][]bind.Field {
|
||||||
|
fields := fields(fields)
|
||||||
|
m := make(map[string][]bind.Field)
|
||||||
|
for _, fi := range fields {
|
||||||
|
m[fi.Name()] = append(m[fi.Name()], fi)
|
||||||
|
}
|
||||||
|
|
||||||
|
return m
|
||||||
|
}
|
||||||
|
|
||||||
|
func boolFields(f []bind.Field) []bind.Field {
|
||||||
|
return filter(
|
||||||
|
fields(f),
|
||||||
|
func(f bind.Field) bool { return f.Type() == bind.Bool },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func positional(f any) ([]reflect.Type, bool) {
|
||||||
|
p := filter(
|
||||||
|
parameters(f),
|
||||||
|
not(or(isReader, isWriter, isStruct)),
|
||||||
)
|
)
|
||||||
|
|
||||||
for i := 0; i < s[0].NumField(); i++ {
|
r := reflect.ValueOf(f)
|
||||||
sf := s[0].Field(i)
|
r = unpackValue(r)
|
||||||
sft := sf.Type
|
t := r.Type()
|
||||||
am := acceptsMultiple(sft)
|
return p, t.IsVariadic()
|
||||||
sft = unpack(sft)
|
}
|
||||||
sfn := sf.Name
|
|
||||||
sfn = strcase.ToKebab(sfn)
|
func positionalIndices(f any) []int {
|
||||||
switch sft.Kind() {
|
r := reflect.ValueOf(f)
|
||||||
|
r = unpackValue(r)
|
||||||
|
if r.Kind() != reflect.Func {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var indices []int
|
||||||
|
t := r.Type()
|
||||||
|
for i := 0; i < t.NumIn(); i++ {
|
||||||
|
p := t.In(i)
|
||||||
|
if isTime(p) || isStruct(p) || isReader(p) || isWriter(p) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
indices = append(indices, i)
|
||||||
|
}
|
||||||
|
|
||||||
|
return indices
|
||||||
|
}
|
||||||
|
|
||||||
|
func ioParameters(f any) ([]reflect.Type, []reflect.Type) {
|
||||||
|
p := parameters(f)
|
||||||
|
return filter(p, isReader), filter(p, isWriter)
|
||||||
|
}
|
||||||
|
|
||||||
|
func bindable(t reflect.Type) bool {
|
||||||
|
if t == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if isTime(t) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
if isStruct(t) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
switch t.Kind() {
|
||||||
case reflect.Bool,
|
case reflect.Bool,
|
||||||
reflect.Int,
|
reflect.Int,
|
||||||
reflect.Int8,
|
reflect.Int8,
|
||||||
@ -173,194 +283,97 @@ func fieldsChecked(visited map[reflect.Type]bool, s ...reflect.Type) ([]field, e
|
|||||||
reflect.Float32,
|
reflect.Float32,
|
||||||
reflect.Float64,
|
reflect.Float64,
|
||||||
reflect.String:
|
reflect.String:
|
||||||
plainFields = append(plainFields, field{
|
return true
|
||||||
name: sfn,
|
|
||||||
path: []string{sf.Name},
|
|
||||||
typ: sft,
|
|
||||||
acceptsMultiple: am,
|
|
||||||
})
|
|
||||||
case reflect.Interface:
|
case reflect.Interface:
|
||||||
if sft.NumMethod() == 0 {
|
return t.NumMethod() == 0
|
||||||
plainFields = append(plainFields, field{
|
case reflect.Slice:
|
||||||
name: sfn,
|
return bindable(t.Elem())
|
||||||
path: []string{sf.Name},
|
|
||||||
typ: sft,
|
|
||||||
acceptsMultiple: am,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
case reflect.Struct:
|
|
||||||
if visited[sft] {
|
|
||||||
return nil, fmt.Errorf("circular type definitions not allowed: %s", sft.Name())
|
|
||||||
}
|
|
||||||
|
|
||||||
if visited == nil {
|
|
||||||
visited = make(map[reflect.Type]bool)
|
|
||||||
}
|
|
||||||
|
|
||||||
visited[sft] = true
|
|
||||||
sff, err := fieldsChecked(visited, sft)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
if sf.Anonymous {
|
|
||||||
anonFields = append(anonFields, sff...)
|
|
||||||
} else {
|
|
||||||
for i := range sff {
|
|
||||||
sff[i].name = sfn + "-" + sff[i].name
|
|
||||||
sff[i].path = append([]string{sf.Name}, sff[i].path...)
|
|
||||||
sff[i].acceptsMultiple = sff[i].acceptsMultiple || am
|
|
||||||
}
|
|
||||||
|
|
||||||
plainFields = append(plainFields, sff...)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
mf := make(map[string]field)
|
|
||||||
for _, fi := range anonFields {
|
|
||||||
mf[fi.name] = fi
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, fi := range plainFields {
|
|
||||||
mf[fi.name] = fi
|
|
||||||
}
|
|
||||||
|
|
||||||
var f []field
|
|
||||||
for _, fi := range mf {
|
|
||||||
f = append(f, fi)
|
|
||||||
}
|
|
||||||
|
|
||||||
ff, err := fieldsChecked(visited, s[1:]...)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return append(f, ff...), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func fields(s ...reflect.Type) []field {
|
|
||||||
f, _ := fieldsChecked(nil, s...)
|
|
||||||
return f
|
|
||||||
}
|
|
||||||
|
|
||||||
func boolFields(f []field) []field {
|
|
||||||
var b []field
|
|
||||||
for _, fi := range f {
|
|
||||||
if fi.typ.Kind() == reflect.Bool {
|
|
||||||
b = append(b, fi)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return b
|
|
||||||
}
|
|
||||||
|
|
||||||
func mapFields(impl any) map[string][]field {
|
|
||||||
v := reflect.ValueOf(impl)
|
|
||||||
t := v.Type()
|
|
||||||
t = unpack(t)
|
|
||||||
s := structParameters(t)
|
|
||||||
f := fields(s...)
|
|
||||||
mf := make(map[string][]field)
|
|
||||||
for _, fi := range f {
|
|
||||||
mf[fi.name] = append(mf[fi.name], fi)
|
|
||||||
}
|
|
||||||
|
|
||||||
return mf
|
|
||||||
}
|
|
||||||
|
|
||||||
func filterParameters(t reflect.Type, f func(reflect.Type) bool) []reflect.Type {
|
|
||||||
var s []reflect.Type
|
|
||||||
for i := 0; i < t.NumIn(); i++ {
|
|
||||||
p := t.In(i)
|
|
||||||
p = unpack(p)
|
|
||||||
if f(p) {
|
|
||||||
s = append(s, p)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return s
|
|
||||||
}
|
|
||||||
|
|
||||||
func positionalParameters(t reflect.Type) []reflect.Type {
|
|
||||||
return filterParameters(t, func(p reflect.Type) bool {
|
|
||||||
return p.Kind() != reflect.Struct
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func ioParameters(p []reflect.Type) ([]int, []int) {
|
|
||||||
var (
|
|
||||||
reader []int
|
|
||||||
writer []int
|
|
||||||
)
|
|
||||||
|
|
||||||
for i, pi := range p {
|
|
||||||
switch {
|
|
||||||
case isReader(pi):
|
|
||||||
reader = append(reader, i)
|
|
||||||
case isWriter(pi):
|
|
||||||
writer = append(writer, i)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return reader, writer
|
|
||||||
}
|
|
||||||
|
|
||||||
func structParameters(t reflect.Type) []reflect.Type {
|
|
||||||
return filterParameters(t, func(p reflect.Type) bool {
|
|
||||||
return p.Kind() == reflect.Struct
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func compatibleTypes(t ...reflect.Type) bool {
|
|
||||||
if len(t) < 2 {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
t0 := t[0]
|
|
||||||
t1 := t[1]
|
|
||||||
switch t0.Kind() {
|
|
||||||
case reflect.Bool:
|
|
||||||
return t1.Kind() == reflect.Bool
|
|
||||||
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
|
|
||||||
switch t1.Kind() {
|
|
||||||
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
|
|
||||||
return true
|
|
||||||
default:
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
|
|
||||||
switch t1.Kind() {
|
|
||||||
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
|
|
||||||
return true
|
|
||||||
default:
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
case reflect.Float32, reflect.Float64:
|
|
||||||
switch t1.Kind() {
|
|
||||||
case reflect.Float32, reflect.Float64:
|
|
||||||
return true
|
|
||||||
default:
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
case reflect.String:
|
|
||||||
return t1.Kind() == reflect.String
|
|
||||||
case reflect.Interface:
|
|
||||||
return t1.Kind() == reflect.Interface && t0.NumMethod() == 0 && t1.NumMethod() == 0
|
|
||||||
default:
|
default:
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func acceptsMultiple(t reflect.Type) bool {
|
func scalarTypeString(t bind.Scalar) string {
|
||||||
if t.Kind() == reflect.Slice {
|
r := reflect.TypeOf(t)
|
||||||
return true
|
p := strings.Split(r.Name(), ".")
|
||||||
|
n := p[len(p) - 1]
|
||||||
|
n = strings.ToLower(n)
|
||||||
|
return n
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func canScan(t bind.Scalar, v any) bool {
|
||||||
|
switch t {
|
||||||
|
case bind.Any:
|
||||||
|
return true
|
||||||
|
case bind.Bool:
|
||||||
|
_, ok := bind.BindScalarCreate[bool](v)
|
||||||
|
return ok
|
||||||
|
case bind.Int:
|
||||||
|
_, ok := bind.BindScalarCreate[int](v)
|
||||||
|
return ok
|
||||||
|
case bind.Uint:
|
||||||
|
_, ok := bind.BindScalarCreate[uint](v)
|
||||||
|
return ok
|
||||||
|
case bind.Float:
|
||||||
|
_, ok := bind.BindScalarCreate[float64](v)
|
||||||
|
return ok
|
||||||
|
case bind.String:
|
||||||
|
_, ok := bind.BindScalarCreate[string](v)
|
||||||
|
return ok
|
||||||
|
case bind.Duration:
|
||||||
|
_, ok := bind.BindScalarCreate[time.Duration](v)
|
||||||
|
return ok
|
||||||
|
case bind.Time:
|
||||||
|
_, ok := bind.BindScalarCreate[time.Time](v)
|
||||||
|
return ok
|
||||||
|
default:
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func canScanType(t reflect.Type, v any) bool {
|
||||||
|
if t == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
r := reflect.Zero(t)
|
||||||
|
return bind.BindScalar(r.Interface(), v)
|
||||||
|
}
|
||||||
|
|
||||||
|
func allocate(t reflect.Type) reflect.Value {
|
||||||
switch t.Kind() {
|
switch t.Kind() {
|
||||||
case reflect.Pointer:
|
case reflect.Pointer:
|
||||||
return acceptsMultiple(t.Elem())
|
et := t.Elem()
|
||||||
|
v := allocate(et)
|
||||||
|
p := reflect.New(et)
|
||||||
|
p.Elem().Set(v)
|
||||||
|
return p
|
||||||
|
case reflect.Slice:
|
||||||
|
v := allocate(t.Elem())
|
||||||
|
s := reflect.MakeSlice(t, 1, 1)
|
||||||
|
s.Index(0).Set(v)
|
||||||
|
return s
|
||||||
default:
|
default:
|
||||||
return false
|
return reflect.Zero(t)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func bindScalar(receiver reflect.Value, value any) {
|
||||||
|
bind.BindScalar(receiver.Interface(), value)
|
||||||
|
}
|
||||||
|
|
||||||
|
func bindFields(receiver reflect.Value, values map[string][]any) []string {
|
||||||
|
var f []bind.Field
|
||||||
|
for name, value := range values {
|
||||||
|
f = append(f, bind.NamedValue(name, value))
|
||||||
|
}
|
||||||
|
|
||||||
|
unmapped := bind.BindFields(receiver, f...)
|
||||||
|
|
||||||
|
var names []string
|
||||||
|
for _, um := range unmapped {
|
||||||
|
names = append(names, um.Name())
|
||||||
|
}
|
||||||
|
|
||||||
|
return names
|
||||||
|
}
|
||||||
|
|||||||
@ -1,9 +1,9 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"code.squareroundforest.org/arpio/docreflect/generate"
|
|
||||||
"log"
|
"log"
|
||||||
"os"
|
"os"
|
||||||
|
"code.squareroundforest.org/arpio/wand/tools"
|
||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
@ -11,7 +11,7 @@ func main() {
|
|||||||
log.Fatalln("expected package name")
|
log.Fatalln("expected package name")
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := generate.GenerateRegistry(os.Stdout, os.Args[1], os.Args[2:]...); err != nil {
|
if err := tools.Docreflect(os.Stdout, os.Args[1], os.Args[2:]...); err != nil {
|
||||||
log.Fatalln(err)
|
log.Fatalln(err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
45
tools/exec.go
Normal file
45
tools/exec.go
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
package tools
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io"
|
||||||
|
"strings"
|
||||||
|
"os/exec"
|
||||||
|
"os"
|
||||||
|
"bytes"
|
||||||
|
)
|
||||||
|
|
||||||
|
func execc(stdin io.Reader, stdout, stderr io.Writer, command string, args []string, env []string) error {
|
||||||
|
c := strings.Split(command, " ")
|
||||||
|
cmd := exec.Command(c[0], append(c[1:], args...)...)
|
||||||
|
cmd.Env = append(os.Environ(), env...)
|
||||||
|
cmd.Stdin = stdin
|
||||||
|
cmd.Stdout = stdout
|
||||||
|
cmd.Stderr = stderr
|
||||||
|
return cmd.Run()
|
||||||
|
}
|
||||||
|
|
||||||
|
func execCommandDir(out io.Writer, commandDir string, env ...string) error {
|
||||||
|
stderr := bytes.NewBuffer(nil)
|
||||||
|
if err := execc(nil, out, stderr, "go run", []string{commandDir}, env); err != nil {
|
||||||
|
io.Copy(os.Stderr, stderr)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func execInternal(command string, args ...string) error {
|
||||||
|
stdout := bytes.NewBuffer(nil)
|
||||||
|
stderr := bytes.NewBuffer(nil)
|
||||||
|
if err := execc(nil, stdout, stderr, command, args, nil); err != nil {
|
||||||
|
io.Copy(os.Stderr, stdout)
|
||||||
|
io.Copy(os.Stderr, stderr)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func execTransparent(command string, args ...string) error {
|
||||||
|
return execc(os.Stdin, os.Stdout, os.Stderr, command, args, nil)
|
||||||
|
}
|
||||||
308
tools/execwand.go
Normal file
308
tools/execwand.go
Normal file
@ -0,0 +1,308 @@
|
|||||||
|
package tools
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io"
|
||||||
|
"bufio"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"unicode"
|
||||||
|
"hash/fnv"
|
||||||
|
"bytes"
|
||||||
|
"encoding/base64"
|
||||||
|
"strings"
|
||||||
|
"os"
|
||||||
|
"sort"
|
||||||
|
"path"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ExecOptions struct {
|
||||||
|
NoCache bool
|
||||||
|
ClearCache bool
|
||||||
|
CacheDir string
|
||||||
|
Import []string
|
||||||
|
InlineImport []string
|
||||||
|
}
|
||||||
|
|
||||||
|
func commandReader(in io.Reader) func() ([]string, error) {
|
||||||
|
var (
|
||||||
|
yieldErr error
|
||||||
|
currentArg []rune
|
||||||
|
args []string
|
||||||
|
escapeOne, escapePartial, escapeFull bool
|
||||||
|
)
|
||||||
|
|
||||||
|
buf := bufio.NewReader(in)
|
||||||
|
return func() ([]string, error) {
|
||||||
|
if yieldErr != nil {
|
||||||
|
return nil, yieldErr
|
||||||
|
}
|
||||||
|
|
||||||
|
for {
|
||||||
|
r, _, err := buf.ReadRune()
|
||||||
|
if errors.Is(err, io.EOF) {
|
||||||
|
if len(currentArg) > 0 {
|
||||||
|
args = append(args, string(currentArg))
|
||||||
|
currentArg = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
yield := args
|
||||||
|
args = nil
|
||||||
|
yieldErr = err
|
||||||
|
return yield, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
yieldErr = fmt.Errorf("failed reading from input: %w", err)
|
||||||
|
return nil, yieldErr
|
||||||
|
}
|
||||||
|
|
||||||
|
if r == unicode.ReplacementChar {
|
||||||
|
if len(currentArg) > 0 {
|
||||||
|
yieldErr = errors.New("broken unicode stream")
|
||||||
|
return nil, yieldErr
|
||||||
|
}
|
||||||
|
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if escapeFull {
|
||||||
|
if r == '\'' {
|
||||||
|
escapeFull = false
|
||||||
|
args = append(args, string(currentArg))
|
||||||
|
currentArg = nil
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
currentArg = append(currentArg, r)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if escapeOne {
|
||||||
|
escapeOne = false
|
||||||
|
currentArg = append(currentArg, r)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if escapePartial {
|
||||||
|
if escapeOne {
|
||||||
|
escapeOne = false
|
||||||
|
currentArg = append(currentArg, r)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if r == '\\' {
|
||||||
|
escapeOne = true
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if r == '"' {
|
||||||
|
escapePartial = false
|
||||||
|
args = append(args, string(currentArg))
|
||||||
|
currentArg = nil
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
currentArg = append(currentArg, r)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if r == '\n' {
|
||||||
|
if len(currentArg) > 0 {
|
||||||
|
args = append(args, string(currentArg))
|
||||||
|
currentArg = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
yield := args
|
||||||
|
args = nil
|
||||||
|
return yield, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if unicode.IsSpace(r) {
|
||||||
|
if len(currentArg) > 0 {
|
||||||
|
args = append(args, string(currentArg))
|
||||||
|
currentArg = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
switch r {
|
||||||
|
case '\\':
|
||||||
|
escapeOne = true
|
||||||
|
case '"':
|
||||||
|
escapePartial = true
|
||||||
|
case '\'':
|
||||||
|
escapeFull = true
|
||||||
|
default:
|
||||||
|
currentArg = append(currentArg, r)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func hash(expression string, imports, inlineImports []string) (string, error) {
|
||||||
|
h := fnv.New128()
|
||||||
|
h.Write([]byte(expression))
|
||||||
|
allImports := append(imports, inlineImports...)
|
||||||
|
sort.Strings(allImports)
|
||||||
|
for _, i := range allImports {
|
||||||
|
h.Write([]byte(i))
|
||||||
|
}
|
||||||
|
|
||||||
|
buf := bytes.NewBuffer(nil)
|
||||||
|
b64 := base64.NewEncoder(base64.RawURLEncoding, buf)
|
||||||
|
if _, err := b64.Write(h.Sum(nil)); err != nil {
|
||||||
|
return "", fmt.Errorf("failed to encode expression: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := b64.Close(); err != nil {
|
||||||
|
return "", fmt.Errorf("failed to complete encoding of expression: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return strings.TrimPrefix(buf.String(), "_"), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func printGoFile(fn string, expression string, imports []string, inlineImports []string) error {
|
||||||
|
f, err := os.Create(fn)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
defer f.Close()
|
||||||
|
fprintf := func(format string, args ...any) {
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = fmt.Fprintf(f, format, args...)
|
||||||
|
}
|
||||||
|
|
||||||
|
fprintf("package main\n")
|
||||||
|
for _, i := range imports {
|
||||||
|
fprintf("import \"%s\"\n", i)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, i := range inlineImports {
|
||||||
|
fprintf("import . \"%s\"\n", i)
|
||||||
|
}
|
||||||
|
|
||||||
|
fprintf("import \"code.squareroundforest.org/arpio/wand\"\n")
|
||||||
|
fprintf("func main() {\n")
|
||||||
|
fprintf("wand.Exec(%s)\n", expression)
|
||||||
|
fprintf("}")
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func execWand(o ExecOptions, args []string) error {
|
||||||
|
expression, args := args[0], args[1:]
|
||||||
|
commandHash, err := hash(expression, o.Import, o.InlineImport)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
cacheDir := o.CacheDir
|
||||||
|
if cacheDir == "" {
|
||||||
|
cacheDir = path.Join(os.Getenv("HOME"), ".wand")
|
||||||
|
}
|
||||||
|
|
||||||
|
commandDir := path.Join(cacheDir, commandHash)
|
||||||
|
if o.NoCache {
|
||||||
|
commandDir = path.Join(cacheDir, "tmp", commandHash)
|
||||||
|
}
|
||||||
|
|
||||||
|
if o.NoCache || o.ClearCache {
|
||||||
|
if err := os.RemoveAll(commandDir); err != nil {
|
||||||
|
return fmt.Errorf("failed to clear cache: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := os.MkdirAll(commandDir, os.ModePerm); err != nil {
|
||||||
|
return fmt.Errorf("failed to ensure cache directory: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
wd, err := os.Getwd()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("error identifying current directory: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
goGet := func(pkg string) error {
|
||||||
|
if err := execInternal("go get", pkg); err != nil {
|
||||||
|
return fmt.Errorf("failed to get go module: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := os.Chdir(commandDir); err != nil {
|
||||||
|
return fmt.Errorf("failed to switch to temporary directory: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
defer os.Chdir(wd)
|
||||||
|
gomodPath := path.Join(commandDir, "go.mod")
|
||||||
|
if _, err := os.Stat(gomodPath); err != nil {
|
||||||
|
if err := execInternal("go mod init", commandHash); err != nil {
|
||||||
|
return fmt.Errorf("failed to initialize temporary module: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, pkg := range o.Import {
|
||||||
|
if err := goGet(pkg); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, pkg := range o.InlineImport {
|
||||||
|
if err := goGet(pkg); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := goGet("code.squareroundforest.org/arpio/wand"); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
goFile := path.Join(commandDir, fmt.Sprintf("%s.go", commandHash))
|
||||||
|
if _, err := os.Stat(goFile); err != nil {
|
||||||
|
if err := printGoFile(goFile, expression, o.Import, o.InlineImport); err != nil {
|
||||||
|
return fmt.Errorf("failed to create temporary go file: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := execTransparent("go run", append([]string{commandDir}, args...)...); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if o.NoCache {
|
||||||
|
if err := os.RemoveAll(commandDir); err != nil {
|
||||||
|
return fmt.Errorf("failed to clean cache: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func readExec(o ExecOptions, stdin io.Reader) error {
|
||||||
|
readCommand := commandReader(stdin)
|
||||||
|
for {
|
||||||
|
args, err := readCommand()
|
||||||
|
if errors.Is(err, io.EOF) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := execWand(o, args); err != nil {
|
||||||
|
fmt.Fprintln(os.Stderr, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func Exec(o ExecOptions, stdin io.Reader, args ...string) error {
|
||||||
|
if len(args) == 0 {
|
||||||
|
return readExec(o, stdin)
|
||||||
|
}
|
||||||
|
|
||||||
|
return execWand(o, args)
|
||||||
|
}
|
||||||
23
tools/lib.go
Normal file
23
tools/lib.go
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
package tools
|
||||||
|
|
||||||
|
import (
|
||||||
|
"code.squareroundforest.org/arpio/docreflect/generate"
|
||||||
|
"io"
|
||||||
|
"fmt"
|
||||||
|
)
|
||||||
|
|
||||||
|
type MarkdownOptions struct {
|
||||||
|
Level int
|
||||||
|
}
|
||||||
|
|
||||||
|
func Docreflect(out io.Writer, packageName string, gopaths ...string) error {
|
||||||
|
return generate.GenerateRegistry(out, packageName, gopaths...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func Man(out io.Writer, commandDir string) error {
|
||||||
|
return execCommandDir(out, commandDir, "_wandgenerate=man")
|
||||||
|
}
|
||||||
|
|
||||||
|
func Markdown(out io.Writer, o MarkdownOptions, commandDir string) error {
|
||||||
|
return execCommandDir(out, commandDir, "_wandgenerate=markdown", fmt.Sprintf("_wandmarkdownlevel=%d", o.Level))
|
||||||
|
}
|
||||||
273
tools/tools.go
273
tools/tools.go
@ -1,273 +0,0 @@
|
|||||||
package tools
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"code.squareroundforest.org/arpio/docreflect/generate"
|
|
||||||
"encoding/base64"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"hash/fnv"
|
|
||||||
"io"
|
|
||||||
"os"
|
|
||||||
"os/exec"
|
|
||||||
"path"
|
|
||||||
"strings"
|
|
||||||
)
|
|
||||||
|
|
||||||
type ExecOptions struct {
|
|
||||||
NoCache bool
|
|
||||||
ClearCache bool
|
|
||||||
CacheDir string
|
|
||||||
}
|
|
||||||
|
|
||||||
func execc(stdin io.Reader, stdout, stderr io.Writer, command string, args []string, env []string) error {
|
|
||||||
c := strings.Split(command, " ")
|
|
||||||
cmd := exec.Command(c[0], append(c[1:], args...)...)
|
|
||||||
cmd.Env = append(os.Environ(), env...)
|
|
||||||
cmd.Stdin = stdin
|
|
||||||
cmd.Stdout = stdout
|
|
||||||
cmd.Stderr = stderr
|
|
||||||
return cmd.Run()
|
|
||||||
}
|
|
||||||
|
|
||||||
func execCommandDir(out io.Writer, commandDir string, env ...string) error {
|
|
||||||
stderr := bytes.NewBuffer(nil)
|
|
||||||
if err := execc(nil, out, stderr, "go run", []string{commandDir}, env); err != nil {
|
|
||||||
io.Copy(os.Stderr, stderr)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func execInternal(command string, args ...string) error {
|
|
||||||
stdout := bytes.NewBuffer(nil)
|
|
||||||
stderr := bytes.NewBuffer(nil)
|
|
||||||
if err := execc(nil, stdout, stderr, command, args, nil); err != nil {
|
|
||||||
io.Copy(os.Stderr, stdout)
|
|
||||||
io.Copy(os.Stderr, stderr)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func execTransparent(command string, args ...string) error {
|
|
||||||
return execc(os.Stdin, os.Stdout, os.Stderr, command, args, nil)
|
|
||||||
}
|
|
||||||
|
|
||||||
func Docreflect(out io.Writer, packageName string, gopaths ...string) error {
|
|
||||||
return generate.GenerateRegistry(out, packageName, gopaths...)
|
|
||||||
}
|
|
||||||
|
|
||||||
func Man(out io.Writer, commandDir string) error {
|
|
||||||
return execCommandDir(out, commandDir, "wandgenerate=man")
|
|
||||||
}
|
|
||||||
|
|
||||||
func Markdown(out io.Writer, commandDir string) error {
|
|
||||||
return execCommandDir(out, commandDir, "wandgenerate=markdown")
|
|
||||||
}
|
|
||||||
|
|
||||||
func splitFunction(function string) (pkg string, expression string, err error) {
|
|
||||||
parts := strings.Split(function, "/")
|
|
||||||
gopath := parts[:len(parts)-1]
|
|
||||||
sparts := strings.Split(parts[len(parts)-1], ".")
|
|
||||||
if len(sparts) == 1 && len(gopath) > 1 {
|
|
||||||
err = errors.New("function cannot be identified")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(sparts) == 1 {
|
|
||||||
expression = sparts[0]
|
|
||||||
} else {
|
|
||||||
expression = parts[len(parts)-1]
|
|
||||||
pkg = strings.Join(append(gopath, sparts[0]), "/")
|
|
||||||
}
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
func functionHash(function string) (string, error) {
|
|
||||||
h := fnv.New128()
|
|
||||||
h.Write([]byte(function))
|
|
||||||
buf := bytes.NewBuffer(nil)
|
|
||||||
b64 := base64.NewEncoder(base64.RawURLEncoding, buf)
|
|
||||||
if _, err := b64.Write(h.Sum(nil)); err != nil {
|
|
||||||
return "", fmt.Errorf("failed to encode function: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := b64.Close(); err != nil {
|
|
||||||
return "", fmt.Errorf("failed to complete encoding of function: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return strings.TrimPrefix(buf.String(), "_"), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func findGomod(wd string) (string, bool) {
|
|
||||||
gomodDir := wd
|
|
||||||
for {
|
|
||||||
gomodPath := path.Join(gomodDir, "go.mod")
|
|
||||||
f, err := os.Stat(gomodPath)
|
|
||||||
if err == nil && !f.IsDir() {
|
|
||||||
return gomodPath, true
|
|
||||||
}
|
|
||||||
|
|
||||||
if gomodDir == "/" {
|
|
||||||
return "", false
|
|
||||||
}
|
|
||||||
|
|
||||||
gomodDir = path.Dir(gomodDir)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func copyGomod(mn, dst, src string) error {
|
|
||||||
srcf, err := os.Open(src)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed to open file: %s; %w", src, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
defer srcf.Close()
|
|
||||||
dstf, err := os.Create(dst)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed to create file: %s; %w", dst, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
defer dstf.Close()
|
|
||||||
b, err := io.ReadAll(srcf)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed to read go.mod file %s: %w", src, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
s := string(b)
|
|
||||||
ss := strings.Split(s, "\n")
|
|
||||||
for i := range ss {
|
|
||||||
if strings.HasPrefix(ss[i], "module ") {
|
|
||||||
ss[i] = fmt.Sprintf("module %s", mn)
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if _, err := dstf.Write([]byte(strings.Join(ss, "\n"))); err != nil {
|
|
||||||
return fmt.Errorf("failed to write go.mod file %s: %w", dst, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func printFile(fn string, pkg, expression string) error {
|
|
||||||
f, err := os.Create(fn)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
defer f.Close()
|
|
||||||
fprintf := func(format string, args ...any) {
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
_, err = fmt.Fprintf(f, format, args...)
|
|
||||||
}
|
|
||||||
|
|
||||||
fprintf("package main\n")
|
|
||||||
if pkg != "" {
|
|
||||||
fprintf("import \"%s\"\n", pkg)
|
|
||||||
}
|
|
||||||
|
|
||||||
fprintf("import \"code.squareroundforest.org/arpio/wand\"\n")
|
|
||||||
fprintf("func main() {\n")
|
|
||||||
fprintf("wand.Exec(%s)\n", expression)
|
|
||||||
fprintf("}")
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
func Exec(o ExecOptions, function string, args ...string) error {
|
|
||||||
pkg, expression, err := splitFunction(function)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
functionHash, err := functionHash(function)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
cacheDir := o.CacheDir
|
|
||||||
if cacheDir == "" {
|
|
||||||
cacheDir = path.Join(os.Getenv("HOME"), ".wand")
|
|
||||||
}
|
|
||||||
|
|
||||||
functionDir := path.Join(cacheDir, functionHash)
|
|
||||||
if o.NoCache {
|
|
||||||
functionDir = path.Join(cacheDir, "tmp", functionHash)
|
|
||||||
}
|
|
||||||
|
|
||||||
if o.NoCache || o.ClearCache {
|
|
||||||
if err := os.RemoveAll(functionDir); err != nil {
|
|
||||||
return fmt.Errorf("failed to clean cache: %w", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := os.MkdirAll(functionDir, os.ModePerm); err != nil {
|
|
||||||
return fmt.Errorf("failed to ensure cache directory: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
wd, err := os.Getwd()
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("error identifying current directory: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
goGet := func(pkg string) error {
|
|
||||||
if err := execInternal("go get", pkg); err != nil {
|
|
||||||
return fmt.Errorf("failed to get go module: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := os.Chdir(functionDir); err != nil {
|
|
||||||
return fmt.Errorf("failed to switch to temporary directory: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
defer os.Chdir(wd)
|
|
||||||
gomodPath, hasGomod := findGomod(wd)
|
|
||||||
if hasGomod {
|
|
||||||
if err := copyGomod(functionHash, path.Join(functionDir, "go.mod"), gomodPath); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if err := execInternal("go mod init", functionHash); err != nil {
|
|
||||||
return fmt.Errorf("failed to initialize temporary module: %w", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// non-robust way of avoiding importing standard library packages:
|
|
||||||
if strings.Contains(pkg, ".") {
|
|
||||||
if err := goGet(pkg); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := goGet("code.squareroundforest.org/arpio/wand"); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
goFile := path.Join(functionDir, fmt.Sprintf("%s.go", functionHash))
|
|
||||||
if _, err := os.Stat(goFile); err != nil {
|
|
||||||
if err := printFile(goFile, pkg, expression); err != nil {
|
|
||||||
return fmt.Errorf("failed to create temporary go file: %w", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := execTransparent("go run", append([]string{functionDir}, args...)...); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if o.NoCache {
|
|
||||||
if err := os.RemoveAll(functionDir); err != nil {
|
|
||||||
return fmt.Errorf("failed to clean cache: %w", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
Loading…
Reference in New Issue
Block a user