2025-09-04 01:09:24 +02:00
|
|
|
// Wand provides utilities for constructing command line applications from functions, with automatic parameter
|
2026-01-07 17:07:46 +01:00
|
|
|
// binding from command line arguments, environment variables and configuration files, and automatically
|
|
|
|
|
// generated help and documentation.
|
2025-08-18 14:24:31 +02:00
|
|
|
package wand
|
|
|
|
|
|
2025-08-24 01:45:25 +02:00
|
|
|
import (
|
2025-08-24 04:46:54 +02:00
|
|
|
"fmt"
|
2025-08-24 01:45:25 +02:00
|
|
|
"os"
|
|
|
|
|
"path"
|
|
|
|
|
)
|
|
|
|
|
|
2026-01-07 17:07:46 +01:00
|
|
|
// Config represents one or more configuration sources.
|
2025-08-24 01:45:25 +02:00
|
|
|
type Config struct {
|
2025-08-24 04:46:54 +02:00
|
|
|
file func(Cmd) *file
|
2025-08-24 01:45:25 +02:00
|
|
|
merge []Config
|
|
|
|
|
fromOption bool
|
|
|
|
|
optional bool
|
2025-08-26 03:21:35 +02:00
|
|
|
test string
|
2025-08-24 01:45:25 +02:00
|
|
|
}
|
2025-08-18 14:24:31 +02:00
|
|
|
|
2026-01-07 17:07:46 +01:00
|
|
|
// Cmd represents a command, a subcommand or a subcommand group.
|
2025-08-18 14:24:31 +02:00
|
|
|
type Cmd struct {
|
|
|
|
|
name string
|
|
|
|
|
impl any
|
2025-08-26 14:12:18 +02:00
|
|
|
group bool
|
2025-08-18 14:24:31 +02:00
|
|
|
subcommands []Cmd
|
|
|
|
|
isDefault bool
|
|
|
|
|
minPositional int
|
|
|
|
|
maxPositional int
|
|
|
|
|
shortForms []string
|
2025-09-05 03:19:00 +02:00
|
|
|
helpFor *Cmd
|
2025-08-26 03:21:35 +02:00
|
|
|
version string
|
2025-08-18 14:24:31 +02:00
|
|
|
}
|
|
|
|
|
|
2026-01-07 17:07:46 +01:00
|
|
|
// Command defines a command or a subcommand.
|
|
|
|
|
//
|
|
|
|
|
// The name argument is expected to be a valid symbol, with non-leading dashes allowed
|
|
|
|
|
// (^[a-zA-Z_][a-zA-Z_0-9-]*$). It is optional to set the name of the top level command, and it is inferred from
|
|
|
|
|
// the executing binary's name. The name of the executable should also be a symbol, otherwise the automatic
|
|
|
|
|
// binding of the environment variables may not work properly.
|
|
|
|
|
//
|
|
|
|
|
// The implementation argument needs to be a function. The input parameters of the function need to be bindable,
|
|
|
|
|
// or either an io.Reader or io.Writer. Bindable means that the type can accept scalar values, like numbers,
|
|
|
|
|
// strings, time and duration, or it has fields like a struct. Pointers to bindable types are also bindable.
|
|
|
|
|
//
|
|
|
|
|
// Scalar arguments are considered as positional arguments of the command. Variadic arguments are supported.
|
|
|
|
|
//
|
|
|
|
|
// The fields of struct arguments define which command line options are supported by the command. Input
|
2026-01-07 22:21:54 +01:00
|
|
|
// for the options is accepted from both command line flags or environment variables, and, if defined, from
|
2026-01-07 17:07:46 +01:00
|
|
|
// configuration. Values defined in the environment override the configuration, and values passed in as command
|
|
|
|
|
// line flags override the environment (not propagated automatically to the environment of any child processes).
|
|
|
|
|
// Zero or more struct parameters are accepted, e.g. func(g Globals, o Options).
|
|
|
|
|
//
|
|
|
|
|
// The names of the command line flags are inferred from the struct path and field names, and are expected in
|
|
|
|
|
// kebab case with double leading dashes, unless a short form is defined. Option values are accepted both
|
|
|
|
|
// with = or just space separated following the flag. In case of bool options, omitting the value is considered
|
|
|
|
|
// as true value. Since the struct path and field names are collapsed into their flat kebab case representation,
|
2026-01-07 22:21:54 +01:00
|
|
|
// this results in ambiguity, e.g. Foo.BarBaz and Foo.Bar.Baz. In such cases, the types of the ambiguous fields
|
2026-01-07 17:07:46 +01:00
|
|
|
// must be compatible, and each such field will be bound to the same input value. Slice fields are supported,
|
|
|
|
|
// they accept zero or more options of the same name, every option value is bound as an item of the slice, e.g.
|
|
|
|
|
// --foo one --foo two --foo three.
|
|
|
|
|
//
|
|
|
|
|
// The names of the environment variables are inferred from the struct path and field names, are prefixed with
|
2026-01-07 22:21:54 +01:00
|
|
|
// the name of the executing binary, and are expected in lower or upper snake case. E.g. for a field InputValue:
|
2026-01-07 17:07:46 +01:00
|
|
|
// FOO_BAR_INPUT_VALUE=42 /usr/bin/foo-bar. The same ambiguity rules apply as in case of the command line
|
|
|
|
|
// flags. Slice fields are supported, the values slice fields can be separated by the : character, like in case
|
|
|
|
|
// of the PATH environment variable. When necessary, the : character can be escaped as \:.
|
|
|
|
|
//
|
|
|
|
|
// For the implementation function, zero or one io.Reader and zero or one io.Writer parameter is accepted. When
|
2026-01-07 22:21:54 +01:00
|
|
|
// present, io.Reader is populated by os.Stdin and io.Writer is populated by os.Stdout.
|
2026-01-07 17:07:46 +01:00
|
|
|
//
|
|
|
|
|
// The implementation function can have zero or more output parameters. If the last output parameter is of type
|
|
|
|
|
// error, and the returned value is not nil, the rest of the output parameters will be ignored and the error
|
|
|
|
|
// will be printed onto os.Stderr, and the command will exit with a non zero code. In case of no error, every
|
|
|
|
|
// return value will be printed on its own line, and if a return value is a slice, then every item will also be
|
|
|
|
|
// printed on its own line. If a return value is an io.Reader, that reader will be copied onto os.Stdout. If a
|
2026-01-07 22:21:54 +01:00
|
|
|
// value is a primitive value, it will be printed onto os.Stdout using the stdlib fmt package. Complex values
|
2026-01-07 17:07:46 +01:00
|
|
|
// will be printed to os.Stdout using code.squareroundforest.org/arpio/notation.
|
|
|
|
|
//
|
|
|
|
|
// A command can have zero or more subcommands. When executing a subcommand, the subcommands path must be at the
|
|
|
|
|
// start of the command line expression. E.g. foo bar baz --qux 42 corge, where bar is a subcommand of foo and
|
|
|
|
|
// baz is a subcommand of bar. All rules that apply to the top command, also apply to the subcommands.
|
|
|
|
|
//
|
|
|
|
|
// If a struct field doesn't override it, a --help command line flag will be automatically injected, and calling
|
|
|
|
|
// it will display an automatically generated help. Similarly, a help subcommand is also automatically injected
|
2026-01-07 22:21:54 +01:00
|
|
|
// under every command in the command tree, if a subcommand has not already taken the name help. The help
|
2026-01-07 17:07:46 +01:00
|
|
|
// subcommand has the same effect as the --help flag. When the user provides invalid input, the command exits
|
|
|
|
|
// with a non zero code, and displays a short suggestion to the user on how to display this help.
|
|
|
|
|
//
|
|
|
|
|
// The automatically generated help can display the command synopsis and the possible args and options, but it
|
|
|
|
|
// will not contain any descriptions. Wand is meant to be used together with docreflect, which can extract the
|
|
|
|
|
// godoc documentation of the implementation function during development time, and compile it into the final
|
|
|
|
|
// binary. When done so, the automatically generated help will include the godocs of the implementation function
|
|
|
|
|
// and the description of the fields that serve as the command line options. It is also possible to generate man
|
|
|
|
|
// pages or markdown from the godoc documentation. For more details, see the documentation of the wand tool.
|
|
|
|
|
//
|
2026-01-07 22:21:54 +01:00
|
|
|
// When executing a command, there are two distinct stages of validation. The first one validates that the
|
2026-01-07 17:07:46 +01:00
|
|
|
// command definition itself is valid, while the second stage validates the user input against the command
|
|
|
|
|
// definition. The validation of the command definition happens without considering the user input, and errors
|
|
|
|
|
// are prefixed with "program error:". This way we can know during development time if the command definition is
|
|
|
|
|
// valid or not.
|
2025-08-18 14:24:31 +02:00
|
|
|
func Command(name string, impl any, subcmds ...Cmd) Cmd {
|
2025-08-26 14:12:18 +02:00
|
|
|
return Cmd{name: name, impl: impl, subcommands: subcmds}
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-07 17:07:46 +01:00
|
|
|
// Group is like command but without implementation. It is used to group subcommands. It must have at least one
|
|
|
|
|
// subcommand. Optionally, one of the subcommands can be set as the default. When a default is set, and calling
|
|
|
|
|
// the group without specifying a subcommand, the default subcommand will be executed.
|
2025-08-26 14:12:18 +02:00
|
|
|
func Group(name string, subcmds ...Cmd) Cmd {
|
|
|
|
|
return Cmd{name: name, group: true, subcommands: subcmds}
|
2025-08-18 14:24:31 +02:00
|
|
|
}
|
|
|
|
|
|
2026-01-07 17:07:46 +01:00
|
|
|
// Default sets a subcommand as the default, to be used in a group.
|
2025-08-18 14:24:31 +02:00
|
|
|
func Default(cmd Cmd) Cmd {
|
|
|
|
|
cmd.isDefault = true
|
|
|
|
|
return cmd
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-07 17:07:46 +01:00
|
|
|
// Args can be used to specify the minimum and the maximum number of positional arguments when the
|
|
|
|
|
// implementation function has variadic parameters.
|
2025-08-18 14:24:31 +02:00
|
|
|
func Args(cmd Cmd, min, max int) Cmd {
|
|
|
|
|
cmd.minPositional = min
|
|
|
|
|
cmd.maxPositional = max
|
|
|
|
|
return cmd
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-07 17:07:46 +01:00
|
|
|
// ShortForm can be used to define short-form flags for command line options. E.g:
|
2026-01-07 22:21:54 +01:00
|
|
|
// ShortForm(cmd, "f", "foo", "b", "bar", "z", "baz"). In which case the resulting command can be called as:
|
2026-01-07 17:07:46 +01:00
|
|
|
// my-command -f one -b two -z three. If say the Foo and Bar fields are of type boolean, then the flags can be
|
|
|
|
|
// grouped as:
|
|
|
|
|
// my-command -fbz three. The defined short forms apply to the entire command tree represented by the cmd
|
|
|
|
|
// parameter.
|
2025-08-26 14:12:18 +02:00
|
|
|
func ShortForm(cmd Cmd, f ...string) Cmd {
|
2025-09-01 02:07:48 +02:00
|
|
|
if len(f)%2 != 0 {
|
2025-12-30 16:52:10 +01:00
|
|
|
f = f[:len(f)-1]
|
2025-09-01 02:07:48 +02:00
|
|
|
}
|
|
|
|
|
|
2025-08-24 01:45:25 +02:00
|
|
|
cmd.shortForms = append(cmd.shortForms, f...)
|
|
|
|
|
for i := range cmd.subcommands {
|
2025-08-26 14:12:18 +02:00
|
|
|
cmd.subcommands[i] = ShortForm(
|
2025-08-24 01:45:25 +02:00
|
|
|
cmd.subcommands[i],
|
|
|
|
|
f...,
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
2025-08-18 14:24:31 +02:00
|
|
|
return cmd
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-07 17:07:46 +01:00
|
|
|
// Version inserts a subcommand that, when called, displays the provided version.
|
2025-08-26 03:21:35 +02:00
|
|
|
func Version(cmd Cmd, version string) Cmd {
|
|
|
|
|
cmd.subcommands = append(
|
|
|
|
|
cmd.subcommands,
|
|
|
|
|
Cmd{name: "version", version: version},
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
return cmd
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-07 17:07:46 +01:00
|
|
|
// ConfigFile defines a configuration file source. Configuration files, when defined, are used in the entire
|
|
|
|
|
// execution scope, regardless of which subcommand is called, and the subcommands interpret only those config
|
|
|
|
|
// file fields that apply to them.
|
|
|
|
|
//
|
|
|
|
|
// The configuration files are expected to use the ini file format, e.g values like foo_bar = 42 on separate
|
|
|
|
|
// lines. Repeated values are interpreted as distinct values for slice fields. Multiple config files are merged,
|
|
|
|
|
// such that value definitions in the config files with the higher index override the value definitions in the
|
|
|
|
|
// config files with lower indices. Discarding a value in a lower definition can be done with setting the key
|
|
|
|
|
// without a value, e.g: foo_bar. The formal definition of the used ini file format can be found here:
|
|
|
|
|
// ./ini.treerack.
|
|
|
|
|
//
|
|
|
|
|
// When a configuration file is not marked as optional with the OptionalConfig() function, it is expected to be
|
|
|
|
|
// provided by the user.
|
|
|
|
|
//
|
|
|
|
|
// Instead of using the static ConfigFile(name) definition, consider one or more of the dynamic definitions:
|
|
|
|
|
// Etc(), UserConfig(), ConfigFromOption() or SystemConfig().
|
2025-09-05 03:19:00 +02:00
|
|
|
func ConfigFile(name string) Config {
|
|
|
|
|
return Config{
|
|
|
|
|
file: func(Cmd) *file { return fileReader(name) },
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-07 17:07:46 +01:00
|
|
|
// Etc defines an optional system wide configuration file found at /etc/<name of the binary>/config.
|
2025-08-24 01:45:25 +02:00
|
|
|
func Etc() Config {
|
|
|
|
|
return OptionalConfig(Config{
|
2025-08-24 04:46:54 +02:00
|
|
|
file: func(cmd Cmd) *file {
|
2025-08-24 01:45:25 +02:00
|
|
|
return fileReader(path.Join("/etc", cmd.name, "config"))
|
|
|
|
|
},
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-07 17:07:46 +01:00
|
|
|
// UserConfig defines an optional user specific config file found at ~/.<name of the binary>/config or
|
|
|
|
|
// ~/.config/<name of the binary>/config.
|
2025-08-24 01:45:25 +02:00
|
|
|
func UserConfig() Config {
|
2025-08-26 14:12:18 +02:00
|
|
|
return OptionalConfig(
|
|
|
|
|
MergeConfig(
|
|
|
|
|
Config{
|
|
|
|
|
file: func(cmd Cmd) *file {
|
|
|
|
|
return fileReader(path.Join(os.Getenv("HOME"), fmt.Sprintf(".%s", cmd.name), "config"))
|
|
|
|
|
},
|
2025-08-24 04:46:54 +02:00
|
|
|
},
|
2025-08-26 14:12:18 +02:00
|
|
|
Config{
|
|
|
|
|
file: func(cmd Cmd) *file {
|
|
|
|
|
return fileReader(path.Join(os.Getenv("HOME"), ".config", cmd.name, "config"))
|
|
|
|
|
},
|
2025-08-24 04:46:54 +02:00
|
|
|
},
|
2025-08-26 14:12:18 +02:00
|
|
|
),
|
|
|
|
|
)
|
2025-08-24 01:45:25 +02:00
|
|
|
}
|
|
|
|
|
|
2026-01-07 17:07:46 +01:00
|
|
|
// ConfigFromOption defines zero or more optional config files provided by the command line option --config.
|
|
|
|
|
// When used, multiple such config files can be specified by the user.
|
2025-08-24 01:45:25 +02:00
|
|
|
func ConfigFromOption() Config {
|
|
|
|
|
return Config{fromOption: true}
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-07 17:07:46 +01:00
|
|
|
// SystemConfig defines a typical set of optional configuration files, merging the Etc(), UserConfig() and
|
|
|
|
|
// ConfigFromOption() definitions.
|
2025-08-24 01:45:25 +02:00
|
|
|
func SystemConfig() Config {
|
|
|
|
|
return MergeConfig(Etc(), UserConfig(), ConfigFromOption())
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-07 17:07:46 +01:00
|
|
|
// MergeConfig merges multiple configuration definitions.
|
|
|
|
|
func MergeConfig(conf ...Config) Config {
|
|
|
|
|
return Config{
|
|
|
|
|
merge: conf,
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// OptionalConfig marks a configuration file definition as optional. Without it, the configuration is expected
|
2026-01-07 22:21:54 +01:00
|
|
|
// to be present during the execution of the command.
|
2026-01-07 17:07:46 +01:00
|
|
|
func OptionalConfig(conf Config) Config {
|
|
|
|
|
conf.optional = true
|
|
|
|
|
for i := range conf.merge {
|
|
|
|
|
conf.merge[i] = OptionalConfig(conf.merge[i])
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return conf
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Exec executes a command.
|
|
|
|
|
//
|
|
|
|
|
// The implementation parameter can be either a command, or a function, in which case it gets automatically
|
|
|
|
|
// wrapped by a command.
|
2025-08-24 01:45:25 +02:00
|
|
|
func Exec(impl any, conf ...Config) {
|
|
|
|
|
exec(os.Stdin, os.Stdout, os.Stderr, os.Exit, wrap(impl), MergeConfig(conf...), os.Environ(), os.Args)
|
2025-08-18 14:24:31 +02:00
|
|
|
}
|