interim checkin

This commit is contained in:
Arpad Ryszka 2025-08-24 01:45:25 +02:00
parent 0131d6d955
commit f206c694b2
24 changed files with 1316 additions and 117 deletions

3
.gitignore vendored Normal file
View File

@ -0,0 +1,3 @@
iniparser.go
docreflect.go
.bin

View File

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

View File

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

View File

@ -1,12 +0,0 @@
package main
// myFunc is.
func myFunc() {
}
// MyFunc is.
func MyFunc() {
}
func main() {
}

14
cmd/wand/main.go Normal file
View 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())
}

View File

@ -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,12 +95,12 @@ 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
}
}
last := t.NumIn()-1
last := t.NumIn() - 1
lastVariadic := t.IsVariadic() &&
!isStruct(t.In(last)) &&
!slices.Contains(ior, last) &&
@ -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
}

View File

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

View File

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

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

View File

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

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

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

370
help.go
View File

@ -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
}
v := reflect.ValueOf(cmd.impl)
t := v.Type()
t = unpack(t)
s := structParameters(t)
return len(fields(s...)) > 0
}
func showHelp(out io.Writer, cmd Cmd, fullCommand []string) {
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
View 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")*;

View File

@ -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",
@ -100,7 +119,7 @@ func validatePositionalArgs(cmd Cmd, a []string) error {
t := v.Type()
p := positionalParameters(t)
ior, iow := ioParameters(p)
last := t.NumIn()-1
last := t.NumIn() - 1
lastVariadic := t.IsVariadic() &&
!isStruct(t.In(last)) &&
!slices.Contains(ior, last) &&
@ -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
}

View File

@ -0,0 +1,8 @@
# test config
# another comment
foo_bar_baz = 42 # a comment
foo
foo_bar = bar
foo_bar = baz

View File

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

View File

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

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

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