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
|
default: build
|
||||||
|
|
||||||
build: $(SOURCES)
|
lib: $(SOURCES) iniparser.go docreflect.go
|
||||||
go build ./...
|
go build
|
||||||
|
go build ./tools
|
||||||
|
|
||||||
check: $(SOURCES)
|
build: lib wand
|
||||||
|
|
||||||
|
check: $(SOURCES) build
|
||||||
go test -count 1 ./...
|
go test -count 1 ./...
|
||||||
|
|
||||||
.cover: $(SOURCES)
|
.cover: $(SOURCES) build
|
||||||
go test -count 1 -coverprofile .cover ./...
|
go test -count 1 -coverprofile .cover ./...
|
||||||
|
|
||||||
cover: .cover
|
cover: .cover
|
||||||
@ -17,5 +20,24 @@ cover: .cover
|
|||||||
showcover: .cover
|
showcover: .cover
|
||||||
go tool cover -html .cover
|
go tool cover -html .cover
|
||||||
|
|
||||||
fmt: $(SOURCES)
|
fmt: $(SOURCES) iniparser.go
|
||||||
go fmt ./...
|
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 (
|
import (
|
||||||
"github.com/iancoleman/strcase"
|
"github.com/iancoleman/strcase"
|
||||||
|
"io"
|
||||||
"reflect"
|
"reflect"
|
||||||
"strings"
|
"strings"
|
||||||
"os"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func ensurePointerAllocation(p reflect.Value, n int) {
|
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)
|
tup := unpack(t)
|
||||||
f := fields(tup)
|
f := fields(tup)
|
||||||
fn := make(map[string]bool)
|
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)
|
om[n] = append(om[n], oi)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var foundConfig []string
|
||||||
|
for n := range c.values {
|
||||||
|
if fn[n] {
|
||||||
|
foundConfig = append(foundConfig, n)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
var foundEnv []string
|
var foundEnv []string
|
||||||
for n := range e.values {
|
for n := range e.values {
|
||||||
if fn[n] {
|
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
|
return reflect.Zero(t), false
|
||||||
}
|
}
|
||||||
|
|
||||||
p := reflect.New(tup)
|
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 {
|
for _, n := range foundEnv {
|
||||||
var v []value
|
var v []value
|
||||||
for _, vi := range e.values[n] {
|
for _, vi := range e.values[n] {
|
||||||
@ -165,7 +181,7 @@ func createPositional(t reflect.Type, v string) reflect.Value {
|
|||||||
return pack(sv, t)
|
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
|
var args []reflect.Value
|
||||||
positional := cl.positional
|
positional := cl.positional
|
||||||
for i := 0; i < t.NumIn(); i++ {
|
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)
|
iow := isWriter(ti)
|
||||||
switch {
|
switch {
|
||||||
case ior:
|
case ior:
|
||||||
args = append(args, reflect.ValueOf(os.Stdin))
|
args = append(args, reflect.ValueOf(stdin))
|
||||||
case iow:
|
case iow:
|
||||||
args = append(args, reflect.ValueOf(os.Stdout))
|
args = append(args, reflect.ValueOf(stdout))
|
||||||
case structure && variadic:
|
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)
|
args = append(args, arg)
|
||||||
}
|
}
|
||||||
case structure:
|
case structure:
|
||||||
arg, _ := createStructArg(ti, shortForms, e, cl.options)
|
arg, _ := createStructArg(ti, shortForms, c, e, cl.options)
|
||||||
args = append(args, arg)
|
args = append(args, arg)
|
||||||
case variadic:
|
case variadic:
|
||||||
for _, p := range positional {
|
for _, p := range positional {
|
||||||
@ -224,11 +240,11 @@ func processResults(t reflect.Type, out []reflect.Value) ([]any, error) {
|
|||||||
return values, err
|
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 := reflect.ValueOf(cmd.impl)
|
||||||
v = unpack(v)
|
v = unpack(v)
|
||||||
t := v.Type()
|
t := v.Type()
|
||||||
args := createArgs(t, cmd.shortForms, e, cl)
|
args := createArgs(stdin, stdout, t, cmd.shortForms, c, e, cl)
|
||||||
out := v.Call(args)
|
out := v.Call(args)
|
||||||
return processResults(t, out)
|
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)
|
return Command("", impl)
|
||||||
}
|
}
|
||||||
|
|
||||||
func validateFields(f []field) error {
|
func validateFields(f []field, conf Config) error {
|
||||||
|
hasConfigFromOption := hasConfigFromOption(conf)
|
||||||
mf := make(map[string]field)
|
mf := make(map[string]field)
|
||||||
for _, fi := range f {
|
for _, fi := range f {
|
||||||
if ef, ok := mf[fi.name]; ok && !compatibleTypes(fi.typ, ef.typ) {
|
if ef, ok := mf[fi.name]; ok && !compatibleTypes(fi.typ, ef.typ) {
|
||||||
return fmt.Errorf("duplicate fields with different types: %s", fi.name)
|
return fmt.Errorf("duplicate fields with different types: %s", fi.name)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if hasConfigFromOption && fi.name == "config" {
|
||||||
|
return errors.New("option reserved for config file shadowed by struct field")
|
||||||
|
}
|
||||||
|
|
||||||
mf[fi.name] = fi
|
mf[fi.name] = fi
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func validateParameter(t reflect.Type) error {
|
func validateParameter(visited map[reflect.Type]bool, t reflect.Type) error {
|
||||||
switch t.Kind() {
|
switch t.Kind() {
|
||||||
case reflect.Bool,
|
case reflect.Bool,
|
||||||
reflect.Int,
|
reflect.Int,
|
||||||
@ -56,8 +61,17 @@ func validateParameter(t reflect.Type) error {
|
|||||||
return nil
|
return nil
|
||||||
case reflect.Pointer,
|
case reflect.Pointer,
|
||||||
reflect.Slice:
|
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)
|
t = unpack(t)
|
||||||
return validateParameter(t)
|
return validateParameter(visited, t)
|
||||||
case reflect.Interface:
|
case reflect.Interface:
|
||||||
if t.NumMethod() > 0 {
|
if t.NumMethod() > 0 {
|
||||||
return errors.New("'non-empty' interface parameter")
|
return errors.New("'non-empty' interface parameter")
|
||||||
@ -81,7 +95,7 @@ func validatePositional(t reflect.Type, min, max int) error {
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := validateParameter(pi); err != nil {
|
if err := validateParameter(nil, pi); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -131,7 +145,7 @@ func validatePositional(t reflect.Type, min, max int) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func validateImpl(cmd Cmd) error {
|
func validateImpl(cmd Cmd, conf Config) error {
|
||||||
v := reflect.ValueOf(cmd.impl)
|
v := reflect.ValueOf(cmd.impl)
|
||||||
v = unpack(v)
|
v = unpack(v)
|
||||||
t := v.Type()
|
t := v.Type()
|
||||||
@ -140,8 +154,12 @@ func validateImpl(cmd Cmd) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
s := structParameters(t)
|
s := structParameters(t)
|
||||||
f := fields(s...)
|
f, err := fieldsChecked(nil, s...)
|
||||||
if err := validateFields(f); err != nil {
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := validateFields(f, conf); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -152,9 +170,8 @@ func validateImpl(cmd Cmd) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func validateShortForms(cmd Cmd) error {
|
func validateShortForms(cmd Cmd, assignedShortForms map[string]string) error {
|
||||||
mf := mapFields(cmd.impl)
|
mf := mapFields(cmd.impl)
|
||||||
ms := make(map[string]string)
|
|
||||||
if len(cmd.shortForms)%2 != 0 {
|
if len(cmd.shortForms)%2 != 0 {
|
||||||
return fmt.Errorf(
|
return fmt.Errorf(
|
||||||
"undefined option short form: %s", cmd.shortForms[len(cmd.shortForms)-1],
|
"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 {
|
for i := 0; i < len(cmd.shortForms); i += 2 {
|
||||||
fn := cmd.shortForms[i]
|
fn := cmd.shortForms[i]
|
||||||
sf := cmd.shortForms[i+1]
|
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') {
|
if len(sf) != 1 && (sf[0] < 'a' || sf[0] > 'z') {
|
||||||
return fmt.Errorf("invalid short form: %s", sf)
|
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)
|
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)
|
return fmt.Errorf("ambigous short form: %s", sf)
|
||||||
}
|
}
|
||||||
|
|
||||||
ms[sf] = fn
|
assignedShortForms[sf] = fn
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func validateCommand(cmd Cmd) error {
|
func validateCommandTree(cmd Cmd, conf Config, assignedShortForms map[string]string) error {
|
||||||
if cmd.isHelp {
|
if cmd.isHelp {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
if cmd.impl != 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)
|
return fmt.Errorf("%s: %w", cmd.name, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -202,7 +219,7 @@ func validateCommand(cmd Cmd) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if cmd.impl != nil {
|
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)
|
return fmt.Errorf("%s: %w", cmd.name, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -219,10 +236,18 @@ func validateCommand(cmd Cmd) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
names[s.name] = true
|
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)
|
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 {
|
if s.isDefault && hasDefault {
|
||||||
return fmt.Errorf("multiple default subcommands for: %s", cmd.name)
|
return fmt.Errorf("multiple default subcommands for: %s", cmd.name)
|
||||||
}
|
}
|
||||||
@ -234,3 +259,32 @@ func validateCommand(cmd Cmd) error {
|
|||||||
|
|
||||||
return nil
|
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
|
package wand
|
||||||
|
|
||||||
|
/*
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"strings"
|
"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"))
|
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
|
package wand
|
||||||
|
|
||||||
|
/*
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"strings"
|
"strings"
|
||||||
@ -40,3 +41,4 @@ func TestEnv(t *testing.T) {
|
|||||||
t.Run("escape char last", testExec(t, fm, "fooOne=bar\\", "foo", "", "bar;"))
|
t.Run("escape char last", testExec(t, fm, "fooOne=bar\\", "foo", "", "bar;"))
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
*/
|
||||||
|
|||||||
59
exec.go
59
exec.go
@ -5,55 +5,90 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"github.com/iancoleman/strcase"
|
"github.com/iancoleman/strcase"
|
||||||
"io"
|
"io"
|
||||||
|
"os"
|
||||||
"path/filepath"
|
"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 = insertHelp(cmd)
|
||||||
_, cmd.name = filepath.Split(args[0])
|
_, cmd.name = filepath.Split(args[0])
|
||||||
cmd.name = strcase.ToKebab(cmd.name)
|
cmd.name = strcase.ToKebab(cmd.name)
|
||||||
if err := validateCommand(cmd); err != nil {
|
if err := validateCommand(cmd, conf); err != nil {
|
||||||
panic(err)
|
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)
|
e := readEnv(cmd.name, env)
|
||||||
cmd, fullCmd, args := selectCommand(cmd, args)
|
cmd, fullCmd, args := selectCommand(cmd, args[1:])
|
||||||
if cmd.impl == nil {
|
if cmd.impl == nil {
|
||||||
fmt.Fprint(stderr, errors.New("subcommand not specified"))
|
fmt.Fprintln(stderr, errors.New("subcommand not specified"))
|
||||||
suggestHelp(stderr, cmd, fullCmd)
|
suggestHelp(stderr, cmd, fullCmd)
|
||||||
exit(1)
|
exit(1)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if cmd.helpRequested {
|
if cmd.helpRequested {
|
||||||
showHelp(stdout, cmd, fullCmd)
|
if err := showHelp(stdout, cmd, fullCmd); err != nil {
|
||||||
|
fmt.Fprintln(stderr, err)
|
||||||
|
exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
bo := boolOptions(cmd)
|
bo := boolOptions(cmd)
|
||||||
cl := readArgs(bo, args)
|
cl := readArgs(bo, args)
|
||||||
if hasHelpOption(cmd, cl.options) {
|
if hasHelpOption(cmd, cl.options) {
|
||||||
showHelp(stdout, cmd, fullCmd)
|
if err := showHelp(stdout, cmd, fullCmd); err != nil {
|
||||||
|
fmt.Fprintln(stderr, err)
|
||||||
|
exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := validateInput(cmd, e, cl); err != nil {
|
c, err := readConfig(cmd, cl, conf)
|
||||||
fmt.Fprint(stderr, err)
|
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)
|
suggestHelp(stderr, cmd, fullCmd)
|
||||||
exit(1)
|
exit(1)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
output, err := apply(cmd, e, cl)
|
output, err := apply(stdin, stdout, cmd, c, e, cl)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Fprint(stderr, err)
|
fmt.Fprintln(stderr, err)
|
||||||
exit(1)
|
exit(1)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := printOutput(stdout, output); err != nil {
|
if err := printOutput(stdout, output); err != nil {
|
||||||
fmt.Fprint(stderr, err)
|
fmt.Fprintln(stderr, err)
|
||||||
exit(1)
|
exit(1)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
package wand
|
package wand
|
||||||
|
|
||||||
|
/*
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"fmt"
|
"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
|
module code.squareroundforest.org/arpio/wand
|
||||||
|
|
||||||
go 1.24.2
|
go 1.24.6
|
||||||
|
|
||||||
require (
|
require (
|
||||||
code.squareroundforest.org/arpio/notation v0.0.0-20241225183158-af3bd591a174 // indirect
|
code.squareroundforest.org/arpio/docreflect v0.0.0-20250823192303-755a103f3788
|
||||||
github.com/iancoleman/strcase v0.3.0 // indirect
|
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 h1:DKMSagVY3uyRhJ4ohiwQzNnR6CWdVKLkg97A8eQGxQU=
|
||||||
code.squareroundforest.org/arpio/notation v0.0.0-20241225183158-af3bd591a174/go.mod h1:ait4Fvg9o0+bq5hlxi9dAcPL5a+/sr33qsZPNpToMLY=
|
code.squareroundforest.org/arpio/notation v0.0.0-20241225183158-af3bd591a174/go.mod h1:ait4Fvg9o0+bq5hlxi9dAcPL5a+/sr33qsZPNpToMLY=
|
||||||
|
code.squareroundforest.org/arpio/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 h1:nTXanmYxhfFAMjZL34Ov6gkzEsSJZ5DbhxWjvSASxEI=
|
||||||
github.com/iancoleman/strcase v0.3.0/go.mod h1:iwCmte+B7n89clKwxIoIXy/HfoL7AsD47ZCWhYzw7ho=
|
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
|
package wand
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"code.squareroundforest.org/arpio/docreflect"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
|
"reflect"
|
||||||
|
"sort"
|
||||||
"strings"
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
type (
|
const defaultWrap = 112
|
||||||
synopsis struct{}
|
|
||||||
docOptions struct{}
|
|
||||||
docArguments struct{}
|
|
||||||
docSubcommands struct{}
|
|
||||||
)
|
|
||||||
|
|
||||||
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
|
name string
|
||||||
|
description string
|
||||||
|
shortNames []string
|
||||||
|
isBool bool
|
||||||
|
acceptsMultiple bool
|
||||||
|
}
|
||||||
|
|
||||||
|
doc struct {
|
||||||
|
name string
|
||||||
|
fullCommand string
|
||||||
synopsis synopsis
|
synopsis synopsis
|
||||||
description string
|
description string
|
||||||
options docOptions
|
isHelp bool
|
||||||
arguments docArguments
|
isDefault bool
|
||||||
subcommands docSubcommands
|
hasHelpSubcommand bool
|
||||||
|
hasHelpOption bool
|
||||||
|
options []docOption
|
||||||
|
arguments argumentSet
|
||||||
|
subcommands []doc
|
||||||
}
|
}
|
||||||
|
)
|
||||||
|
|
||||||
func help() Cmd {
|
func help() Cmd {
|
||||||
return Cmd{
|
return Cmd{
|
||||||
@ -73,9 +101,323 @@ func suggestHelp(out io.Writer, cmd Cmd, fullCommand []string) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func constructDoc(cmd Cmd, fullCommand []string) doc {
|
func hasOptions(cmd Cmd) bool {
|
||||||
return doc{}
|
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"
|
"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)
|
mf := mapFields(cmd.impl)
|
||||||
for name, values := range e.values {
|
for name, values := range keyValues {
|
||||||
f, ok := mf[name]
|
f, ok := mf[name]
|
||||||
if !ok {
|
if !ok {
|
||||||
continue
|
continue
|
||||||
@ -19,7 +19,7 @@ func validateEnv(cmd Cmd, e env) error {
|
|||||||
return fmt.Errorf(
|
return fmt.Errorf(
|
||||||
"expected only one value, received %d, as environment value, %s",
|
"expected only one value, received %d, as environment value, %s",
|
||||||
len(values),
|
len(values),
|
||||||
e.originalNames[name],
|
originalNames[name],
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -27,7 +27,7 @@ func validateEnv(cmd Cmd, e env) error {
|
|||||||
if !canScan(fi.typ, v) {
|
if !canScan(fi.typ, v) {
|
||||||
return fmt.Errorf(
|
return fmt.Errorf(
|
||||||
"environment variable cannot be applied, type mismatch: %s",
|
"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
|
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)
|
ml := make(map[string]string)
|
||||||
ms := make(map[string]string)
|
ms := make(map[string]string)
|
||||||
for i := 0; i < len(cmd.shortForms); i += 2 {
|
for i := 0; i < len(cmd.shortForms); i += 2 {
|
||||||
@ -57,14 +65,25 @@ func validateOptions(cmd Cmd, o []option) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
mf := mapFields(cmd.impl)
|
mf := mapFields(cmd.impl)
|
||||||
|
if hasConfigFromOption(conf) {
|
||||||
|
mf["config"] = []field{{
|
||||||
|
acceptsMultiple: true,
|
||||||
|
typ: reflect.TypeFor[string](),
|
||||||
|
}}
|
||||||
|
}
|
||||||
|
|
||||||
for n, os := range mo {
|
for n, os := range mo {
|
||||||
f := mf[n]
|
|
||||||
for _, fi := range f {
|
|
||||||
en := "--" + n
|
en := "--" + n
|
||||||
if sn, ok := ml[n]; ok {
|
if sn, ok := ml[n]; ok {
|
||||||
en += ", -" + sn
|
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 {
|
if len(os) > 1 && !fi.acceptsMultiple {
|
||||||
return fmt.Errorf(
|
return fmt.Errorf(
|
||||||
"expected only one value, received %d, as option, %s",
|
"expected only one value, received %d, as option, %s",
|
||||||
@ -149,12 +168,16 @@ func validatePositionalArgs(cmd Cmd, a []string) error {
|
|||||||
return nil
|
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 {
|
if err := validateEnv(cmd, e); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := validateOptions(cmd, cl.options); err != nil {
|
if err := validateOptions(cmd, cl.options, conf); err != nil {
|
||||||
return err
|
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
|
help:
|
||||||
io.Reader arg: pass in os.Stdin
|
- what if cmd.impl is nil, but there is a default?
|
||||||
test: method docs
|
- config in help
|
||||||
during validation, reject circular type references
|
- 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
|
package wand
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
"github.com/iancoleman/strcase"
|
"github.com/iancoleman/strcase"
|
||||||
|
"io"
|
||||||
"reflect"
|
"reflect"
|
||||||
"strconv"
|
"strconv"
|
||||||
"io"
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
type packedKind[T any] interface {
|
type packedKind[T any] interface {
|
||||||
@ -14,6 +16,7 @@ type packedKind[T any] interface {
|
|||||||
|
|
||||||
type field struct {
|
type field struct {
|
||||||
name string
|
name string
|
||||||
|
path []string
|
||||||
typ reflect.Type
|
typ reflect.Type
|
||||||
acceptsMultiple bool
|
acceptsMultiple bool
|
||||||
}
|
}
|
||||||
@ -64,16 +67,44 @@ func isStruct(t reflect.Type) bool {
|
|||||||
return t.Kind() == reflect.Struct
|
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 {
|
func canScan(t reflect.Type, s string) bool {
|
||||||
switch t.Kind() {
|
switch t.Kind() {
|
||||||
case reflect.Bool:
|
case reflect.Bool:
|
||||||
_, err := strconv.ParseBool(s)
|
_, err := strconv.ParseBool(s)
|
||||||
return err == nil
|
return err == nil
|
||||||
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
|
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
|
return err == nil
|
||||||
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
|
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
|
return err == nil
|
||||||
case reflect.Float32, reflect.Float64:
|
case reflect.Float32, reflect.Float64:
|
||||||
_, err := strconv.ParseFloat(s, int(t.Size())*8)
|
_, err := strconv.ParseFloat(s, int(t.Size())*8)
|
||||||
@ -92,10 +123,10 @@ func scan(t reflect.Type, s string) any {
|
|||||||
v, _ := strconv.ParseBool(s)
|
v, _ := strconv.ParseBool(s)
|
||||||
p.Elem().Set(reflect.ValueOf(v).Convert(t))
|
p.Elem().Set(reflect.ValueOf(v).Convert(t))
|
||||||
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
|
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))
|
p.Elem().Set(reflect.ValueOf(v).Convert(t))
|
||||||
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
|
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))
|
p.Elem().Set(reflect.ValueOf(v).Convert(t))
|
||||||
case reflect.Float32, reflect.Float64:
|
case reflect.Float32, reflect.Float64:
|
||||||
v, _ := strconv.ParseFloat(s, int(t.Size())*8)
|
v, _ := strconv.ParseFloat(s, int(t.Size())*8)
|
||||||
@ -107,9 +138,9 @@ func scan(t reflect.Type, s string) any {
|
|||||||
return p.Elem().Interface()
|
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 {
|
if len(s) == 0 {
|
||||||
return nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
var (
|
var (
|
||||||
@ -139,18 +170,42 @@ func fields(s ...reflect.Type) []field {
|
|||||||
reflect.Float32,
|
reflect.Float32,
|
||||||
reflect.Float64,
|
reflect.Float64,
|
||||||
reflect.String:
|
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:
|
case reflect.Interface:
|
||||||
if sft.NumMethod() == 0 {
|
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:
|
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 {
|
if sf.Anonymous {
|
||||||
anonFields = append(anonFields, sff...)
|
anonFields = append(anonFields, sff...)
|
||||||
} else {
|
} else {
|
||||||
for i := range sff {
|
for i := range sff {
|
||||||
sff[i].name = sfn + "-" + sff[i].name
|
sff[i].name = sfn + "-" + sff[i].name
|
||||||
|
sff[i].path = append([]string{sf.Name}, sff[i].path...)
|
||||||
sff[i].acceptsMultiple = sff[i].acceptsMultiple || am
|
sff[i].acceptsMultiple = sff[i].acceptsMultiple || am
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -173,7 +228,17 @@ func fields(s ...reflect.Type) []field {
|
|||||||
f = append(f, fi)
|
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 {
|
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
|
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 {
|
type Cmd struct {
|
||||||
name string
|
name string
|
||||||
@ -24,19 +35,65 @@ func Default(cmd Cmd) Cmd {
|
|||||||
return cmd
|
return cmd
|
||||||
}
|
}
|
||||||
|
|
||||||
// io doesn't count
|
|
||||||
func Args(cmd Cmd, min, max int) Cmd {
|
func Args(cmd Cmd, min, max int) Cmd {
|
||||||
cmd.minPositional = min
|
cmd.minPositional = min
|
||||||
cmd.maxPositional = max
|
cmd.maxPositional = max
|
||||||
return cmd
|
return cmd
|
||||||
}
|
}
|
||||||
|
|
||||||
func ShortForm(cmd Cmd, f ...string) Cmd {
|
func ShortFormOptions(cmd Cmd, f ...string) Cmd {
|
||||||
cmd.shortForms = f
|
cmd.shortForms = append(cmd.shortForms, f...)
|
||||||
|
for i := range cmd.subcommands {
|
||||||
|
cmd.subcommands[i] = ShortFormOptions(
|
||||||
|
cmd.subcommands[i],
|
||||||
|
f...,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
return cmd
|
return cmd
|
||||||
}
|
}
|
||||||
|
|
||||||
func Exec(impl any) {
|
func MergeConfig(conf ...Config) Config {
|
||||||
cmd := wrap(impl)
|
return Config{
|
||||||
exec(os.Stdout, os.Stderr, os.Exit, cmd, os.Environ(), os.Args)
|
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