interim checkin
This commit is contained in:
parent
0131d6d955
commit
f206c694b2
3
.gitignore
vendored
Normal file
3
.gitignore
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
iniparser.go
|
||||
docreflect.go
|
||||
.bin
|
||||
34
Makefile
34
Makefile
@ -1,14 +1,17 @@
|
||||
SOURCES = $(shell find . -name "*.go")
|
||||
SOURCES = $(shell find . -name "*.go" | grep -v iniparser.go | grep -v docreflect.go)
|
||||
|
||||
default: build
|
||||
|
||||
build: $(SOURCES)
|
||||
go build ./...
|
||||
lib: $(SOURCES) iniparser.go docreflect.go
|
||||
go build
|
||||
go build ./tools
|
||||
|
||||
check: $(SOURCES)
|
||||
build: lib wand
|
||||
|
||||
check: $(SOURCES) build
|
||||
go test -count 1 ./...
|
||||
|
||||
.cover: $(SOURCES)
|
||||
.cover: $(SOURCES) build
|
||||
go test -count 1 -coverprofile .cover ./...
|
||||
|
||||
cover: .cover
|
||||
@ -17,5 +20,24 @@ cover: .cover
|
||||
showcover: .cover
|
||||
go tool cover -html .cover
|
||||
|
||||
fmt: $(SOURCES)
|
||||
fmt: $(SOURCES) iniparser.go
|
||||
go fmt ./...
|
||||
|
||||
iniparser.go: ini.treerack
|
||||
go run script/ini-parser/parser.go wand < ini.treerack > iniparser.go || rm iniparser.go
|
||||
|
||||
docreflect.go: $(SOURCES)
|
||||
go run script/docreflect/docs.go \
|
||||
wand \
|
||||
code.squareroundforest.org/arpio/docreflect/generate \
|
||||
code.squareroundforest.org/arpio/wand/tools \
|
||||
> docreflect.go
|
||||
|
||||
.bin:
|
||||
mkdir -p .bin
|
||||
|
||||
wand: $(SOURCES) iniparser.go docreflect.go .bin
|
||||
go build -o .bin/wand ./cmd/wand
|
||||
|
||||
install: wand
|
||||
cp .bin/wand ~/bin
|
||||
|
||||
36
apply.go
36
apply.go
@ -2,9 +2,9 @@ package wand
|
||||
|
||||
import (
|
||||
"github.com/iancoleman/strcase"
|
||||
"io"
|
||||
"reflect"
|
||||
"strings"
|
||||
"os"
|
||||
)
|
||||
|
||||
func ensurePointerAllocation(p reflect.Value, n int) {
|
||||
@ -95,7 +95,7 @@ func setField(s reflect.Value, name string, v []value) {
|
||||
}
|
||||
}
|
||||
|
||||
func createStructArg(t reflect.Type, shortForms []string, e env, o []option) (reflect.Value, bool) {
|
||||
func createStructArg(t reflect.Type, shortForms []string, c config, e env, o []option) (reflect.Value, bool) {
|
||||
tup := unpack(t)
|
||||
f := fields(tup)
|
||||
fn := make(map[string]bool)
|
||||
@ -119,6 +119,13 @@ func createStructArg(t reflect.Type, shortForms []string, e env, o []option) (re
|
||||
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] {
|
||||
@ -133,11 +140,20 @@ func createStructArg(t reflect.Type, shortForms []string, e env, o []option) (re
|
||||
}
|
||||
}
|
||||
|
||||
if len(foundEnv) == 0 && len(foundOptions) == 0 {
|
||||
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] {
|
||||
@ -165,7 +181,7 @@ func createPositional(t reflect.Type, v string) reflect.Value {
|
||||
return pack(sv, t)
|
||||
}
|
||||
|
||||
func createArgs(t reflect.Type, shortForms []string, e env, cl commandLine) []reflect.Value {
|
||||
func createArgs(stdin io.Reader, stdout io.Writer, t reflect.Type, shortForms []string, c config, e env, cl commandLine) []reflect.Value {
|
||||
var args []reflect.Value
|
||||
positional := cl.positional
|
||||
for i := 0; i < t.NumIn(); i++ {
|
||||
@ -176,15 +192,15 @@ func createArgs(t reflect.Type, shortForms []string, e env, cl commandLine) []re
|
||||
iow := isWriter(ti)
|
||||
switch {
|
||||
case ior:
|
||||
args = append(args, reflect.ValueOf(os.Stdin))
|
||||
args = append(args, reflect.ValueOf(stdin))
|
||||
case iow:
|
||||
args = append(args, reflect.ValueOf(os.Stdout))
|
||||
args = append(args, reflect.ValueOf(stdout))
|
||||
case structure && variadic:
|
||||
if arg, ok := createStructArg(ti, shortForms, e, cl.options); ok {
|
||||
if arg, ok := createStructArg(ti, shortForms, c, e, cl.options); ok {
|
||||
args = append(args, arg)
|
||||
}
|
||||
case structure:
|
||||
arg, _ := createStructArg(ti, shortForms, e, cl.options)
|
||||
arg, _ := createStructArg(ti, shortForms, c, e, cl.options)
|
||||
args = append(args, arg)
|
||||
case variadic:
|
||||
for _, p := range positional {
|
||||
@ -224,11 +240,11 @@ func processResults(t reflect.Type, out []reflect.Value) ([]any, error) {
|
||||
return values, err
|
||||
}
|
||||
|
||||
func apply(cmd Cmd, e env, cl commandLine) ([]any, error) {
|
||||
func apply(stdin io.Reader, stdout io.Writer, cmd Cmd, c config, e env, cl commandLine) ([]any, error) {
|
||||
v := reflect.ValueOf(cmd.impl)
|
||||
v = unpack(v)
|
||||
t := v.Type()
|
||||
args := createArgs(t, cmd.shortForms, e, cl)
|
||||
args := createArgs(stdin, stdout, t, cmd.shortForms, c, e, cl)
|
||||
out := v.Call(args)
|
||||
return processResults(t, out)
|
||||
}
|
||||
|
||||
@ -1,12 +0,0 @@
|
||||
package main
|
||||
|
||||
// myFunc is.
|
||||
func myFunc() {
|
||||
}
|
||||
|
||||
// MyFunc is.
|
||||
func MyFunc() {
|
||||
}
|
||||
|
||||
func main() {
|
||||
}
|
||||
14
cmd/wand/main.go
Normal file
14
cmd/wand/main.go
Normal file
@ -0,0 +1,14 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
. "code.squareroundforest.org/arpio/wand"
|
||||
"code.squareroundforest.org/arpio/wand/tools"
|
||||
)
|
||||
|
||||
func main() {
|
||||
docreflect := Command("docreflect", tools.Docreflect)
|
||||
man := Command("manpages", tools.Man)
|
||||
md := Command("markdown", tools.Markdown)
|
||||
exec := Default(Command("exec", tools.Exec))
|
||||
Exec(Command("wand", nil, docreflect, man, md, exec), Etc(), UserConfig())
|
||||
}
|
||||
92
command.go
92
command.go
@ -24,20 +24,25 @@ func wrap(impl any) Cmd {
|
||||
return Command("", impl)
|
||||
}
|
||||
|
||||
func validateFields(f []field) error {
|
||||
func validateFields(f []field, conf Config) error {
|
||||
hasConfigFromOption := hasConfigFromOption(conf)
|
||||
mf := make(map[string]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 hasConfigFromOption && fi.name == "config" {
|
||||
return errors.New("option reserved for config file shadowed by struct field")
|
||||
}
|
||||
|
||||
mf[fi.name] = fi
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func validateParameter(t reflect.Type) error {
|
||||
func validateParameter(visited map[reflect.Type]bool, t reflect.Type) error {
|
||||
switch t.Kind() {
|
||||
case reflect.Bool,
|
||||
reflect.Int,
|
||||
@ -56,8 +61,17 @@ func validateParameter(t reflect.Type) error {
|
||||
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(t)
|
||||
return validateParameter(visited, t)
|
||||
case reflect.Interface:
|
||||
if t.NumMethod() > 0 {
|
||||
return errors.New("'non-empty' interface parameter")
|
||||
@ -81,7 +95,7 @@ func validatePositional(t reflect.Type, min, max int) error {
|
||||
continue
|
||||
}
|
||||
|
||||
if err := validateParameter(pi); err != nil {
|
||||
if err := validateParameter(nil, pi); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
@ -131,7 +145,7 @@ func validatePositional(t reflect.Type, min, max int) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func validateImpl(cmd Cmd) error {
|
||||
func validateImpl(cmd Cmd, conf Config) error {
|
||||
v := reflect.ValueOf(cmd.impl)
|
||||
v = unpack(v)
|
||||
t := v.Type()
|
||||
@ -140,8 +154,12 @@ func validateImpl(cmd Cmd) error {
|
||||
}
|
||||
|
||||
s := structParameters(t)
|
||||
f := fields(s...)
|
||||
if err := validateFields(f); err != nil {
|
||||
f, err := fieldsChecked(nil, s...)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := validateFields(f, conf); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@ -152,9 +170,8 @@ func validateImpl(cmd Cmd) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func validateShortForms(cmd Cmd) error {
|
||||
func validateShortForms(cmd Cmd, assignedShortForms map[string]string) error {
|
||||
mf := mapFields(cmd.impl)
|
||||
ms := make(map[string]string)
|
||||
if len(cmd.shortForms)%2 != 0 {
|
||||
return fmt.Errorf(
|
||||
"undefined option short form: %s", cmd.shortForms[len(cmd.shortForms)-1],
|
||||
@ -164,10 +181,6 @@ func validateShortForms(cmd Cmd) error {
|
||||
for i := 0; i < len(cmd.shortForms); i += 2 {
|
||||
fn := cmd.shortForms[i]
|
||||
sf := cmd.shortForms[i+1]
|
||||
if _, ok := mf[fn]; !ok {
|
||||
return fmt.Errorf("undefined field: %s", fn)
|
||||
}
|
||||
|
||||
if len(sf) != 1 && (sf[0] < 'a' || sf[0] > 'z') {
|
||||
return fmt.Errorf("invalid short form: %s", sf)
|
||||
}
|
||||
@ -176,23 +189,27 @@ func validateShortForms(cmd Cmd) error {
|
||||
return fmt.Errorf("short form shadowing field name: %s", sf)
|
||||
}
|
||||
|
||||
if lf, ok := ms[sf]; ok && lf != fn {
|
||||
if _, ok := mf[fn]; !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
if lf, ok := assignedShortForms[sf]; ok && lf != fn {
|
||||
return fmt.Errorf("ambigous short form: %s", sf)
|
||||
}
|
||||
|
||||
ms[sf] = fn
|
||||
assignedShortForms[sf] = fn
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func validateCommand(cmd Cmd) error {
|
||||
func validateCommandTree(cmd Cmd, conf Config, assignedShortForms map[string]string) error {
|
||||
if cmd.isHelp {
|
||||
return nil
|
||||
}
|
||||
|
||||
if cmd.impl != nil {
|
||||
if err := validateImpl(cmd); err != nil {
|
||||
if err := validateImpl(cmd, conf); err != nil {
|
||||
return fmt.Errorf("%s: %w", cmd.name, err)
|
||||
}
|
||||
}
|
||||
@ -202,7 +219,7 @@ func validateCommand(cmd Cmd) error {
|
||||
}
|
||||
|
||||
if cmd.impl != nil {
|
||||
if err := validateShortForms(cmd); err != nil {
|
||||
if err := validateShortForms(cmd, assignedShortForms); err != nil {
|
||||
return fmt.Errorf("%s: %w", cmd.name, err)
|
||||
}
|
||||
}
|
||||
@ -219,10 +236,18 @@ func validateCommand(cmd Cmd) error {
|
||||
}
|
||||
|
||||
names[s.name] = true
|
||||
if err := validateCommand(s); err != nil {
|
||||
if err := validateCommandTree(s, conf, assignedShortForms); 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",
|
||||
cmd.name,
|
||||
s.name,
|
||||
)
|
||||
}
|
||||
|
||||
if s.isDefault && hasDefault {
|
||||
return fmt.Errorf("multiple default subcommands for: %s", cmd.name)
|
||||
}
|
||||
@ -234,3 +259,32 @@ func validateCommand(cmd Cmd) error {
|
||||
|
||||
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); 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)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
package wand
|
||||
|
||||
/*
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
@ -237,3 +238,4 @@ func TestCommand(t *testing.T) {
|
||||
t.Run("default", testExec(t, Command("", nil, Command("bar", ff), Default(Command("baz", ff))), "", "foo", "", "0"))
|
||||
})
|
||||
}
|
||||
*/
|
||||
|
||||
191
config.go
Normal file
191
config.go
Normal file
@ -0,0 +1,191 @@
|
||||
package wand
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/iancoleman/strcase"
|
||||
"io"
|
||||
"os"
|
||||
)
|
||||
|
||||
type file struct {
|
||||
filename string
|
||||
file io.ReadCloser
|
||||
}
|
||||
|
||||
type config struct {
|
||||
values map[string][]string
|
||||
discard []string
|
||||
originalNames map[string]string
|
||||
}
|
||||
|
||||
func fileReader(filename string) io.ReadCloser {
|
||||
return &file{
|
||||
filename: filename,
|
||||
}
|
||||
}
|
||||
|
||||
func (f *file) wrapErr(err error) error {
|
||||
return fmt.Errorf("%s: %w", f.filename, err)
|
||||
}
|
||||
|
||||
func (f *file) Read(p []byte) (int, error) {
|
||||
if f.file == nil {
|
||||
file, err := os.Open(f.filename)
|
||||
if err != nil {
|
||||
return 0, f.wrapErr(err)
|
||||
}
|
||||
|
||||
f.file = file
|
||||
}
|
||||
|
||||
n, err := f.file.Read(p)
|
||||
if err != nil {
|
||||
return n, f.wrapErr(err)
|
||||
}
|
||||
|
||||
return n, nil
|
||||
}
|
||||
|
||||
func (f *file) Close() error {
|
||||
if f.file == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
if err := f.file.Close(); err != nil {
|
||||
return f.wrapErr(err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func readConfigFile(cmd Cmd, conf Config) (config, error) {
|
||||
f := conf.file(cmd)
|
||||
defer f.Close()
|
||||
doc, err := parse(f)
|
||||
if err != nil {
|
||||
if conf.optional && (errors.Is(err, os.ErrPermission) || errors.Is(err, os.ErrNotExist)) {
|
||||
return config{}, nil
|
||||
}
|
||||
|
||||
return config{}, fmt.Errorf("failed to read config file: %w", err)
|
||||
}
|
||||
|
||||
var c config
|
||||
for _, entry := range doc.Nodes {
|
||||
if entry.Name != "key-val" {
|
||||
continue
|
||||
}
|
||||
|
||||
var (
|
||||
key string
|
||||
value string
|
||||
hasValue bool
|
||||
)
|
||||
|
||||
for _, token := range entry.Nodes {
|
||||
if token.Name == "key" {
|
||||
key = token.Text()
|
||||
continue
|
||||
}
|
||||
|
||||
if token.Name == "value" {
|
||||
value = token.Text()
|
||||
hasValue = true
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
if c.originalNames == nil {
|
||||
c.originalNames = make(map[string]string)
|
||||
}
|
||||
|
||||
name := strcase.ToKebab(key)
|
||||
c.originalNames[name] = key
|
||||
if !hasValue {
|
||||
c.discard = append(c.discard, name)
|
||||
continue
|
||||
}
|
||||
|
||||
if c.values == nil {
|
||||
c.values = make(map[string][]string)
|
||||
}
|
||||
|
||||
c.values[name] = append(c.values[name], value)
|
||||
}
|
||||
|
||||
return c, nil
|
||||
}
|
||||
|
||||
func readConfigFromOption(cmd Cmd, cl commandLine, conf Config) (config, error) {
|
||||
f := mapFields(cmd.impl)
|
||||
if _, ok := f["config"]; ok {
|
||||
return config{}, nil
|
||||
}
|
||||
|
||||
var c []Config
|
||||
for _, o := range cl.options {
|
||||
if o.name != "config" {
|
||||
continue
|
||||
}
|
||||
|
||||
c = append(c, Config{file: func(Cmd) io.ReadCloser { return fileReader(o.value.str) }})
|
||||
}
|
||||
|
||||
return readConfig(cmd, cl, Config{merge: c})
|
||||
}
|
||||
|
||||
func readMergeConfig(cmd Cmd, cl commandLine, conf Config) (config, error) {
|
||||
var c []config
|
||||
for _, ci := range conf.merge {
|
||||
cci, err := readConfig(cmd, cl, ci)
|
||||
if err != nil {
|
||||
return config{}, err
|
||||
}
|
||||
|
||||
c = append(c, cci)
|
||||
}
|
||||
|
||||
var mc config
|
||||
for _, ci := range c {
|
||||
for _, d := range ci.discard {
|
||||
delete(mc.values, d)
|
||||
}
|
||||
|
||||
for name, values := range ci.values {
|
||||
if mc.values == nil {
|
||||
mc.values = make(map[string][]string)
|
||||
}
|
||||
|
||||
mc.values[name] = values
|
||||
}
|
||||
}
|
||||
|
||||
return mc, nil
|
||||
}
|
||||
|
||||
func readConfig(cmd Cmd, cl commandLine, conf Config) (config, error) {
|
||||
if conf.file != nil {
|
||||
return readConfigFile(cmd, conf)
|
||||
}
|
||||
|
||||
if conf.fromOption {
|
||||
return readConfigFromOption(cmd, cl, conf)
|
||||
}
|
||||
|
||||
return readMergeConfig(cmd, cl, conf)
|
||||
}
|
||||
|
||||
func hasConfigFromOption(conf Config) bool {
|
||||
if conf.fromOption {
|
||||
return true
|
||||
}
|
||||
|
||||
for _, c := range conf.merge {
|
||||
if hasConfigFromOption(c) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
44
config_test.go
Normal file
44
config_test.go
Normal file
@ -0,0 +1,44 @@
|
||||
package wand
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"code.squareroundforest.org/arpio/notation"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestConfig(t *testing.T) {
|
||||
type options struct {
|
||||
FooBarBaz int
|
||||
Foo string
|
||||
FooBar []string
|
||||
}
|
||||
|
||||
impl := func(o options) {
|
||||
if o.FooBarBaz != 42 {
|
||||
t.Fatal(notation.Sprintw(o))
|
||||
}
|
||||
|
||||
if o.Foo != "" {
|
||||
t.Fatal(notation.Sprintw(o))
|
||||
}
|
||||
|
||||
if len(o.FooBar) != 2 || o.FooBar[0] != "bar" || o.FooBar[1] != "baz" {
|
||||
t.Fatal(notation.Sprintw(o))
|
||||
}
|
||||
}
|
||||
|
||||
stdin := bytes.NewBuffer(nil)
|
||||
stdout := bytes.NewBuffer(nil)
|
||||
stderr := bytes.NewBuffer(nil)
|
||||
|
||||
exec(
|
||||
stdin,
|
||||
stdout,
|
||||
stderr,
|
||||
func(int) {},
|
||||
Command("test", impl),
|
||||
SystemConfig(),
|
||||
nil,
|
||||
[]string{"test", "--config", "./internal/tests/config.ini"},
|
||||
)
|
||||
}
|
||||
@ -1,5 +1,6 @@
|
||||
package wand
|
||||
|
||||
/*
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
@ -40,3 +41,4 @@ func TestEnv(t *testing.T) {
|
||||
t.Run("escape char last", testExec(t, fm, "fooOne=bar\\", "foo", "", "bar;"))
|
||||
})
|
||||
}
|
||||
*/
|
||||
|
||||
59
exec.go
59
exec.go
@ -5,55 +5,90 @@ import (
|
||||
"fmt"
|
||||
"github.com/iancoleman/strcase"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
func exec(stdout, stderr io.Writer, exit func(int), cmd Cmd, env, args []string) {
|
||||
func exec(stdin io.Reader, stdout, stderr io.Writer, exit func(int), cmd Cmd, conf Config, env, args []string) {
|
||||
cmd = insertHelp(cmd)
|
||||
_, cmd.name = filepath.Split(args[0])
|
||||
cmd.name = strcase.ToKebab(cmd.name)
|
||||
if err := validateCommand(cmd); err != nil {
|
||||
if err := validateCommand(cmd, conf); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
args = args[1:]
|
||||
if os.Getenv("wandgenerate") == "man" {
|
||||
if err := generateMan(stdout, cmd); err != nil {
|
||||
fmt.Fprintln(stderr, err)
|
||||
exit(1)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
if os.Getenv("wandgenerate") == "markdown" {
|
||||
level, _ := strconv.Atoi(os.Getenv("wandmarkdownlevel"))
|
||||
if err := generateMarkdown(stdout, cmd, level); err != nil {
|
||||
fmt.Fprintln(stderr, err)
|
||||
exit(1)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
e := readEnv(cmd.name, env)
|
||||
cmd, fullCmd, args := selectCommand(cmd, args)
|
||||
cmd, fullCmd, args := selectCommand(cmd, args[1:])
|
||||
if cmd.impl == nil {
|
||||
fmt.Fprint(stderr, errors.New("subcommand not specified"))
|
||||
fmt.Fprintln(stderr, errors.New("subcommand not specified"))
|
||||
suggestHelp(stderr, cmd, fullCmd)
|
||||
exit(1)
|
||||
return
|
||||
}
|
||||
|
||||
if cmd.helpRequested {
|
||||
showHelp(stdout, cmd, fullCmd)
|
||||
if err := showHelp(stdout, cmd, fullCmd); err != nil {
|
||||
fmt.Fprintln(stderr, err)
|
||||
exit(1)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
bo := boolOptions(cmd)
|
||||
cl := readArgs(bo, args)
|
||||
if hasHelpOption(cmd, cl.options) {
|
||||
showHelp(stdout, cmd, fullCmd)
|
||||
if err := showHelp(stdout, cmd, fullCmd); err != nil {
|
||||
fmt.Fprintln(stderr, err)
|
||||
exit(1)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
if err := validateInput(cmd, e, cl); err != nil {
|
||||
fmt.Fprint(stderr, err)
|
||||
c, err := readConfig(cmd, cl, conf)
|
||||
if err != nil {
|
||||
fmt.Fprintf(stderr, "configuration error: %v", err)
|
||||
exit(1)
|
||||
return
|
||||
}
|
||||
|
||||
if err := validateInput(cmd, conf, c, e, cl); err != nil {
|
||||
fmt.Fprintln(stderr, err)
|
||||
suggestHelp(stderr, cmd, fullCmd)
|
||||
exit(1)
|
||||
return
|
||||
}
|
||||
|
||||
output, err := apply(cmd, e, cl)
|
||||
output, err := apply(stdin, stdout, cmd, c, e, cl)
|
||||
if err != nil {
|
||||
fmt.Fprint(stderr, err)
|
||||
fmt.Fprintln(stderr, err)
|
||||
exit(1)
|
||||
return
|
||||
}
|
||||
|
||||
if err := printOutput(stdout, output); err != nil {
|
||||
fmt.Fprint(stderr, err)
|
||||
fmt.Fprintln(stderr, err)
|
||||
exit(1)
|
||||
return
|
||||
}
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
package wand
|
||||
|
||||
/*
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
@ -43,3 +44,4 @@ func testExec(impl any, env, commandLine, err string, expect ...string) func(*te
|
||||
}
|
||||
}
|
||||
}
|
||||
*/
|
||||
|
||||
10
go.mod
10
go.mod
@ -1,8 +1,12 @@
|
||||
module code.squareroundforest.org/arpio/wand
|
||||
|
||||
go 1.24.2
|
||||
go 1.24.6
|
||||
|
||||
require (
|
||||
code.squareroundforest.org/arpio/notation v0.0.0-20241225183158-af3bd591a174 // indirect
|
||||
github.com/iancoleman/strcase v0.3.0 // indirect
|
||||
code.squareroundforest.org/arpio/docreflect v0.0.0-20250823192303-755a103f3788
|
||||
code.squareroundforest.org/arpio/notation v0.0.0-20241225183158-af3bd591a174
|
||||
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
|
||||
|
||||
8
go.sum
8
go.sum
@ -1,4 +1,12 @@
|
||||
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/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/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=
|
||||
github.com/aryszka/notation v0.0.0-20230129164653-172017dde5e4/go.mod h1:myJFmFAZ/75y5xdA1jjpc4ItNJwdRqaL+TQhIvDU8Vk=
|
||||
github.com/iancoleman/strcase v0.3.0 h1:nTXanmYxhfFAMjZL34Ov6gkzEsSJZ5DbhxWjvSASxEI=
|
||||
github.com/iancoleman/strcase v0.3.0/go.mod h1:iwCmte+B7n89clKwxIoIXy/HfoL7AsD47ZCWhYzw7ho=
|
||||
golang.org/x/mod v0.27.0 h1:kb+q2PyFnEADO2IEF935ehFUXlWiNjJWtRNgBLSfbxQ=
|
||||
golang.org/x/mod v0.27.0/go.mod h1:rWI627Fq0DEoudcK+MBkNkCe0EetEaDSwJJkCcjpazc=
|
||||
|
||||
368
help.go
368
help.go
@ -1,26 +1,54 @@
|
||||
package wand
|
||||
|
||||
import (
|
||||
"code.squareroundforest.org/arpio/docreflect"
|
||||
"fmt"
|
||||
"io"
|
||||
"reflect"
|
||||
"sort"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type (
|
||||
synopsis struct{}
|
||||
docOptions struct{}
|
||||
docArguments struct{}
|
||||
docSubcommands struct{}
|
||||
)
|
||||
const defaultWrap = 112
|
||||
|
||||
type doc struct {
|
||||
type (
|
||||
argumentSet struct {
|
||||
count int
|
||||
names []string
|
||||
variadic bool
|
||||
usesStdin bool
|
||||
usesStdout bool
|
||||
}
|
||||
|
||||
synopsis struct {
|
||||
command string
|
||||
hasOptions bool
|
||||
arguments argumentSet
|
||||
hasSubcommands bool
|
||||
}
|
||||
|
||||
docOption struct {
|
||||
name string
|
||||
description string
|
||||
shortNames []string
|
||||
isBool bool
|
||||
acceptsMultiple bool
|
||||
}
|
||||
|
||||
doc struct {
|
||||
name string
|
||||
fullCommand string
|
||||
synopsis synopsis
|
||||
description string
|
||||
options docOptions
|
||||
arguments docArguments
|
||||
subcommands docSubcommands
|
||||
isHelp bool
|
||||
isDefault bool
|
||||
hasHelpSubcommand bool
|
||||
hasHelpOption bool
|
||||
options []docOption
|
||||
arguments argumentSet
|
||||
subcommands []doc
|
||||
}
|
||||
)
|
||||
|
||||
func help() Cmd {
|
||||
return Cmd{
|
||||
@ -73,9 +101,323 @@ func suggestHelp(out io.Writer, cmd Cmd, fullCommand []string) {
|
||||
}
|
||||
}
|
||||
|
||||
func constructDoc(cmd Cmd, fullCommand []string) doc {
|
||||
return doc{}
|
||||
func hasOptions(cmd Cmd) bool {
|
||||
if cmd.impl == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
func showHelp(out io.Writer, cmd Cmd, fullCommand []string) {
|
||||
v := reflect.ValueOf(cmd.impl)
|
||||
t := v.Type()
|
||||
t = unpack(t)
|
||||
s := structParameters(t)
|
||||
return len(fields(s...)) > 0
|
||||
}
|
||||
|
||||
func functionParams(v reflect.Value, skip []int) []string {
|
||||
names := docreflect.FunctionParams(v)
|
||||
for _, i := range skip {
|
||||
names = append(names[:i], names[i+1:]...)
|
||||
}
|
||||
|
||||
return names
|
||||
}
|
||||
|
||||
func constructArguments(cmd Cmd) argumentSet {
|
||||
if cmd.impl == nil {
|
||||
return argumentSet{}
|
||||
}
|
||||
|
||||
v := reflect.ValueOf(cmd.impl)
|
||||
t := unpack(v.Type())
|
||||
p := positionalParameters(t)
|
||||
ior, iow := ioParameters(p)
|
||||
count := len(p) - len(ior) - len(iow)
|
||||
names := functionParams(v, append(ior, iow...))
|
||||
if len(names) < count {
|
||||
names = nil
|
||||
for i := 0; i < count; i++ {
|
||||
names = append(names, fmt.Sprintf("arg%d", i))
|
||||
}
|
||||
}
|
||||
|
||||
return argumentSet{
|
||||
count: count,
|
||||
names: names,
|
||||
variadic: t.IsVariadic(),
|
||||
usesStdin: len(ior) > 0,
|
||||
usesStdout: len(iow) > 0,
|
||||
}
|
||||
}
|
||||
|
||||
func constructSynopsis(cmd Cmd, fullCommand []string) synopsis {
|
||||
return synopsis{
|
||||
command: strings.Join(fullCommand, " "),
|
||||
hasOptions: hasOptions(cmd),
|
||||
arguments: constructArguments(cmd),
|
||||
hasSubcommands: len(cmd.subcommands) > 0,
|
||||
}
|
||||
}
|
||||
|
||||
func constructDescription(cmd Cmd) string {
|
||||
if cmd.impl == nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
return strings.TrimSpace(docreflect.Function(reflect.ValueOf(cmd.impl)))
|
||||
}
|
||||
|
||||
func constructOptions(cmd Cmd) []docOption {
|
||||
if cmd.impl == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
sf := make(map[string][]string)
|
||||
for i := 0; i < len(cmd.shortForms); i += 2 {
|
||||
l, s := cmd.shortForms[i], cmd.shortForms[i+1]
|
||||
sf[l] = append(sf[l], s)
|
||||
}
|
||||
|
||||
t := unpack(reflect.ValueOf(cmd.impl).Type())
|
||||
s := structParameters(t)
|
||||
d := make(map[string]string)
|
||||
for _, si := range s {
|
||||
f := fields(si)
|
||||
for _, fi := range f {
|
||||
d[fi.name] = docreflect.Field(si, fi.path...)
|
||||
}
|
||||
}
|
||||
|
||||
var o []docOption
|
||||
f := mapFields(cmd.impl)
|
||||
for name, fi := range f {
|
||||
opt := docOption{
|
||||
name: name,
|
||||
description: d[name],
|
||||
shortNames: sf[name],
|
||||
isBool: fi[0].typ.Kind() == reflect.Bool,
|
||||
}
|
||||
|
||||
for _, fii := range fi {
|
||||
if fii.acceptsMultiple {
|
||||
opt.acceptsMultiple = true
|
||||
}
|
||||
}
|
||||
|
||||
o = append(o, opt)
|
||||
}
|
||||
|
||||
return o
|
||||
}
|
||||
|
||||
func constructDoc(cmd Cmd, fullCommand []string) doc {
|
||||
var subcommands []doc
|
||||
for _, sc := range cmd.subcommands {
|
||||
subcommands = append(subcommands, constructDoc(sc, append(fullCommand, sc.name)))
|
||||
}
|
||||
|
||||
return doc{
|
||||
name: fullCommand[len(fullCommand)-1],
|
||||
fullCommand: strings.Join(fullCommand, " "),
|
||||
synopsis: constructSynopsis(cmd, fullCommand),
|
||||
description: constructDescription(cmd),
|
||||
isDefault: cmd.isDefault,
|
||||
isHelp: cmd.isHelp,
|
||||
hasHelpSubcommand: hasHelpSubcommand(cmd),
|
||||
hasHelpOption: hasCustomHelpOption(cmd),
|
||||
options: constructOptions(cmd),
|
||||
arguments: constructArguments(cmd),
|
||||
subcommands: subcommands,
|
||||
}
|
||||
}
|
||||
|
||||
func formatHelp(w io.Writer, doc doc) error {
|
||||
var err error
|
||||
println := func(a ...any) {
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
_, err = fmt.Fprintln(w, a...)
|
||||
}
|
||||
|
||||
printf := func(f string, a ...any) {
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
_, err = fmt.Fprintf(w, f, a...)
|
||||
}
|
||||
|
||||
printf(doc.fullCommand)
|
||||
println()
|
||||
println()
|
||||
printf("Synopsis")
|
||||
println()
|
||||
println()
|
||||
printf(doc.synopsis.command)
|
||||
if doc.synopsis.hasOptions {
|
||||
printf(" [options ...]")
|
||||
}
|
||||
|
||||
for _, n := range doc.synopsis.arguments.names {
|
||||
printf(" %s", n)
|
||||
}
|
||||
|
||||
if doc.synopsis.arguments.variadic {
|
||||
printf("...")
|
||||
}
|
||||
|
||||
println()
|
||||
if doc.synopsis.hasSubcommands {
|
||||
printf("%s <subcommand> [options or args...]", doc.synopsis.command)
|
||||
println()
|
||||
println()
|
||||
printf("(For the details about the available subcommands, see the according section below.)")
|
||||
}
|
||||
|
||||
if len(doc.description) > 0 {
|
||||
println()
|
||||
println()
|
||||
printf(doc.description)
|
||||
}
|
||||
|
||||
if len(doc.options) > 0 {
|
||||
println()
|
||||
println()
|
||||
printf("Options")
|
||||
println()
|
||||
println()
|
||||
printf("[*]: accepts multiple instances of the same option")
|
||||
println()
|
||||
printf("[b]: booelan flag, true or false, or no argument means true")
|
||||
println()
|
||||
|
||||
var names []string
|
||||
od := make(map[string]string)
|
||||
for _, o := range doc.options {
|
||||
ons := []string{fmt.Sprintf("--%s", o.name)}
|
||||
for _, sn := range o.shortNames {
|
||||
ons = append(ons, fmt.Sprintf("-%s", sn))
|
||||
}
|
||||
|
||||
n := strings.Join(ons, ", ")
|
||||
if o.acceptsMultiple {
|
||||
n = fmt.Sprintf("%s [*]", n)
|
||||
}
|
||||
|
||||
if o.isBool {
|
||||
n = fmt.Sprintf("%s [b]", n)
|
||||
}
|
||||
|
||||
names = append(names, n)
|
||||
od[n] = o.description
|
||||
}
|
||||
|
||||
sort.Strings(names)
|
||||
|
||||
var max int
|
||||
for _, n := range names {
|
||||
if len(n) > max {
|
||||
max = len(n)
|
||||
}
|
||||
}
|
||||
|
||||
for i := range names {
|
||||
pad := strings.Join(make([]string, max-len(names[i])+1), " ")
|
||||
names[i] = fmt.Sprintf("%s%s", names[i], pad)
|
||||
}
|
||||
|
||||
for _, n := range names {
|
||||
println()
|
||||
printf(n)
|
||||
if od[n] != "" {
|
||||
printf(": %s", od[n])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if len(doc.subcommands) > 0 {
|
||||
println()
|
||||
println()
|
||||
printf("Subcommands")
|
||||
println()
|
||||
|
||||
var names []string
|
||||
cd := make(map[string]string)
|
||||
for _, sc := range doc.subcommands {
|
||||
name := sc.name
|
||||
if sc.isDefault {
|
||||
name = fmt.Sprintf("%s (default)", name)
|
||||
}
|
||||
|
||||
d := sc.description
|
||||
if sc.isHelp {
|
||||
d = fmt.Sprintf("Show this help. %s", d)
|
||||
}
|
||||
|
||||
if sc.hasHelpSubcommand {
|
||||
d = fmt.Sprintf("%s - For help, see: %s %s help", d, doc.name, sc.name)
|
||||
} else if sc.hasHelpOption {
|
||||
d = fmt.Sprintf("%s - For help, see: %s %s --help", d, doc.name, sc.name)
|
||||
}
|
||||
|
||||
cd[name] = d
|
||||
}
|
||||
|
||||
sort.Strings(names)
|
||||
|
||||
var max int
|
||||
for _, n := range names {
|
||||
if len(n) > max {
|
||||
max = len(n)
|
||||
}
|
||||
}
|
||||
|
||||
for i := range names {
|
||||
pad := strings.Join(make([]string, max-len(names[i])+1), " ")
|
||||
names[i] = fmt.Sprintf("%s%s", names[i], pad)
|
||||
}
|
||||
|
||||
for _, n := range names {
|
||||
println()
|
||||
printf(n)
|
||||
if cd[n] != "" {
|
||||
printf(": %s", cd[n])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
println()
|
||||
return err
|
||||
}
|
||||
|
||||
func showHelp(out io.Writer, cmd Cmd, fullCommand []string) error {
|
||||
doc := constructDoc(cmd, fullCommand)
|
||||
return formatHelp(out, doc)
|
||||
}
|
||||
|
||||
func formatMan(out io.Writer, doc doc) error {
|
||||
// if no subcommands, then similar to help
|
||||
// otherwise:
|
||||
// title
|
||||
// all commands
|
||||
return nil
|
||||
}
|
||||
|
||||
func generateMan(out io.Writer, cmd Cmd) error {
|
||||
doc := constructDoc(cmd, []string{cmd.name})
|
||||
return formatMan(out, doc)
|
||||
}
|
||||
|
||||
func formatMarkdown(out io.Writer, doc doc) error {
|
||||
// if no subcommands, then similar to help
|
||||
// otherwise:
|
||||
// title
|
||||
// all commands
|
||||
return nil
|
||||
}
|
||||
|
||||
func generateMarkdown(out io.Writer, cmd Cmd, level int) error {
|
||||
doc := constructDoc(cmd, []string{cmd.name})
|
||||
return formatMarkdown(out, doc)
|
||||
}
|
||||
|
||||
10
ini.treerack
Normal file
10
ini.treerack
Normal file
@ -0,0 +1,10 @@
|
||||
whitespace:ws = [ \b\f\r\t\v];
|
||||
comment-line:alias = "#" [^\n]*;
|
||||
comment = comment-line ("\n" comment-line)*;
|
||||
quoted:alias:nows = "\"" ([^\\"] | "\\" .)* "\"";
|
||||
word:alias:nows = [a-zA-Z_]([a-zA-Z_0-9\-] | "\\" .)*;
|
||||
key = word | quoted;
|
||||
value-chars:alias:nows = ([^\\"\n=# \b\f\r\t\v] | "\\" .)+;
|
||||
value = value-chars+ | quoted;
|
||||
key-val = (comment "\n")? key ("=" value?)? comment-line?;
|
||||
doc:root = (key-val | comment-line | "\n")*;
|
||||
41
input.go
41
input.go
@ -6,9 +6,9 @@ import (
|
||||
"slices"
|
||||
)
|
||||
|
||||
func validateEnv(cmd Cmd, e env) error {
|
||||
func validateKeyValues(cmd Cmd, keyValues map[string][]string, originalNames map[string]string) error {
|
||||
mf := mapFields(cmd.impl)
|
||||
for name, values := range e.values {
|
||||
for name, values := range keyValues {
|
||||
f, ok := mf[name]
|
||||
if !ok {
|
||||
continue
|
||||
@ -19,7 +19,7 @@ func validateEnv(cmd Cmd, e env) error {
|
||||
return fmt.Errorf(
|
||||
"expected only one value, received %d, as environment value, %s",
|
||||
len(values),
|
||||
e.originalNames[name],
|
||||
originalNames[name],
|
||||
)
|
||||
}
|
||||
|
||||
@ -27,7 +27,7 @@ func validateEnv(cmd Cmd, e env) error {
|
||||
if !canScan(fi.typ, v) {
|
||||
return fmt.Errorf(
|
||||
"environment variable cannot be applied, type mismatch: %s",
|
||||
e.originalNames[name],
|
||||
originalNames[name],
|
||||
)
|
||||
}
|
||||
}
|
||||
@ -37,7 +37,15 @@ func validateEnv(cmd Cmd, e env) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func validateOptions(cmd Cmd, o []option) error {
|
||||
func validateConfig(cmd Cmd, c config) error {
|
||||
return validateKeyValues(cmd, c.values, c.originalNames)
|
||||
}
|
||||
|
||||
func validateEnv(cmd Cmd, e env) error {
|
||||
return validateKeyValues(cmd, e.values, e.originalNames)
|
||||
}
|
||||
|
||||
func validateOptions(cmd Cmd, o []option, conf Config) error {
|
||||
ml := make(map[string]string)
|
||||
ms := make(map[string]string)
|
||||
for i := 0; i < len(cmd.shortForms); i += 2 {
|
||||
@ -57,14 +65,25 @@ func validateOptions(cmd Cmd, o []option) error {
|
||||
}
|
||||
|
||||
mf := mapFields(cmd.impl)
|
||||
if hasConfigFromOption(conf) {
|
||||
mf["config"] = []field{{
|
||||
acceptsMultiple: true,
|
||||
typ: reflect.TypeFor[string](),
|
||||
}}
|
||||
}
|
||||
|
||||
for n, os := range mo {
|
||||
f := mf[n]
|
||||
for _, fi := range f {
|
||||
en := "--" + n
|
||||
if sn, ok := ml[n]; ok {
|
||||
en += ", -" + sn
|
||||
}
|
||||
|
||||
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 {
|
||||
return fmt.Errorf(
|
||||
"expected only one value, received %d, as option, %s",
|
||||
@ -149,12 +168,16 @@ func validatePositionalArgs(cmd Cmd, a []string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func validateInput(cmd Cmd, e env, cl commandLine) error {
|
||||
func validateInput(cmd Cmd, conf Config, c config, e env, cl commandLine) error {
|
||||
if err := validateConfig(cmd, c); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := validateEnv(cmd, e); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := validateOptions(cmd, cl.options); err != nil {
|
||||
if err := validateOptions(cmd, cl.options, conf); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
|
||||
8
internal/tests/config.ini
Normal file
8
internal/tests/config.ini
Normal file
@ -0,0 +1,8 @@
|
||||
# test config
|
||||
|
||||
# another comment
|
||||
foo_bar_baz = 42 # a comment
|
||||
foo
|
||||
foo_bar = bar
|
||||
|
||||
foo_bar = baz
|
||||
11
notes.txt
11
notes.txt
@ -1,4 +1,7 @@
|
||||
io.Writer arg: pass in os.Stdout
|
||||
io.Reader arg: pass in os.Stdin
|
||||
test: method docs
|
||||
during validation, reject circular type references
|
||||
help:
|
||||
- what if cmd.impl is nil, but there is a default?
|
||||
- config in help
|
||||
- min/max args in help
|
||||
- env vars in help
|
||||
- test: method docs
|
||||
- testing formatting may need to be necessary for the help docs
|
||||
|
||||
87
reflect.go
87
reflect.go
@ -1,10 +1,12 @@
|
||||
package wand
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/iancoleman/strcase"
|
||||
"io"
|
||||
"reflect"
|
||||
"strconv"
|
||||
"io"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type packedKind[T any] interface {
|
||||
@ -14,6 +16,7 @@ type packedKind[T any] interface {
|
||||
|
||||
type field struct {
|
||||
name string
|
||||
path []string
|
||||
typ reflect.Type
|
||||
acceptsMultiple bool
|
||||
}
|
||||
@ -64,16 +67,44 @@ func isStruct(t reflect.Type) bool {
|
||||
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[2:], 2, byteSize*8)
|
||||
}
|
||||
}
|
||||
|
||||
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[2:], 2, 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 := strconv.ParseInt(s, 10, int(t.Size())*8)
|
||||
_, err := parseInt(s, int(t.Size()))
|
||||
return err == nil
|
||||
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
|
||||
_, err := strconv.ParseUint(s, 10, int(t.Size())*8)
|
||||
_, err := parseUint(s, int(t.Size()))
|
||||
return err == nil
|
||||
case reflect.Float32, reflect.Float64:
|
||||
_, err := strconv.ParseFloat(s, int(t.Size())*8)
|
||||
@ -92,10 +123,10 @@ func scan(t reflect.Type, s string) any {
|
||||
v, _ := strconv.ParseBool(s)
|
||||
p.Elem().Set(reflect.ValueOf(v).Convert(t))
|
||||
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
|
||||
v, _ := strconv.ParseInt(s, 10, int(t.Size())*8)
|
||||
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, _ := strconv.ParseUint(s, 10, int(t.Size())*8)
|
||||
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)
|
||||
@ -107,9 +138,9 @@ func scan(t reflect.Type, s string) any {
|
||||
return p.Elem().Interface()
|
||||
}
|
||||
|
||||
func fields(s ...reflect.Type) []field {
|
||||
func fieldsChecked(visited map[reflect.Type]bool, s ...reflect.Type) ([]field, error) {
|
||||
if len(s) == 0 {
|
||||
return nil
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
var (
|
||||
@ -139,18 +170,42 @@ func fields(s ...reflect.Type) []field {
|
||||
reflect.Float32,
|
||||
reflect.Float64,
|
||||
reflect.String:
|
||||
plainFields = append(plainFields, field{name: sfn, typ: sft, acceptsMultiple: am})
|
||||
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, typ: sft, acceptsMultiple: am})
|
||||
plainFields = append(plainFields, field{
|
||||
name: sfn,
|
||||
path: []string{sf.Name},
|
||||
typ: sft,
|
||||
acceptsMultiple: am,
|
||||
})
|
||||
}
|
||||
case reflect.Struct:
|
||||
sff := fields(sft)
|
||||
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
|
||||
}
|
||||
|
||||
@ -173,7 +228,17 @@ func fields(s ...reflect.Type) []field {
|
||||
f = append(f, fi)
|
||||
}
|
||||
|
||||
return append(f, fields(s[1:]...)...)
|
||||
ff, err := fieldsChecked(visited, s[1:]...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return append(f, ff...), nil
|
||||
}
|
||||
|
||||
func fields(s ...reflect.Type) []field {
|
||||
f, _ := fieldsChecked(nil, s...)
|
||||
return f
|
||||
}
|
||||
|
||||
func boolFields(f []field) []field {
|
||||
|
||||
17
script/docreflect/docs.go
Normal file
17
script/docreflect/docs.go
Normal file
@ -0,0 +1,17 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"code.squareroundforest.org/arpio/docreflect/generate"
|
||||
"log"
|
||||
"os"
|
||||
)
|
||||
|
||||
func main() {
|
||||
if len(os.Args) < 2 {
|
||||
log.Fatalln("expected package name")
|
||||
}
|
||||
|
||||
if err := generate.GenerateRegistry(os.Stdout, os.Args[1], os.Args[2:]...); err != nil {
|
||||
log.Fatalln(err)
|
||||
}
|
||||
}
|
||||
31
script/ini-parser/parser.go
Normal file
31
script/ini-parser/parser.go
Normal file
@ -0,0 +1,31 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"code.squareroundforest.org/arpio/treerack"
|
||||
"log"
|
||||
"os"
|
||||
)
|
||||
|
||||
func main() {
|
||||
packageName := "wand"
|
||||
if len(os.Args) > 1 {
|
||||
packageName = os.Args[1]
|
||||
}
|
||||
|
||||
syntax := &treerack.Syntax{}
|
||||
if err := syntax.ReadSyntax(os.Stdin); err != nil {
|
||||
log.Fatalln(err)
|
||||
}
|
||||
|
||||
if err := syntax.Init(); err != nil {
|
||||
log.Fatalln(err)
|
||||
}
|
||||
|
||||
options := treerack.GeneratorOptions{
|
||||
PackageName: packageName,
|
||||
}
|
||||
|
||||
if err := syntax.Generate(options, os.Stdout); err != nil {
|
||||
log.Fatalln(err)
|
||||
}
|
||||
}
|
||||
258
tools/tools.go
Normal file
258
tools/tools.go
Normal file
@ -0,0 +1,258 @@
|
||||
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
|
||||
PurgeCache 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 {
|
||||
pkg = strings.Join(append(gopath[:len(gopath)-1], sparts[0]), "/")
|
||||
expression = gopath[len(parts)-1]
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func functionHash(function string) (string, error) {
|
||||
h := fnv.New128()
|
||||
h.Write([]byte(function))
|
||||
buf := bytes.NewBuffer(nil)
|
||||
b64 := base64.NewEncoder(base64.URLEncoding, 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 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 copyFile(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()
|
||||
if _, err := io.Copy(dstf, srcf); err != nil {
|
||||
return fmt.Errorf("failed to copy file %s to %s; %w", src, 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 == "" {
|
||||
path.Join(os.Getenv("HOME"), ".wand")
|
||||
}
|
||||
|
||||
functionDir := path.Join(cacheDir, functionHash)
|
||||
if o.NoCache {
|
||||
functionDir = path.Join(cacheDir, "tmp", functionHash)
|
||||
}
|
||||
|
||||
if o.NoCache || o.PurgeCache {
|
||||
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 := copyFile(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)
|
||||
}
|
||||
}
|
||||
|
||||
if 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
|
||||
}
|
||||
71
wand.go
71
wand.go
@ -1,6 +1,17 @@
|
||||
package wand
|
||||
|
||||
import "os"
|
||||
import (
|
||||
"io"
|
||||
"os"
|
||||
"path"
|
||||
)
|
||||
|
||||
type Config struct {
|
||||
file func(Cmd) io.ReadCloser
|
||||
merge []Config
|
||||
fromOption bool
|
||||
optional bool
|
||||
}
|
||||
|
||||
type Cmd struct {
|
||||
name string
|
||||
@ -24,19 +35,65 @@ func Default(cmd Cmd) Cmd {
|
||||
return cmd
|
||||
}
|
||||
|
||||
// io doesn't count
|
||||
func Args(cmd Cmd, min, max int) Cmd {
|
||||
cmd.minPositional = min
|
||||
cmd.maxPositional = max
|
||||
return cmd
|
||||
}
|
||||
|
||||
func ShortForm(cmd Cmd, f ...string) Cmd {
|
||||
cmd.shortForms = f
|
||||
func ShortFormOptions(cmd Cmd, f ...string) Cmd {
|
||||
cmd.shortForms = append(cmd.shortForms, f...)
|
||||
for i := range cmd.subcommands {
|
||||
cmd.subcommands[i] = ShortFormOptions(
|
||||
cmd.subcommands[i],
|
||||
f...,
|
||||
)
|
||||
}
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
func Exec(impl any) {
|
||||
cmd := wrap(impl)
|
||||
exec(os.Stdout, os.Stderr, os.Exit, cmd, os.Environ(), os.Args)
|
||||
func MergeConfig(conf ...Config) Config {
|
||||
return Config{
|
||||
merge: conf,
|
||||
}
|
||||
}
|
||||
|
||||
func OptionalConfig(conf Config) Config {
|
||||
conf.optional = true
|
||||
for i := range conf.merge {
|
||||
conf.merge[i] = OptionalConfig(conf.merge[i])
|
||||
}
|
||||
|
||||
return conf
|
||||
}
|
||||
|
||||
func Etc() Config {
|
||||
return OptionalConfig(Config{
|
||||
file: func(cmd Cmd) io.ReadCloser {
|
||||
return fileReader(path.Join("/etc", cmd.name, "config"))
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
func UserConfig() Config {
|
||||
return OptionalConfig(Config{
|
||||
file: func(cmd Cmd) io.ReadCloser {
|
||||
return fileReader(
|
||||
path.Join(os.Getenv("HOME"), ".config", cmd.name, "config"),
|
||||
)
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
func ConfigFromOption() Config {
|
||||
return Config{fromOption: true}
|
||||
}
|
||||
|
||||
func SystemConfig() Config {
|
||||
return MergeConfig(Etc(), UserConfig(), ConfigFromOption())
|
||||
}
|
||||
|
||||
func Exec(impl any, conf ...Config) {
|
||||
exec(os.Stdin, os.Stdout, os.Stderr, os.Exit, wrap(impl), MergeConfig(conf...), os.Environ(), os.Args)
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user