apply bind

This commit is contained in:
Arpad Ryszka 2025-09-01 02:07:48 +02:00
parent 72083c1f33
commit 6d2d51d211
26 changed files with 1238 additions and 2736 deletions

1249
.cover

File diff suppressed because it is too large Load Diff

1
.gitignore vendored
View File

@ -1 +1,2 @@
.build
.cover

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

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

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

File diff suppressed because one or more lines are too long

View File

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

View File

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

View File

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

3
readme.md Normal file
View File

@ -0,0 +1,3 @@
# Wand
*Made in Berlin, DE*

View File

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

View File

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

View File

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