wand/lib.go

139 lines
2.9 KiB
Go

// Wand provides utilities for constructing command line applications from functions, with automatic parameter
// binding from command line arguments, environment variables and configuration files.
package wand
import (
"fmt"
"os"
"path"
)
// the parsing errors might be tricky
type Config struct {
file func(Cmd) *file
merge []Config
fromOption bool
optional bool
test string
}
type Cmd struct {
name string
impl any
group bool
subcommands []Cmd
isDefault bool
minPositional int
maxPositional int
shortForms []string
helpFor *Cmd
version string
}
// name needs to be a valid symbol. The application name should also be a valid symbol,
// though not mandatory. If it is not, the environment variables may not work properly.
func Command(name string, impl any, subcmds ...Cmd) Cmd {
return Cmd{name: name, impl: impl, subcommands: subcmds}
}
func Group(name string, subcmds ...Cmd) Cmd {
return Cmd{name: name, group: true, subcommands: subcmds}
}
func Default(cmd Cmd) Cmd {
cmd.isDefault = true
return cmd
}
func Args(cmd Cmd, min, max int) Cmd {
cmd.minPositional = min
cmd.maxPositional = max
return cmd
}
func ShortForm(cmd Cmd, f ...string) Cmd {
if len(f)%2 != 0 {
f = f[:len(f)-1]
}
cmd.shortForms = append(cmd.shortForms, f...)
for i := range cmd.subcommands {
cmd.subcommands[i] = ShortForm(
cmd.subcommands[i],
f...,
)
}
return cmd
}
func Version(cmd Cmd, version string) Cmd {
cmd.subcommands = append(
cmd.subcommands,
Cmd{name: "version", version: version},
)
return cmd
}
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 ConfigFile(name string) Config {
return Config{
file: func(Cmd) *file { return fileReader(name) },
}
}
// the config will consider the command name as in the arguments
func Etc() Config {
return OptionalConfig(Config{
file: func(cmd Cmd) *file {
return fileReader(path.Join("/etc", cmd.name, "config"))
},
})
}
// the config will consider the command name as in the arguments
func UserConfig() Config {
return OptionalConfig(
MergeConfig(
Config{
file: func(cmd Cmd) *file {
return fileReader(path.Join(os.Getenv("HOME"), fmt.Sprintf(".%s", cmd.name), "config"))
},
},
Config{
file: func(cmd Cmd) *file {
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())
}
// the env will consider the command name as in the arguments
func Exec(impl any, conf ...Config) {
exec(os.Stdin, os.Stdout, os.Stderr, os.Exit, wrap(impl), MergeConfig(conf...), os.Environ(), os.Args)
}