library documentation
This commit is contained in:
parent
96f3e02de8
commit
114ef27b6c
@ -2,9 +2,10 @@
|
|||||||
Generated with https://code.squareroundforest.org/arpio/docreflect
|
Generated with https://code.squareroundforest.org/arpio/docreflect
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
|
||||||
package wand
|
package wand
|
||||||
|
|
||||||
import "code.squareroundforest.org/arpio/docreflect"
|
import "code.squareroundforest.org/arpio/docreflect"
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
docreflect.Register("code.squareroundforest.org/arpio/wand/tools", "")
|
docreflect.Register("code.squareroundforest.org/arpio/wand/tools", "")
|
||||||
docreflect.Register("code.squareroundforest.org/arpio/wand/tools.Docreflect", "\nfunc(out, packageName, gopaths)")
|
docreflect.Register("code.squareroundforest.org/arpio/wand/tools.Docreflect", "\nfunc(out, packageName, gopaths)")
|
||||||
|
|||||||
@ -2,9 +2,10 @@
|
|||||||
Generated with https://code.squareroundforest.org/arpio/docreflect
|
Generated with https://code.squareroundforest.org/arpio/docreflect
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
|
||||||
package wand
|
package wand
|
||||||
|
|
||||||
import "code.squareroundforest.org/arpio/docreflect"
|
import "code.squareroundforest.org/arpio/docreflect"
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
docreflect.Register("code.squareroundforest.org/arpio/wand/internal/tests/testlib", "")
|
docreflect.Register("code.squareroundforest.org/arpio/wand/internal/tests/testlib", "")
|
||||||
docreflect.Register("code.squareroundforest.org/arpio/wand/internal/tests/testlib.Bar", "\nfunc(out, a, b, c)")
|
docreflect.Register("code.squareroundforest.org/arpio/wand/internal/tests/testlib.Bar", "\nfunc(out, a, b, c)")
|
||||||
|
|||||||
151
lib.go
151
lib.go
@ -1,5 +1,6 @@
|
|||||||
// Wand provides utilities for constructing command line applications from functions, with automatic parameter
|
// Wand provides utilities for constructing command line applications from functions, with automatic parameter
|
||||||
// binding from command line arguments, environment variables and configuration files.
|
// binding from command line arguments, environment variables and configuration files, and automatically
|
||||||
|
// generated help and documentation.
|
||||||
package wand
|
package wand
|
||||||
|
|
||||||
import (
|
import (
|
||||||
@ -8,7 +9,7 @@ import (
|
|||||||
"path"
|
"path"
|
||||||
)
|
)
|
||||||
|
|
||||||
// the parsing errors might be tricky
|
// Config represents one or more configuration sources.
|
||||||
type Config struct {
|
type Config struct {
|
||||||
file func(Cmd) *file
|
file func(Cmd) *file
|
||||||
merge []Config
|
merge []Config
|
||||||
@ -17,6 +18,7 @@ type Config struct {
|
|||||||
test string
|
test string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Cmd represents a command, a subcommand or a subcommand group.
|
||||||
type Cmd struct {
|
type Cmd struct {
|
||||||
name string
|
name string
|
||||||
impl any
|
impl any
|
||||||
@ -30,27 +32,104 @@ type Cmd struct {
|
|||||||
version string
|
version string
|
||||||
}
|
}
|
||||||
|
|
||||||
// name needs to be a valid symbol. The application name should also be a valid symbol,
|
// Command defines a command or a subcommand.
|
||||||
// though not mandatory. If it is not, the environment variables may not work properly.
|
//
|
||||||
|
// 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
|
||||||
|
// for the options is accepted both from command line flags or environment variables, and, if defined, from
|
||||||
|
// 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,
|
||||||
|
// this results in ambiguity, e.g. Foo.BarBaz and Foo.Bar.Baz. In such cases, the types of the ambigous fields
|
||||||
|
// 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
|
||||||
|
// the name of the execting binary, and are expected in lower or upper snake case. E.g. for a field InputValue:
|
||||||
|
// 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
|
||||||
|
// present, io.Reader is poplated by os.Stdin and io.Writer is populated by os.Stdout.
|
||||||
|
//
|
||||||
|
// 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
|
||||||
|
// values is a primitive value, it will be printed onto os.Stdout using the stdlib fmt package. Complex values
|
||||||
|
// 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
|
||||||
|
// under every command in the command tree, if a subcommand does not have already taken the name help. The help
|
||||||
|
// 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.
|
||||||
|
//
|
||||||
|
// When executing a command, there is two distinct stages of validation. The first one validates that the
|
||||||
|
// 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.
|
||||||
func Command(name string, impl any, subcmds ...Cmd) Cmd {
|
func Command(name string, impl any, subcmds ...Cmd) Cmd {
|
||||||
return Cmd{name: name, impl: impl, subcommands: subcmds}
|
return Cmd{name: name, impl: impl, subcommands: subcmds}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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.
|
||||||
func Group(name string, subcmds ...Cmd) Cmd {
|
func Group(name string, subcmds ...Cmd) Cmd {
|
||||||
return Cmd{name: name, group: true, subcommands: subcmds}
|
return Cmd{name: name, group: true, subcommands: subcmds}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Default sets a subcommand as the default, to be used in a group.
|
||||||
func Default(cmd Cmd) Cmd {
|
func Default(cmd Cmd) Cmd {
|
||||||
cmd.isDefault = true
|
cmd.isDefault = true
|
||||||
return cmd
|
return cmd
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Args can be used to specify the minimum and the maximum number of positional arguments when the
|
||||||
|
// implementation function has variadic parameters.
|
||||||
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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ShortForm can be used to define short-form flags for command line options. E.g:
|
||||||
|
// ShortForm(cmd, "f", "foo", "b", "bar", "z", "baz"). In which case the resulting command be called as:
|
||||||
|
// 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.
|
||||||
func ShortForm(cmd Cmd, f ...string) Cmd {
|
func ShortForm(cmd Cmd, f ...string) Cmd {
|
||||||
if len(f)%2 != 0 {
|
if len(f)%2 != 0 {
|
||||||
f = f[:len(f)-1]
|
f = f[:len(f)-1]
|
||||||
@ -67,6 +146,7 @@ func ShortForm(cmd Cmd, f ...string) Cmd {
|
|||||||
return cmd
|
return cmd
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Version inserts a subcommand that, when called, displays the provided version.
|
||||||
func Version(cmd Cmd, version string) Cmd {
|
func Version(cmd Cmd, version string) Cmd {
|
||||||
cmd.subcommands = append(
|
cmd.subcommands = append(
|
||||||
cmd.subcommands,
|
cmd.subcommands,
|
||||||
@ -76,28 +156,29 @@ func Version(cmd Cmd, version string) Cmd {
|
|||||||
return cmd
|
return cmd
|
||||||
}
|
}
|
||||||
|
|
||||||
func MergeConfig(conf ...Config) Config {
|
// ConfigFile defines a configuration file source. Configuration files, when defined, are used in the entire
|
||||||
return Config{
|
// execution scope, regardless of which subcommand is called, and the subcommands interpret only those config
|
||||||
merge: conf,
|
// 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,
|
||||||
func OptionalConfig(conf Config) Config {
|
// such that value definitions in the config files with the higher index override the value definitions in the
|
||||||
conf.optional = true
|
// config files with lower indices. Discarding a value in a lower definition can be done with setting the key
|
||||||
for i := range conf.merge {
|
// without a value, e.g: foo_bar. The formal definition of the used ini file format can be found here:
|
||||||
conf.merge[i] = OptionalConfig(conf.merge[i])
|
// ./ini.treerack.
|
||||||
}
|
//
|
||||||
|
// When a configuration file is not marked as optional with the OptionalConfig() function, it is expected to be
|
||||||
return conf
|
// 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().
|
||||||
func ConfigFile(name string) Config {
|
func ConfigFile(name string) Config {
|
||||||
return Config{
|
return Config{
|
||||||
file: func(Cmd) *file { return fileReader(name) },
|
file: func(Cmd) *file { return fileReader(name) },
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// the config will consider the command name as in the arguments
|
// Etc defines an optional system wide configuration file found at /etc/<name of the binary>/config.
|
||||||
func Etc() Config {
|
func Etc() Config {
|
||||||
return OptionalConfig(Config{
|
return OptionalConfig(Config{
|
||||||
file: func(cmd Cmd) *file {
|
file: func(cmd Cmd) *file {
|
||||||
@ -106,7 +187,8 @@ func Etc() Config {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// the config will consider the command name as in the arguments
|
// UserConfig defines an optional user specific config file found at ~/.<name of the binary>/config or
|
||||||
|
// ~/.config/<name of the binary>/config.
|
||||||
func UserConfig() Config {
|
func UserConfig() Config {
|
||||||
return OptionalConfig(
|
return OptionalConfig(
|
||||||
MergeConfig(
|
MergeConfig(
|
||||||
@ -124,15 +206,40 @@ func UserConfig() Config {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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.
|
||||||
func ConfigFromOption() Config {
|
func ConfigFromOption() Config {
|
||||||
return Config{fromOption: true}
|
return Config{fromOption: true}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SystemConfig defines a typical set of optional configuration files, merging the Etc(), UserConfig() and
|
||||||
|
// ConfigFromOption() definitions.
|
||||||
func SystemConfig() Config {
|
func SystemConfig() Config {
|
||||||
return MergeConfig(Etc(), UserConfig(), ConfigFromOption())
|
return MergeConfig(Etc(), UserConfig(), ConfigFromOption())
|
||||||
}
|
}
|
||||||
|
|
||||||
// the env will consider the command name as in the arguments
|
// 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
|
||||||
|
// to be present during executing the command.
|
||||||
|
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.
|
||||||
func Exec(impl any, conf ...Config) {
|
func Exec(impl any, conf ...Config) {
|
||||||
exec(os.Stdin, os.Stdout, os.Stderr, os.Exit, wrap(impl), MergeConfig(conf...), os.Environ(), os.Args)
|
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