apply bind
This commit is contained in:
parent
72083c1f33
commit
6d2d51d211
1
.gitignore
vendored
1
.gitignore
vendored
@ -1 +1,2 @@
|
||||
.build
|
||||
.cover
|
||||
|
||||
2
Makefile
2
Makefile
@ -29,7 +29,6 @@ iniparser.gen.go: ini.treerack
|
||||
docreflect.gen.go: $(SOURCES)
|
||||
go run script/docreflect/docs.go \
|
||||
wand \
|
||||
code.squareroundforest.org/arpio/docreflect/generate \
|
||||
code.squareroundforest.org/arpio/wand/tools \
|
||||
> docreflect.gen.go \
|
||||
|| rm -f docreflect.gen.go
|
||||
@ -44,6 +43,7 @@ install: .build/wand
|
||||
cp .build/wand ~/bin
|
||||
|
||||
clean:
|
||||
go clean ./...
|
||||
rm -rf .build
|
||||
rm -f docreflect.gen.go
|
||||
rm -f iniparser.gen.go
|
||||
|
||||
215
apply.go
215
apply.go
@ -1,188 +1,61 @@
|
||||
package wand
|
||||
|
||||
import (
|
||||
"github.com/iancoleman/strcase"
|
||||
"io"
|
||||
"reflect"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func ensurePointerAllocation(p reflect.Value, n int) {
|
||||
if p.IsNil() {
|
||||
p.Set(reflect.New(p.Type().Elem()))
|
||||
}
|
||||
|
||||
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) {
|
||||
switch v.Type().Kind() {
|
||||
case reflect.Pointer:
|
||||
ensurePointerAllocation(v, n)
|
||||
case reflect.Slice:
|
||||
ensureSliceAllocation(v, n)
|
||||
}
|
||||
}
|
||||
|
||||
func setPointerValue(p reflect.Value, v []value) {
|
||||
setFieldValue(p.Elem(), v)
|
||||
}
|
||||
|
||||
func setSliceValue(s reflect.Value, v []value) {
|
||||
for i := 0; i < s.Len(); i++ {
|
||||
setFieldValue(s.Index(i), v[i:i+1])
|
||||
}
|
||||
}
|
||||
|
||||
func setValue(f reflect.Value, v value) {
|
||||
if v.isBool {
|
||||
f.Set(reflect.ValueOf(v.boolean))
|
||||
return
|
||||
}
|
||||
|
||||
f.Set(reflect.ValueOf(scan(f.Type(), v.str)))
|
||||
}
|
||||
|
||||
func setFieldValue(field reflect.Value, v []value) {
|
||||
switch field.Kind() {
|
||||
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 bindKeyVals(receiver reflect.Value, keyVals map[string][]string) bool {
|
||||
v := make(map[string][]any)
|
||||
for name, values := range keyVals {
|
||||
for _, vi := range values {
|
||||
v[name] = append(v[name], vi)
|
||||
}
|
||||
}
|
||||
|
||||
u := bindFields(receiver, v)
|
||||
return len(v) > 0 && len(u) < len(v)
|
||||
}
|
||||
|
||||
func bindOptions(receiver reflect.Value, shortForms []string, o []option) bool {
|
||||
ms := make(map[string]string)
|
||||
for i := 0; i < len(shortForms); i += 2 {
|
||||
ms[shortForms[i]] = shortForms[i + 1]
|
||||
}
|
||||
|
||||
v := make(map[string][]any)
|
||||
for _, oi := range o {
|
||||
n := oi.name
|
||||
if oi.shortForm {
|
||||
n = ms[n]
|
||||
}
|
||||
|
||||
var val any
|
||||
if oi.value.isBool {
|
||||
val = oi.value.boolean
|
||||
} else {
|
||||
val = oi.value.str
|
||||
}
|
||||
|
||||
v[n] = append(v[n], val)
|
||||
}
|
||||
|
||||
u := bindFields(receiver, v)
|
||||
return len(v) > 0 && len(u) < len(v)
|
||||
}
|
||||
|
||||
func createStructArg(t reflect.Type, shortForms []string, c config, e env, o []option) (reflect.Value, bool) {
|
||||
tup := unpack(t)
|
||||
f := fields(tup)
|
||||
fn := make(map[string]bool)
|
||||
for _, fi := range f {
|
||||
fn[fi.name] = true
|
||||
}
|
||||
|
||||
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
|
||||
r := allocate(t)
|
||||
hasConfigMatches := bindKeyVals(r, c.values)
|
||||
hasEnvMatches := bindKeyVals(r, e.values)
|
||||
hasOptionMatches := bindOptions(r, shortForms, o)
|
||||
return r, hasConfigMatches || hasEnvMatches || hasOptionMatches
|
||||
}
|
||||
|
||||
func createPositional(t reflect.Type, v string) reflect.Value {
|
||||
if t.Kind() == reflect.Interface {
|
||||
return reflect.ValueOf(v)
|
||||
}
|
||||
|
||||
tup := unpack(t)
|
||||
sv := reflect.ValueOf(scan(tup, v))
|
||||
return pack(sv, t)
|
||||
r := allocate(t)
|
||||
bindScalar(r, v)
|
||||
return r
|
||||
}
|
||||
|
||||
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) {
|
||||
v := reflect.ValueOf(cmd.impl)
|
||||
v = unpack(v)
|
||||
v = unpackValue(v)
|
||||
t := v.Type()
|
||||
args := createArgs(stdin, stdout, t, cmd.shortForms, c, e, cl)
|
||||
out := v.Call(args)
|
||||
|
||||
380
command.go
380
command.go
@ -5,7 +5,7 @@ import (
|
||||
"fmt"
|
||||
"reflect"
|
||||
"regexp"
|
||||
"slices"
|
||||
"code.squareroundforest.org/arpio/bind"
|
||||
)
|
||||
|
||||
var commandNameExpression = regexp.MustCompile("^[a-zA-Z_][a-zA-Z_0-9]*$")
|
||||
@ -19,103 +19,41 @@ func wrap(impl any) Cmd {
|
||||
return Command("", impl)
|
||||
}
|
||||
|
||||
func validateFields(f []field, conf Config) error {
|
||||
func validateFields(f []bind.Field, conf Config) error {
|
||||
hasConfigFromOption := hasConfigFromOption(conf)
|
||||
mf := make(map[string]field)
|
||||
mf := make(map[string]bind.Field)
|
||||
for _, fi := range f {
|
||||
if ef, ok := mf[fi.name]; ok && !compatibleTypes(fi.typ, ef.typ) {
|
||||
return fmt.Errorf("duplicate fields with different types: %s", fi.name)
|
||||
if ef, ok := mf[fi.Name()]; ok && !compatibleTypes(fi.Type(), ef.Type()) {
|
||||
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")
|
||||
}
|
||||
|
||||
mf[fi.name] = fi
|
||||
mf[fi.Name()] = fi
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func validateParameter(visited map[reflect.Type]bool, t reflect.Type) error {
|
||||
switch t.Kind() {
|
||||
case reflect.Bool,
|
||||
reflect.Int,
|
||||
reflect.Int8,
|
||||
reflect.Int16,
|
||||
reflect.Int32,
|
||||
reflect.Int64,
|
||||
reflect.Uint,
|
||||
reflect.Uint8,
|
||||
reflect.Uint16,
|
||||
reflect.Uint32,
|
||||
reflect.Uint64,
|
||||
reflect.Float32,
|
||||
reflect.Float64,
|
||||
reflect.String:
|
||||
return 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 {
|
||||
func validatePositional(p []reflect.Type, variadic bool, min, max int) error {
|
||||
fixedPositional := len(p)
|
||||
if variadic {
|
||||
fixedPositional--
|
||||
}
|
||||
|
||||
if min > 0 && min < fixedPositional {
|
||||
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,
|
||||
fixedPositional,
|
||||
)
|
||||
}
|
||||
|
||||
if min > 0 && min > fixedPositional && !lastVariadic {
|
||||
if min > 0 && min > fixedPositional && !variadic {
|
||||
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,
|
||||
fixedPositional,
|
||||
)
|
||||
@ -123,7 +61,7 @@ func validatePositional(t reflect.Type, min, max int) error {
|
||||
|
||||
if max > 0 && max < fixedPositional {
|
||||
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,
|
||||
fixedPositional,
|
||||
)
|
||||
@ -131,7 +69,7 @@ func validatePositional(t reflect.Type, min, max int) error {
|
||||
|
||||
if min > 0 && max > 0 && min > max {
|
||||
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,
|
||||
max,
|
||||
)
|
||||
@ -140,65 +78,45 @@ func validatePositional(t reflect.Type, min, max int) error {
|
||||
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 {
|
||||
v := reflect.ValueOf(cmd.impl)
|
||||
v = unpack(v)
|
||||
t := v.Type()
|
||||
if t.Kind() != reflect.Func {
|
||||
return errors.New("command implementation not a function")
|
||||
if !isFunc(cmd.impl) {
|
||||
return errors.New("command implementation must be a function or a pointer to a function")
|
||||
}
|
||||
|
||||
s := structParameters(t)
|
||||
f, err := fieldsChecked(nil, s...)
|
||||
if err != nil {
|
||||
return err
|
||||
p := parameters(cmd.impl)
|
||||
for _, pi := range p {
|
||||
if !isReader(pi) && !isWriter(pi) && !bindable(pi) {
|
||||
return fmt.Errorf("unsupported parameter type: %s", pi.Name())
|
||||
}
|
||||
}
|
||||
|
||||
f := fields(cmd.impl)
|
||||
if err := validateFields(f, conf); err != nil {
|
||||
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 nil
|
||||
}
|
||||
|
||||
func validateShortForms(cmd Cmd, assignedShortForms map[string]string) 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 {
|
||||
func validateCommandTree(cmd Cmd, conf Config) error {
|
||||
if cmd.isHelp {
|
||||
return nil
|
||||
}
|
||||
@ -207,35 +125,25 @@ func validateCommandTree(cmd Cmd, conf Config, assignedShortForms map[string]str
|
||||
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 {
|
||||
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 err := validateImpl(cmd, conf); err != nil {
|
||||
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
|
||||
names := make(map[string]bool)
|
||||
for _, s := range cmd.subcommands {
|
||||
if s.name == "" {
|
||||
return fmt.Errorf("unnamed subcommand of: %s", cmd.name)
|
||||
if !commandNameExpression.MatchString(s.name) {
|
||||
return fmt.Errorf("command name is not a valid symbol: '%s'", cmd.name)
|
||||
}
|
||||
|
||||
if names[s.name] {
|
||||
@ -243,13 +151,9 @@ func validateCommandTree(cmd Cmd, conf Config, assignedShortForms map[string]str
|
||||
}
|
||||
|
||||
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 {
|
||||
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,
|
||||
s.name,
|
||||
)
|
||||
@ -262,36 +166,174 @@ func validateCommandTree(cmd Cmd, conf Config, assignedShortForms map[string]str
|
||||
if s.isDefault {
|
||||
hasDefault = true
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func allShortForms(cmd Cmd) []string {
|
||||
var sf []string
|
||||
for _, sc := range cmd.subcommands {
|
||||
sf = append(sf, allShortForms(sc)...)
|
||||
}
|
||||
|
||||
for i := 0; i < len(cmd.shortForms); i += 2 {
|
||||
sf = append(sf, cmd.shortForms[i])
|
||||
}
|
||||
|
||||
return sf
|
||||
}
|
||||
|
||||
func validateCommand(cmd Cmd, conf Config) error {
|
||||
assignedShortForms := make(map[string]string)
|
||||
if err := validateCommandTree(cmd, conf, assignedShortForms, true); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
asf := allShortForms(cmd)
|
||||
for _, sf := range asf {
|
||||
if _, ok := assignedShortForms[sf]; !ok {
|
||||
return fmt.Errorf("unassigned option short form: %s", sf)
|
||||
if err := validateCommandTree(s, conf); err != nil {
|
||||
return fmt.Errorf("%s: %w", s.name, err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func checkShortFormDefinition(existing map[string]string, short, long string) error {
|
||||
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 {
|
||||
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 {
|
||||
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)
|
||||
}
|
||||
|
||||
if err := checkShortFormDefinition(mapped, s, l); err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
if err := checkShortFormDefinition(unmapped, s, l); err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
_, 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
|
||||
}
|
||||
|
||||
if len(um) != 0 {
|
||||
return errors.New("unmapped short forms")
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"slices"
|
||||
"strconv"
|
||||
"strings"
|
||||
"unicode"
|
||||
"code.squareroundforest.org/arpio/bind"
|
||||
)
|
||||
|
||||
type value struct {
|
||||
@ -33,57 +33,6 @@ func stringValue(s string) value {
|
||||
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 {
|
||||
a := []rune(arg)
|
||||
if len(a) <= 2 {
|
||||
@ -325,15 +274,14 @@ func readArgs(boolOptions, args []string) commandLine {
|
||||
}
|
||||
|
||||
func hasHelpOption(cmd Cmd, o []option) bool {
|
||||
var mf map[string][]field
|
||||
var mf map[string][]bind.Field
|
||||
if cmd.impl != nil {
|
||||
mf = mapFields(cmd.impl)
|
||||
}
|
||||
|
||||
sf := make(map[string]bool)
|
||||
sf := make(map[string]string)
|
||||
for i := 0; i < len(cmd.shortForms); i += 2 {
|
||||
s := cmd.shortForms[i+1]
|
||||
sf[s] = true
|
||||
sf[cmd.shortForms[i]] = cmd.shortForms[i + 1]
|
||||
}
|
||||
|
||||
for _, oi := range o {
|
||||
@ -341,14 +289,22 @@ func hasHelpOption(cmd Cmd, o []option) bool {
|
||||
continue
|
||||
}
|
||||
|
||||
if oi.name == "help" {
|
||||
if _, ok := mf["help"]; !ok {
|
||||
return true
|
||||
n := oi.name
|
||||
if oi.shortForm && n == "h" {
|
||||
l, ok := sf["h"]
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
if l != "help" {
|
||||
continue
|
||||
}
|
||||
|
||||
n = "help"
|
||||
}
|
||||
|
||||
if oi.shortForm && oi.name == "h" {
|
||||
if !sf["h"] {
|
||||
if n == "help" {
|
||||
if _, ok := mf["help"]; !ok {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
@ -111,6 +111,7 @@ func readConfigFile(cmd Cmd, conf Config) (config, error) {
|
||||
name := strcase.ToKebab(key)
|
||||
c.originalNames[name] = key
|
||||
if !hasValue {
|
||||
delete(c.values, name)
|
||||
c.discard = append(c.discard, name)
|
||||
continue
|
||||
}
|
||||
|
||||
@ -1,62 +1,31 @@
|
||||
/*
|
||||
Generated with https://code.squareroundforest.org/arpio/docreflect
|
||||
*/
|
||||
|
||||
|
||||
package wand
|
||||
import "code.squareroundforest.org/arpio/docreflect"
|
||||
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", "\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.CacheDir", "")
|
||||
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.Man", "\nfunc(out, commandDir)")
|
||||
docreflect.Register("code.squareroundforest.org/arpio/wand/tools.Markdown", "\nfunc(out, commandDir)")
|
||||
docreflect.Register("code.squareroundforest.org/arpio/wand/tools.copyGomod", "\nfunc(mn, dst, src)")
|
||||
docreflect.Register("code.squareroundforest.org/arpio/wand/tools.Markdown", "\nfunc(out, o, commandDir)")
|
||||
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.execInternal", "\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.findGomod", "\nfunc(wd)")
|
||||
docreflect.Register("code.squareroundforest.org/arpio/wand/tools.functionHash", "\nfunc(function)")
|
||||
docreflect.Register("code.squareroundforest.org/arpio/wand/tools.printFile", "\nfunc(fn, pkg, expression)")
|
||||
docreflect.Register("code.squareroundforest.org/arpio/wand/tools.splitFunction", "\nfunc(function)")
|
||||
docreflect.Register("code.squareroundforest.org/arpio/wand/tools.hash", "\nfunc(expression, imports, inlineImports)")
|
||||
docreflect.Register("code.squareroundforest.org/arpio/wand/tools.printGoFile", "\nfunc(fn, expression, imports, inlineImports)")
|
||||
docreflect.Register("code.squareroundforest.org/arpio/wand/tools.readExec", "\nfunc(o, stdin)")
|
||||
}
|
||||
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
|
||||
}
|
||||
|
||||
if os.Getenv("wandgenerate") == "man" {
|
||||
if os.Getenv("_wandgenerate") == "man" {
|
||||
if err := generateMan(stdout, cmd, conf); err != nil {
|
||||
fmt.Fprintln(stderr, err)
|
||||
exit(1)
|
||||
@ -27,8 +27,8 @@ func exec(stdin io.Reader, stdout, stderr io.Writer, exit func(int), cmd Cmd, co
|
||||
return
|
||||
}
|
||||
|
||||
if os.Getenv("wandgenerate") == "markdown" {
|
||||
level, _ := strconv.Atoi(os.Getenv("wandmarkdownlevel"))
|
||||
if os.Getenv("_wandgenerate") == "markdown" {
|
||||
level, _ := strconv.Atoi(os.Getenv("_wandmarkdownlevel"))
|
||||
if err := generateMarkdown(stdout, cmd, conf, level); err != nil {
|
||||
fmt.Fprintln(stderr, err)
|
||||
exit(1)
|
||||
|
||||
33
format.go
33
format.go
@ -70,21 +70,6 @@ func lines(s string) string {
|
||||
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 {
|
||||
p := paragraphs(s)
|
||||
pp := strings.Split(p, "\n\n")
|
||||
@ -105,6 +90,21 @@ func manLines(s string) string {
|
||||
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 {
|
||||
var (
|
||||
rr []rune
|
||||
@ -163,8 +163,9 @@ func escapeMD(s string) string {
|
||||
rr = append(rr, ri)
|
||||
default:
|
||||
rr = append(rr, ri)
|
||||
lastDigit = ri >= 0 && ri <= 9
|
||||
}
|
||||
|
||||
lastDigit = ri >= 0 && ri <= 9
|
||||
}
|
||||
|
||||
return string(rr)
|
||||
|
||||
11
go.mod
11
go.mod
@ -1,12 +1,15 @@
|
||||
module code.squareroundforest.org/arpio/wand
|
||||
|
||||
go 1.24.6
|
||||
go 1.25.0
|
||||
|
||||
require (
|
||||
code.squareroundforest.org/arpio/docreflect v0.0.0-20250823192303-755a103f3788
|
||||
code.squareroundforest.org/arpio/notation v0.0.0-20241225183158-af3bd591a174
|
||||
code.squareroundforest.org/arpio/docreflect v0.0.0-20250831183400-d26ecc663a30
|
||||
code.squareroundforest.org/arpio/notation v0.0.0-20250826181910-5140794b16b2
|
||||
code.squareroundforest.org/arpio/treerack v0.0.0-20250820014405-1d956dcc6610
|
||||
github.com/iancoleman/strcase v0.3.0
|
||||
)
|
||||
|
||||
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/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/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/go.mod h1:9XhPcVt1Y1M609z02lHvEcp00dwPD9NUCoVxS2TpcH8=
|
||||
github.com/aryszka/notation v0.0.0-20230129164653-172017dde5e4 h1:JzqT9RArcw2sD4QPAyTss/sHaCZvCv+91DDJPZOrShw=
|
||||
|
||||
71
help.go
71
help.go
@ -2,6 +2,7 @@ package wand
|
||||
|
||||
import (
|
||||
"code.squareroundforest.org/arpio/docreflect"
|
||||
"code.squareroundforest.org/arpio/bind"
|
||||
"fmt"
|
||||
"io"
|
||||
"reflect"
|
||||
@ -67,10 +68,11 @@ type (
|
||||
}
|
||||
)
|
||||
|
||||
func help() Cmd {
|
||||
func help(sf []string) Cmd {
|
||||
return Cmd{
|
||||
name: "help",
|
||||
isHelp: true,
|
||||
name: "help",
|
||||
isHelp: true,
|
||||
shortForms: sf,
|
||||
}
|
||||
}
|
||||
|
||||
@ -84,7 +86,7 @@ func insertHelp(cmd Cmd) Cmd {
|
||||
}
|
||||
|
||||
if !hasHelpCmd && cmd.version == "" {
|
||||
cmd.subcommands = append(cmd.subcommands, help())
|
||||
cmd.subcommands = append(cmd.subcommands, help(cmd.shortForms))
|
||||
}
|
||||
|
||||
return cmd
|
||||
@ -127,11 +129,7 @@ func hasOptions(cmd Cmd) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
v := reflect.ValueOf(cmd.impl)
|
||||
t := v.Type()
|
||||
t = unpack(t)
|
||||
s := structParameters(t)
|
||||
return len(fields(s...)) > 0
|
||||
return len(fields(cmd.impl)) > 0
|
||||
}
|
||||
|
||||
func allCommands(cmd doc) []doc {
|
||||
@ -147,17 +145,17 @@ func allCommands(cmd doc) []doc {
|
||||
return commands
|
||||
}
|
||||
|
||||
func functionParams(v reflect.Value, skip []int) ([]string, []string) {
|
||||
names := docreflect.FunctionParams(v)
|
||||
|
||||
var types []reflect.Kind
|
||||
for i := 0; i < v.Type().NumIn(); i++ {
|
||||
types = append(types, v.Type().In(i).Kind())
|
||||
func functionParams(v any, indices []int) ([]string, []string) {
|
||||
var names []string
|
||||
r := reflect.ValueOf(v)
|
||||
allNames := docreflect.FunctionParams(r)
|
||||
for _, i := range indices {
|
||||
names = append(names, allNames[i])
|
||||
}
|
||||
|
||||
for _, i := range skip {
|
||||
names = append(names[:i], names[i+1:]...)
|
||||
types = append(types[:i], types[i+1:]...)
|
||||
var types []reflect.Kind
|
||||
for _, i := range indices {
|
||||
types = append(types, r.Type().In(i).Kind())
|
||||
}
|
||||
|
||||
var stypes []string
|
||||
@ -173,24 +171,22 @@ func constructArguments(cmd Cmd) argumentSet {
|
||||
return argumentSet{}
|
||||
}
|
||||
|
||||
v := unpack(reflect.ValueOf(cmd.impl))
|
||||
t := v.Type()
|
||||
p := positionalParameters(t)
|
||||
ior, iow := ioParameters(p)
|
||||
count := len(p) - len(ior) - len(iow)
|
||||
names, types := functionParams(v, append(ior, iow...))
|
||||
if len(names) < count {
|
||||
p, variadic := positional(cmd.impl)
|
||||
pi := positionalIndices(cmd.impl)
|
||||
ior, iow := ioParameters(cmd.impl)
|
||||
names, types := functionParams(cmd.impl, pi)
|
||||
if len(names) < len(p) {
|
||||
names = nil
|
||||
for i := 0; i < count; i++ {
|
||||
for i := 0; i < len(p); i++ {
|
||||
names = append(names, fmt.Sprintf("arg%d", i))
|
||||
}
|
||||
}
|
||||
|
||||
return argumentSet{
|
||||
count: count,
|
||||
count: len(p),
|
||||
names: names,
|
||||
types: types,
|
||||
variadic: t.IsVariadic(),
|
||||
variadic: variadic,
|
||||
usesStdin: len(ior) > 0,
|
||||
usesStdout: len(iow) > 0,
|
||||
minPositional: cmd.minPositional,
|
||||
@ -230,13 +226,12 @@ func constructOptions(cmd Cmd, hasConfigFromOption bool) []docOption {
|
||||
sf[l] = append(sf[l], s)
|
||||
}
|
||||
|
||||
t := unpack(reflect.ValueOf(cmd.impl).Type())
|
||||
s := structParameters(t)
|
||||
s := structParameters(cmd.impl)
|
||||
d := make(map[string]string)
|
||||
for _, si := range s {
|
||||
f := fields(si)
|
||||
f := structFields(si)
|
||||
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 {
|
||||
opt := docOption{
|
||||
name: name,
|
||||
typ: strings.ToLower(fmt.Sprint(fi[0].typ.Kind())),
|
||||
typ: scalarTypeString(fi[0].Type()),
|
||||
description: d[name],
|
||||
shortNames: sf[name],
|
||||
isBool: fi[0].typ.Kind() == reflect.Bool,
|
||||
isBool: fi[0].Type() == bind.Bool,
|
||||
}
|
||||
|
||||
for _, fii := range fi {
|
||||
if fii.acceptsMultiple {
|
||||
if fii.List() {
|
||||
opt.acceptsMultiple = true
|
||||
}
|
||||
}
|
||||
@ -292,15 +287,13 @@ func constructConfigDocs(cmd Cmd, conf Config) []docConfig {
|
||||
}
|
||||
|
||||
func constructDoc(cmd Cmd, conf Config, fullCommand []string) doc {
|
||||
hasConfigFromOption := hasConfigFromOption(conf)
|
||||
|
||||
var subcommands []doc
|
||||
for _, sc := range cmd.subcommands {
|
||||
subcommands = append(subcommands, constructDoc(sc, conf, append(fullCommand, sc.name)))
|
||||
}
|
||||
|
||||
var hasBoolOptions, hasListOptions bool
|
||||
options := constructOptions(cmd, hasConfigFromOption)
|
||||
options := constructOptions(cmd, hasConfigFromOption(conf))
|
||||
for _, o := range options {
|
||||
if o.isBool {
|
||||
hasBoolOptions = true
|
||||
@ -322,7 +315,7 @@ func constructDoc(cmd Cmd, conf Config, fullCommand []string) doc {
|
||||
isHelp: cmd.isHelp,
|
||||
isVersion: cmd.version != "",
|
||||
hasHelpSubcommand: hasHelpSubcommand(cmd),
|
||||
hasHelpOption: hasCustomHelpOption(cmd),
|
||||
hasHelpOption: !hasCustomHelpOption(cmd),
|
||||
options: options,
|
||||
hasBoolOptions: hasBoolOptions,
|
||||
hasListOptions: hasListOptions,
|
||||
|
||||
383
iniparser.gen.go
383
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 (
|
||||
"fmt"
|
||||
"reflect"
|
||||
"slices"
|
||||
"code.squareroundforest.org/arpio/bind"
|
||||
)
|
||||
|
||||
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 {
|
||||
if len(values) > 1 && !fi.acceptsMultiple {
|
||||
if len(values) > 1 && !fi.List() {
|
||||
return fmt.Errorf(
|
||||
"expected only one value, received %d, as environment value, %s",
|
||||
"expected only one value, received %d, for %s",
|
||||
len(values),
|
||||
originalNames[name],
|
||||
)
|
||||
}
|
||||
|
||||
for _, v := range values {
|
||||
if !canScan(fi.typ, v) {
|
||||
return fmt.Errorf(
|
||||
"environment variable cannot be applied, type mismatch: %s",
|
||||
originalNames[name],
|
||||
)
|
||||
if !canScan(fi.Type(), v) {
|
||||
return fmt.Errorf("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 {
|
||||
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 {
|
||||
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 {
|
||||
@ -64,27 +69,25 @@ func validateOptions(cmd Cmd, o []option, conf Config) error {
|
||||
mo[n] = append(mo[n], oi)
|
||||
}
|
||||
|
||||
hasConfigOption := hasConfigFromOption(conf)
|
||||
mf := mapFields(cmd.impl)
|
||||
if hasConfigFromOption(conf) {
|
||||
mf["config"] = []field{{
|
||||
acceptsMultiple: true,
|
||||
typ: reflect.TypeFor[string](),
|
||||
}}
|
||||
}
|
||||
|
||||
for n, os := range mo {
|
||||
en := "--" + n
|
||||
if sn, ok := ml[n]; ok {
|
||||
en += ", -" + sn
|
||||
}
|
||||
|
||||
if hasConfigOption && n == "config" {
|
||||
continue
|
||||
}
|
||||
|
||||
f := mf[n]
|
||||
if len(f) == 0 {
|
||||
return fmt.Errorf("option not supported: %s", en)
|
||||
}
|
||||
|
||||
for _, fi := range f {
|
||||
if len(os) > 1 && !fi.acceptsMultiple {
|
||||
if len(os) > 1 && !fi.List() {
|
||||
return fmt.Errorf(
|
||||
"expected only one value, received %d, as option, %s",
|
||||
len(os),
|
||||
@ -93,14 +96,14 @@ func validateOptions(cmd Cmd, o []option, conf Config) error {
|
||||
}
|
||||
|
||||
for _, oi := range os {
|
||||
if oi.value.isBool && fi.typ.Kind() != reflect.Bool {
|
||||
if oi.value.isBool && fi.Type() != bind.Bool {
|
||||
return fmt.Errorf(
|
||||
"received boolean value for field that does not accept it: %s",
|
||||
en,
|
||||
)
|
||||
}
|
||||
|
||||
if !oi.value.isBool && !canScan(fi.typ, oi.value.str) {
|
||||
if !oi.value.isBool && !canScan(fi.Type(), oi.value.str) {
|
||||
return fmt.Errorf(
|
||||
"option cannot be applied, type mismatch: %s",
|
||||
en,
|
||||
@ -114,20 +117,10 @@ func validateOptions(cmd Cmd, o []option, conf Config) error {
|
||||
}
|
||||
|
||||
func validatePositionalArgs(cmd Cmd, a []string) error {
|
||||
v := reflect.ValueOf(cmd.impl)
|
||||
v = unpack(v)
|
||||
t := v.Type()
|
||||
p := positionalParameters(t)
|
||||
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 {
|
||||
p, variadic := positional(cmd.impl)
|
||||
min := len(p)
|
||||
max := len(p)
|
||||
if variadic {
|
||||
min--
|
||||
max = -1
|
||||
}
|
||||
@ -149,26 +142,18 @@ func validatePositionalArgs(cmd Cmd, a []string) error {
|
||||
}
|
||||
|
||||
for i, ai := range a {
|
||||
if slices.Contains(ior, i) || slices.Contains(iow, i) {
|
||||
continue
|
||||
}
|
||||
|
||||
var pi reflect.Type
|
||||
if i >= length {
|
||||
pi = p[length-1]
|
||||
if i >= len(p) {
|
||||
pi = p[len(p)-1]
|
||||
} else {
|
||||
pi = p[i]
|
||||
}
|
||||
|
||||
if pi.Kind() == reflect.Interface {
|
||||
continue
|
||||
}
|
||||
|
||||
if !canScan(pi, ai) {
|
||||
if !canScanType(pi, ai) {
|
||||
return fmt.Errorf(
|
||||
"cannot apply positional argument at index %d, expecting %v",
|
||||
"cannot apply positional argument at index %d, expecting %s",
|
||||
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 {
|
||||
if len(f)%2 != 0 {
|
||||
f = append(f, "")
|
||||
}
|
||||
|
||||
cmd.shortForms = append(cmd.shortForms, f...)
|
||||
for i := range cmd.subcommands {
|
||||
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
|
||||
93
output.go
93
output.go
@ -7,59 +7,60 @@ import (
|
||||
"reflect"
|
||||
)
|
||||
|
||||
func printOutput(w io.Writer, o []any) error {
|
||||
wraperr := func(err error) error {
|
||||
return fmt.Errorf("error copying output: %w", err)
|
||||
func fprintOne(out io.Writer, v any) error {
|
||||
reader, ok := v.(io.Reader)
|
||||
if ok {
|
||||
_, err := io.Copy(out, reader)
|
||||
return err
|
||||
}
|
||||
|
||||
for _, oi := range o {
|
||||
r, ok := oi.(io.Reader)
|
||||
if ok {
|
||||
if _, err := io.Copy(w, r); err != nil {
|
||||
return wraperr(err)
|
||||
}
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
t := reflect.TypeOf(oi)
|
||||
r := reflect.ValueOf(v)
|
||||
if r.IsValid() {
|
||||
t := r.Type()
|
||||
if t.Implements(reflect.TypeFor[fmt.Stringer]()) {
|
||||
if _, err := fmt.Fprintln(w, oi); err != nil {
|
||||
return wraperr(err)
|
||||
}
|
||||
|
||||
continue
|
||||
_, err := fmt.Fprintln(out, r.Interface())
|
||||
return err
|
||||
}
|
||||
|
||||
t = unpack(t, reflect.Pointer)
|
||||
switch t.Kind() {
|
||||
case reflect.Bool,
|
||||
reflect.Int,
|
||||
reflect.Int8,
|
||||
reflect.Int16,
|
||||
reflect.Int32,
|
||||
reflect.Int64,
|
||||
reflect.Uint,
|
||||
reflect.Uint8,
|
||||
reflect.Uint16,
|
||||
reflect.Uint32,
|
||||
reflect.Uint64,
|
||||
reflect.Uintptr,
|
||||
reflect.Float32,
|
||||
reflect.Float64,
|
||||
reflect.String,
|
||||
reflect.UnsafePointer:
|
||||
if _, err := fmt.Fprintln(w, oi); err != nil {
|
||||
return wraperr(err)
|
||||
}
|
||||
default:
|
||||
if _, err := notation.Fprintwt(w, oi); err != nil {
|
||||
return wraperr(err)
|
||||
if t.Kind() == reflect.Slice {
|
||||
for i := 0; i < r.Len(); i++ {
|
||||
if err := fprintOne(out, r.Index(i).Interface()); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if _, err := fmt.Fprintln(w); err != nil {
|
||||
return wraperr(err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
switch r.Kind() {
|
||||
case reflect.Bool,
|
||||
reflect.Int,
|
||||
reflect.Int8,
|
||||
reflect.Int16,
|
||||
reflect.Int32,
|
||||
reflect.Int64,
|
||||
reflect.Uint,
|
||||
reflect.Uint8,
|
||||
reflect.Uint16,
|
||||
reflect.Uint32,
|
||||
reflect.Uint64,
|
||||
reflect.Uintptr,
|
||||
reflect.Float32,
|
||||
reflect.Float64,
|
||||
reflect.String:
|
||||
_, err := fmt.Fprintln(out, v)
|
||||
return err
|
||||
default:
|
||||
_, err := notation.Fprintlnwt(out, v)
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
func printOutput(out io.Writer, o []any) error {
|
||||
for _, oi := range o {
|
||||
if err := fprintOne(out, oi); err != nil {
|
||||
return fmt.Errorf("error displaying output: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
619
reflect.go
619
reflect.go
@ -1,366 +1,379 @@
|
||||
package wand
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/iancoleman/strcase"
|
||||
"io"
|
||||
"reflect"
|
||||
"slices"
|
||||
"strconv"
|
||||
"code.squareroundforest.org/arpio/bind"
|
||||
"io"
|
||||
"time"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type packedKind[T any] interface {
|
||||
Kind() reflect.Kind
|
||||
Elem() T
|
||||
func filter[T any](list []T, predicate func(T) bool) []T {
|
||||
var filtered []T
|
||||
for _, item := range list {
|
||||
if predicate(item) {
|
||||
filtered = append(filtered, item)
|
||||
}
|
||||
}
|
||||
|
||||
return filtered
|
||||
}
|
||||
|
||||
type field struct {
|
||||
name string
|
||||
path []string
|
||||
typ reflect.Type
|
||||
acceptsMultiple bool
|
||||
func not[T any](p func(T) bool) func(T) bool {
|
||||
return func(v T) bool {
|
||||
return !p(v)
|
||||
}
|
||||
}
|
||||
|
||||
var (
|
||||
readerType = reflect.TypeFor[io.Reader]()
|
||||
writerType = reflect.TypeFor[io.Writer]()
|
||||
)
|
||||
func and[T any](p ...func(T) bool) func(T) bool {
|
||||
return func(v T) bool {
|
||||
for _, pi := range p {
|
||||
if !pi(v) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func pack(v reflect.Value, t reflect.Type) reflect.Value {
|
||||
if v.Type() == t {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
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() {
|
||||
case reflect.Pointer, reflect.Slice:
|
||||
return unpackTypeChecked(visited, t.Elem())
|
||||
default:
|
||||
return t
|
||||
}
|
||||
}
|
||||
|
||||
func unpackType(t reflect.Type) reflect.Type {
|
||||
return unpackTypeChecked(nil, t)
|
||||
}
|
||||
|
||||
func unpackValueChecked(visited map[uintptr]bool, v reflect.Value) reflect.Value {
|
||||
if !v.IsValid() {
|
||||
return v
|
||||
}
|
||||
|
||||
if t.Kind() == reflect.Pointer {
|
||||
pv := pack(v, t.Elem())
|
||||
p := reflect.New(t.Elem())
|
||||
p.Elem().Set(pv)
|
||||
return p
|
||||
}
|
||||
switch v.Kind() {
|
||||
case reflect.Pointer:
|
||||
p := v.Pointer()
|
||||
if visited[p] {
|
||||
return v
|
||||
}
|
||||
|
||||
iv := pack(v, t.Elem())
|
||||
s := reflect.MakeSlice(t, 1, 1)
|
||||
s.Index(0).Set(iv)
|
||||
return s
|
||||
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 unpack[T packedKind[T]](p T, kinds ...reflect.Kind) T {
|
||||
if len(kinds) == 0 {
|
||||
kinds = []reflect.Kind{reflect.Pointer, reflect.Slice}
|
||||
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
|
||||
}
|
||||
|
||||
if slices.Contains(kinds, p.Kind()) {
|
||||
return unpack(p.Elem(), kinds...)
|
||||
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 isReader(t reflect.Type) bool {
|
||||
return unpack(t) == readerType
|
||||
func structParameters(f any) []reflect.Type {
|
||||
return filter(parameters(f), isStruct)
|
||||
}
|
||||
|
||||
func isWriter(t reflect.Type) bool {
|
||||
return unpack(t) == writerType
|
||||
func structFields(s reflect.Type) []bind.Field {
|
||||
s = unpackType(s)
|
||||
v := reflect.Zero(s)
|
||||
return bind.FieldValues(v)
|
||||
}
|
||||
|
||||
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) {
|
||||
bitSize := byteSize * 8
|
||||
switch {
|
||||
case strings.HasPrefix(s, "0b"):
|
||||
return strconv.ParseUint(s[2:], 2, bitSize)
|
||||
case strings.HasPrefix(s, "0x"):
|
||||
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 {
|
||||
switch t.Kind() {
|
||||
case reflect.Bool:
|
||||
_, err := strconv.ParseBool(s)
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
func scan(t reflect.Type, s string) any {
|
||||
p := reflect.New(t)
|
||||
switch t.Kind() {
|
||||
case reflect.Bool:
|
||||
v, _ := strconv.ParseBool(s)
|
||||
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:
|
||||
p.Elem().Set(reflect.ValueOf(s).Convert(t))
|
||||
func fields(f any) []bind.Field {
|
||||
var fields []bind.Field
|
||||
s := structParameters(fields)
|
||||
for _, si := range s {
|
||||
fields = append(fields, structFields(si)...)
|
||||
}
|
||||
|
||||
return p.Elem().Interface()
|
||||
return fields
|
||||
}
|
||||
|
||||
func fieldsChecked(visited map[reflect.Type]bool, s ...reflect.Type) ([]field, error) {
|
||||
if len(s) == 0 {
|
||||
return nil, nil
|
||||
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)
|
||||
}
|
||||
|
||||
var (
|
||||
anonFields []field
|
||||
plainFields []field
|
||||
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++ {
|
||||
sf := s[0].Field(i)
|
||||
sft := sf.Type
|
||||
am := acceptsMultiple(sft)
|
||||
sft = unpack(sft)
|
||||
sfn := sf.Name
|
||||
sfn = strcase.ToKebab(sfn)
|
||||
switch sft.Kind() {
|
||||
case reflect.Bool,
|
||||
reflect.Int,
|
||||
reflect.Int8,
|
||||
reflect.Int16,
|
||||
reflect.Int32,
|
||||
reflect.Int64,
|
||||
reflect.Uint,
|
||||
reflect.Uint8,
|
||||
reflect.Uint16,
|
||||
reflect.Uint32,
|
||||
reflect.Uint64,
|
||||
reflect.Float32,
|
||||
reflect.Float64,
|
||||
reflect.String:
|
||||
plainFields = append(plainFields, field{
|
||||
name: sfn,
|
||||
path: []string{sf.Name},
|
||||
typ: sft,
|
||||
acceptsMultiple: am,
|
||||
})
|
||||
case reflect.Interface:
|
||||
if sft.NumMethod() == 0 {
|
||||
plainFields = append(plainFields, field{
|
||||
name: sfn,
|
||||
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
|
||||
r := reflect.ValueOf(f)
|
||||
r = unpackValue(r)
|
||||
t := r.Type()
|
||||
return p, t.IsVariadic()
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
func positionalIndices(f any) []int {
|
||||
r := reflect.ValueOf(f)
|
||||
r = unpackValue(r)
|
||||
if r.Kind() != reflect.Func {
|
||||
return nil
|
||||
}
|
||||
|
||||
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
|
||||
var indices []int
|
||||
t := r.Type()
|
||||
for i := 0; i < t.NumIn(); i++ {
|
||||
p := t.In(i)
|
||||
p = unpack(p)
|
||||
if f(p) {
|
||||
s = append(s, p)
|
||||
if isTime(p) || isStruct(p) || isReader(p) || isWriter(p) {
|
||||
continue
|
||||
}
|
||||
|
||||
indices = append(indices, i)
|
||||
}
|
||||
|
||||
return s
|
||||
return indices
|
||||
}
|
||||
|
||||
func positionalParameters(t reflect.Type) []reflect.Type {
|
||||
return filterParameters(t, func(p reflect.Type) bool {
|
||||
return p.Kind() != reflect.Struct
|
||||
})
|
||||
func ioParameters(f any) ([]reflect.Type, []reflect.Type) {
|
||||
p := parameters(f)
|
||||
return filter(p, isReader), filter(p, isWriter)
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
func bindable(t reflect.Type) bool {
|
||||
if t == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
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 {
|
||||
if isTime(t) {
|
||||
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:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func acceptsMultiple(t reflect.Type) bool {
|
||||
if t.Kind() == reflect.Slice {
|
||||
if isStruct(t) {
|
||||
return true
|
||||
}
|
||||
|
||||
switch t.Kind() {
|
||||
case reflect.Pointer:
|
||||
return acceptsMultiple(t.Elem())
|
||||
case reflect.Bool,
|
||||
reflect.Int,
|
||||
reflect.Int8,
|
||||
reflect.Int16,
|
||||
reflect.Int32,
|
||||
reflect.Int64,
|
||||
reflect.Uint,
|
||||
reflect.Uint8,
|
||||
reflect.Uint16,
|
||||
reflect.Uint32,
|
||||
reflect.Uint64,
|
||||
reflect.Float32,
|
||||
reflect.Float64,
|
||||
reflect.String:
|
||||
return true
|
||||
case reflect.Interface:
|
||||
return t.NumMethod() == 0
|
||||
case reflect.Slice:
|
||||
return bindable(t.Elem())
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func scalarTypeString(t bind.Scalar) string {
|
||||
r := reflect.TypeOf(t)
|
||||
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() {
|
||||
case reflect.Pointer:
|
||||
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:
|
||||
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
|
||||
|
||||
import (
|
||||
"code.squareroundforest.org/arpio/docreflect/generate"
|
||||
"log"
|
||||
"os"
|
||||
"code.squareroundforest.org/arpio/wand/tools"
|
||||
)
|
||||
|
||||
func main() {
|
||||
@ -11,7 +11,7 @@ func main() {
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
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