From 114ef27b6c513be7e065dfd38cdeae5ec4a83e22 Mon Sep 17 00:00:00 2001 From: Arpad Ryszka Date: Wed, 7 Jan 2026 17:07:46 +0100 Subject: [PATCH] library documentation --- docreflect.gen.go | 57 ++++++++--------- docreflect_test.go | 31 +++++----- lib.go | 151 ++++++++++++++++++++++++++++++++++++++------- 3 files changed, 174 insertions(+), 65 deletions(-) diff --git a/docreflect.gen.go b/docreflect.gen.go index 3427e07..6469672 100644 --- a/docreflect.gen.go +++ b/docreflect.gen.go @@ -2,34 +2,35 @@ Generated with https://code.squareroundforest.org/arpio/docreflect */ - package wand + import "code.squareroundforest.org/arpio/docreflect" + func init() { -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.Exec", "\nfunc(o, stdin, stdout, args)") -docreflect.Register("code.squareroundforest.org/arpio/wand/tools.ExecOptions", "") -docreflect.Register("code.squareroundforest.org/arpio/wand/tools.ExecOptions.CacheDir", "") -docreflect.Register("code.squareroundforest.org/arpio/wand/tools.ExecOptions.ClearCache", "") -docreflect.Register("code.squareroundforest.org/arpio/wand/tools.ExecOptions.Import", "") -docreflect.Register("code.squareroundforest.org/arpio/wand/tools.ExecOptions.NoCache", "") -docreflect.Register("code.squareroundforest.org/arpio/wand/tools.ExecOptions.ReplaceModule", "") -docreflect.Register("code.squareroundforest.org/arpio/wand/tools.Man", "\nfunc(out, o, commandDir)") -docreflect.Register("code.squareroundforest.org/arpio/wand/tools.ManOptions", "") -docreflect.Register("code.squareroundforest.org/arpio/wand/tools.ManOptions.DateString", "") -docreflect.Register("code.squareroundforest.org/arpio/wand/tools.ManOptions.Version", "") -docreflect.Register("code.squareroundforest.org/arpio/wand/tools.Markdown", "\nfunc(out, o, commandDir)") -docreflect.Register("code.squareroundforest.org/arpio/wand/tools.MarkdownOptions", "") -docreflect.Register("code.squareroundforest.org/arpio/wand/tools.MarkdownOptions.Level", "") -docreflect.Register("code.squareroundforest.org/arpio/wand/tools.commandReader", "\nfunc(in)") -docreflect.Register("code.squareroundforest.org/arpio/wand/tools.execCommandDir", "\nfunc(out, commandDir, env)") -docreflect.Register("code.squareroundforest.org/arpio/wand/tools.execInput", "\nfunc(o, stdin, stdout, stderr, args)") -docreflect.Register("code.squareroundforest.org/arpio/wand/tools.execInternal", "\nfunc(command, args)") -docreflect.Register("code.squareroundforest.org/arpio/wand/tools.execWand", "\nfunc(o, stdin, stdout, stderr, args)") -docreflect.Register("code.squareroundforest.org/arpio/wand/tools.execc", "\nfunc(stdin, stdout, stderr, command, args, env)") -docreflect.Register("code.squareroundforest.org/arpio/wand/tools.hash", "\nfunc(expression, imports)") -docreflect.Register("code.squareroundforest.org/arpio/wand/tools.printGoFile", "\nfunc(fn, expression, imports)") -docreflect.Register("code.squareroundforest.org/arpio/wand/tools.readExec", "\nfunc(o, stdin, stdout, stderr)") -docreflect.Register("code.squareroundforest.org/arpio/wand/tools.selfPkg", "") -} \ No newline at end of file + 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.Exec", "\nfunc(o, stdin, stdout, args)") + docreflect.Register("code.squareroundforest.org/arpio/wand/tools.ExecOptions", "") + docreflect.Register("code.squareroundforest.org/arpio/wand/tools.ExecOptions.CacheDir", "") + docreflect.Register("code.squareroundforest.org/arpio/wand/tools.ExecOptions.ClearCache", "") + docreflect.Register("code.squareroundforest.org/arpio/wand/tools.ExecOptions.Import", "") + docreflect.Register("code.squareroundforest.org/arpio/wand/tools.ExecOptions.NoCache", "") + docreflect.Register("code.squareroundforest.org/arpio/wand/tools.ExecOptions.ReplaceModule", "") + docreflect.Register("code.squareroundforest.org/arpio/wand/tools.Man", "\nfunc(out, o, commandDir)") + docreflect.Register("code.squareroundforest.org/arpio/wand/tools.ManOptions", "") + docreflect.Register("code.squareroundforest.org/arpio/wand/tools.ManOptions.DateString", "") + docreflect.Register("code.squareroundforest.org/arpio/wand/tools.ManOptions.Version", "") + docreflect.Register("code.squareroundforest.org/arpio/wand/tools.Markdown", "\nfunc(out, o, commandDir)") + docreflect.Register("code.squareroundforest.org/arpio/wand/tools.MarkdownOptions", "") + docreflect.Register("code.squareroundforest.org/arpio/wand/tools.MarkdownOptions.Level", "") + docreflect.Register("code.squareroundforest.org/arpio/wand/tools.commandReader", "\nfunc(in)") + docreflect.Register("code.squareroundforest.org/arpio/wand/tools.execCommandDir", "\nfunc(out, commandDir, env)") + docreflect.Register("code.squareroundforest.org/arpio/wand/tools.execInput", "\nfunc(o, stdin, stdout, stderr, args)") + docreflect.Register("code.squareroundforest.org/arpio/wand/tools.execInternal", "\nfunc(command, args)") + docreflect.Register("code.squareroundforest.org/arpio/wand/tools.execWand", "\nfunc(o, stdin, stdout, stderr, args)") + docreflect.Register("code.squareroundforest.org/arpio/wand/tools.execc", "\nfunc(stdin, stdout, stderr, command, args, env)") + docreflect.Register("code.squareroundforest.org/arpio/wand/tools.hash", "\nfunc(expression, imports)") + docreflect.Register("code.squareroundforest.org/arpio/wand/tools.printGoFile", "\nfunc(fn, expression, imports)") + docreflect.Register("code.squareroundforest.org/arpio/wand/tools.readExec", "\nfunc(o, stdin, stdout, stderr)") + docreflect.Register("code.squareroundforest.org/arpio/wand/tools.selfPkg", "") +} diff --git a/docreflect_test.go b/docreflect_test.go index 8cf356a..10e7c94 100644 --- a/docreflect_test.go +++ b/docreflect_test.go @@ -2,21 +2,22 @@ Generated with https://code.squareroundforest.org/arpio/docreflect */ - package wand + import "code.squareroundforest.org/arpio/docreflect" + func init() { -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.Baz", "\nfunc(o)") -docreflect.Register("code.squareroundforest.org/arpio/wand/internal/tests/testlib.CustomHelp", "\nfunc(o)") -docreflect.Register("code.squareroundforest.org/arpio/wand/internal/tests/testlib.Foo", "Foo sums three numbers.\nIt prints the sum to stdout.\n\nThe input numbers can be any integer.\n\nfunc(a, b, c)") -docreflect.Register("code.squareroundforest.org/arpio/wand/internal/tests/testlib.OptionWithHelp", "") -docreflect.Register("code.squareroundforest.org/arpio/wand/internal/tests/testlib.OptionWithHelp.Help", "Custom help.\n") -docreflect.Register("code.squareroundforest.org/arpio/wand/internal/tests/testlib.Options", "") -docreflect.Register("code.squareroundforest.org/arpio/wand/internal/tests/testlib.Options.Bar", "Bars, any number.\n") -docreflect.Register("code.squareroundforest.org/arpio/wand/internal/tests/testlib.Options.Duration", "Duration is another option.\n") -docreflect.Register("code.squareroundforest.org/arpio/wand/internal/tests/testlib.Options.Foo", "Foo is an option.\n") -docreflect.Register("code.squareroundforest.org/arpio/wand/internal/tests/testlib.Options.Some", "Some is an option of any type.\n") -docreflect.Register("code.squareroundforest.org/arpio/wand/internal/tests/testlib.Options.Time", "Time is the third option here.\n") -} \ No newline at end of file + 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.Baz", "\nfunc(o)") + docreflect.Register("code.squareroundforest.org/arpio/wand/internal/tests/testlib.CustomHelp", "\nfunc(o)") + docreflect.Register("code.squareroundforest.org/arpio/wand/internal/tests/testlib.Foo", "Foo sums three numbers.\nIt prints the sum to stdout.\n\nThe input numbers can be any integer.\n\nfunc(a, b, c)") + docreflect.Register("code.squareroundforest.org/arpio/wand/internal/tests/testlib.OptionWithHelp", "") + docreflect.Register("code.squareroundforest.org/arpio/wand/internal/tests/testlib.OptionWithHelp.Help", "Custom help.\n") + docreflect.Register("code.squareroundforest.org/arpio/wand/internal/tests/testlib.Options", "") + docreflect.Register("code.squareroundforest.org/arpio/wand/internal/tests/testlib.Options.Bar", "Bars, any number.\n") + docreflect.Register("code.squareroundforest.org/arpio/wand/internal/tests/testlib.Options.Duration", "Duration is another option.\n") + docreflect.Register("code.squareroundforest.org/arpio/wand/internal/tests/testlib.Options.Foo", "Foo is an option.\n") + docreflect.Register("code.squareroundforest.org/arpio/wand/internal/tests/testlib.Options.Some", "Some is an option of any type.\n") + docreflect.Register("code.squareroundforest.org/arpio/wand/internal/tests/testlib.Options.Time", "Time is the third option here.\n") +} diff --git a/lib.go b/lib.go index ffc00f9..407f72e 100644 --- a/lib.go +++ b/lib.go @@ -1,5 +1,6 @@ // 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 import ( @@ -8,7 +9,7 @@ import ( "path" ) -// the parsing errors might be tricky +// Config represents one or more configuration sources. type Config struct { file func(Cmd) *file merge []Config @@ -17,6 +18,7 @@ type Config struct { test string } +// Cmd represents a command, a subcommand or a subcommand group. type Cmd struct { name string impl any @@ -30,27 +32,104 @@ type Cmd struct { 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. +// 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 +// 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 { 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 { 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 { cmd.isDefault = true 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 { cmd.minPositional = min cmd.maxPositional = max 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 { if len(f)%2 != 0 { f = f[:len(f)-1] @@ -67,6 +146,7 @@ func ShortForm(cmd Cmd, f ...string) Cmd { return cmd } +// Version inserts a subcommand that, when called, displays the provided version. func Version(cmd Cmd, version string) Cmd { cmd.subcommands = append( cmd.subcommands, @@ -76,28 +156,29 @@ func Version(cmd Cmd, version string) Cmd { 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 -} - +// 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(). 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 +// Etc defines an optional system wide configuration file found at /etc//config. func Etc() Config { return OptionalConfig(Config{ 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 ~/./config or +// ~/.config//config. func UserConfig() Config { return OptionalConfig( 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 { return Config{fromOption: true} } +// SystemConfig defines a typical set of optional configuration files, merging the Etc(), UserConfig() and +// ConfigFromOption() definitions. func SystemConfig() Config { 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) { exec(os.Stdin, os.Stdout, os.Stderr, os.Exit, wrap(impl), MergeConfig(conf...), os.Environ(), os.Args) }