wand/help_test.go
2025-12-10 20:31:10 +01:00

1833 lines
34 KiB
Go

package wand
import (
"code.squareroundforest.org/arpio/wand/internal/tests/testlib"
"testing"
)
func TestSuggestHelp(t *testing.T) {
t.Run("help command", func(t *testing.T) {
const expect = `Show help:
foo help`
cmd := Command("foo", func() {})
execTest(t, testCase{impl: cmd, command: "foo bar"}, expect)
})
t.Run("help option", func(t *testing.T) {
const expect = `Show help:
foo --help`
cmd := Command("foo", func() {}, Command("help", func() {}))
execTest(t, testCase{impl: cmd, command: "foo bar"}, expect)
})
t.Run("no implementation", func(t *testing.T) {
const expect = `Show help:
foo --help`
cmd := Group("foo", Command("help", func() {}))
execTest(t, testCase{impl: cmd, command: "foo baz"}, expect)
})
}
func TestHelp(t *testing.T) {
t.Run("no impl", func(t *testing.T) {
const expect = `foo
Synopsis:
foo <subcommand>
Options:
--help: Show help.
Subcommands:
bar:
help: Show help.
Show help for each subcommand by calling foo <subcommand> help or foo
<subcommand> --help.`
cmd := Group("foo", Command("bar", func() {}))
execTest(t, testCase{impl: cmd, command: "foo help"}, "", expect)
})
t.Run("no impl option", func(t *testing.T) {
const expect = `foo
Synopsis:
foo <subcommand>
Options:
--help: Show help.
Subcommands:
bar:
help: Show help.
Show help for each subcommand by calling foo <subcommand> help or foo
<subcommand> --help.`
cmd := Group("foo", Command("bar", func() {}))
execTest(t, testCase{impl: cmd, command: "foo --help"}, "", expect)
})
t.Run("empty", func(t *testing.T) {
const expect = `foo
Synopsis:
foo [options]...
foo <subcommand>
Options:
--help: Show help.
Subcommands:
help: Show help.`
f := func() {}
execTest(t, testCase{impl: f, command: "foo help"}, "", expect)
})
t.Run("empty option", func(t *testing.T) {
const expect = `foo
Synopsis:
foo [options]...
foo <subcommand>
Options:
--help: Show help.
Subcommands:
help: Show help.`
f := func() {}
execTest(t, testCase{impl: f, command: "foo --help"}, "", expect)
})
t.Run("args", func(t *testing.T) {
const expect = `foo
Synopsis:
foo [options]... [--] <arg1 int> <arg2 int> <arg3 string>
foo <subcommand>
Options:
--help: Show help.
Subcommands:
help: Show help.`
f := func(a, b int, c string) {}
execTest(t, testCase{impl: f, command: "foo help"}, "", expect)
})
t.Run("args option", func(t *testing.T) {
const expect = `foo
Synopsis:
foo [options]... [--] <arg1 int> <arg2 int> <arg3 string>
foo <subcommand>
Options:
--help: Show help.
Subcommands:
help: Show help.`
f := func(a, b int, c string) {}
execTest(t, testCase{impl: f, command: "foo --help"}, "", expect)
})
t.Run("variadic args", func(t *testing.T) {
const expect = `foo
Synopsis:
foo [options]... [--] <arg1 int> [arg2 int]...
foo <subcommand>
Options:
--help: Show help.
Subcommands:
help: Show help.`
f := func(a int, b ...int) {}
execTest(t, testCase{impl: f, command: "foo help"}, "", expect)
})
t.Run("min variadic args", func(t *testing.T) {
const expect = `foo
Synopsis:
foo [options]... [--] <arg1 int> <arg2 int>...
foo <subcommand>
Expecting min 2 total number of arguments.
Options:
--help: Show help.
Subcommands:
help: Show help.`
cmd := Args(Command("foo", func(a int, b ...int) {}), 2, 0)
execTest(t, testCase{impl: cmd, command: "foo help"}, "", expect)
})
t.Run("max variadic args", func(t *testing.T) {
const expect = `foo
Synopsis:
foo [options]... [--] <arg1 int> [arg2 int]...
foo <subcommand>
Expecting max 3 total number of arguments.
Options:
--help: Show help.
Subcommands:
help: Show help.`
cmd := Args(Command("foo", func(a int, b ...int) {}), 0, 3)
execTest(t, testCase{impl: cmd, command: "foo help"}, "", expect)
})
t.Run("min and max variadic args", func(t *testing.T) {
const expect = `foo
Synopsis:
foo [options]... [--] <arg1 int> <arg2 int>...
foo <subcommand>
Expecting min 2 and max 3 total number of arguments.
Options:
--help: Show help.
Subcommands:
help: Show help.`
cmd := Args(Command("foo", func(a int, b ...int) {}), 2, 3)
execTest(t, testCase{impl: cmd, command: "foo help"}, "", expect)
})
t.Run("options", func(t *testing.T) {
const expect = `foo
Synopsis:
foo [options]...
foo <subcommand>
Options:
--bar string [*]: Bars, any number.
--duration duration: Duration is another option.
--foo int: Foo is an option.
--time time: Time is the third option here.
--help: Show help.
Hints:
- Options marked with [*] accept any number of instances.
- Option values can be set both via = or just separated by space.
Subcommands:
help: Show help.`
execTest(t, testCase{impl: testlib.Baz, command: "foo help"}, "", expect)
})
t.Run("options option", func(t *testing.T) {
const expect = `foo
Synopsis:
foo [options]...
foo <subcommand>
Options:
--bar string [*]: Bars, any number.
--duration duration: Duration is another option.
--foo int: Foo is an option.
--time time: Time is the third option here.
--help: Show help.
Hints:
- Options marked with [*] accept any number of instances.
- Option values can be set both via = or just separated by space.
Subcommands:
help: Show help.`
execTest(t, testCase{impl: testlib.Baz, command: "foo --help"}, "", expect)
})
t.Run("help option shadowed", func(t *testing.T) {
const expect = `foo
Synopsis:
foo [options]...
foo <subcommand>
Options:
--help bool: Custom help.
Hints:
- Bool options can be used with implicit true values.
- Option values can be set both via = or just separated by space.
Subcommands:
help: Show help.`
execTest(t, testCase{impl: testlib.CustomHelp, command: "foo help"}, "", expect)
})
t.Run("config option", func(t *testing.T) {
const expect = `foo
Synopsis:
foo [options]...
foo <subcommand>
Options:
--config: Configuration file.
--help: Show help.
Subcommands:
help: Show help.`
execTest(
t,
testCase{impl: func() {}, mergeConfTyped: []Config{ConfigFromOption()}, command: "foo help"},
"",
expect,
)
})
t.Run("grouped options", func(t *testing.T) {
const expect = `foo
Synopsis:
foo [options]...
foo <subcommand>
Options:
--bar, -b string:
--foo, -f bool:
--help: Show help.
Hints:
- Bool options can be used with implicit true values.
- The short form of bool options can be combined. The last short form
does not need to be a bool option. E.g. -abc=42.
- Option values can be set both via = or just separated by space.
Subcommands:
help: Show help.`
execTest(
t,
testCase{
impl: ShortForm(
Command("foo", func(struct {
Foo bool
Bar string
}) {}),
"f", "foo", "b", "bar",
),
command: "foo help",
},
"",
expect,
)
})
t.Run("single option hint", func(t *testing.T) {
const expect = `foo
Synopsis:
foo [options]...
foo <subcommand>
Options:
--foo int:
--help: Show help.
Option values can be set both via = or just separated by space.
Subcommands:
help: Show help.`
execTest(
t,
testCase{
impl: func(struct{ Foo int }) {},
command: "foo help",
},
"",
expect,
)
})
t.Run("description", func(t *testing.T) {
const expect = `foo - Foo sums three numbers
Synopsis:
foo [options]... [--] <a int> <b int> <c int>
foo <subcommand>
Foo sums three numbers. It prints the sum to stdout.
The input numbers can be any integer.
Options:
--help: Show help.
Subcommands:
help: Show help.`
execTest(t, testCase{impl: testlib.Foo, command: "foo help"}, "", expect)
})
t.Run("help subcommand shadowed", func(t *testing.T) {
const expect = `foo
Synopsis:
foo [options]...
foo <subcommand>
Options:
--help: Show help.
Subcommands:
help:
Show help for each subcommand by calling foo <subcommand> help or foo
<subcommand> --help.`
execTest(
t,
testCase{impl: Command("foo", func() {}, Command("help", func() {})), command: "foo --help"},
"",
expect,
)
})
t.Run("default subcommand", func(t *testing.T) {
const expect = `foo
Synopsis:
foo [options]... [--] <arg1 int>
foo <subcommand>
Options:
--help: Show help.
Subcommands:
bar (default):
help: Show help.
Show help for each subcommand by calling foo <subcommand> help or foo
<subcommand> --help.`
execTest(
t,
testCase{impl: Group("foo", Default(Command("bar", func(a int) {}))), command: "foo help"},
"",
expect,
)
})
t.Run("subcommand description", func(t *testing.T) {
const expect = `bar
Synopsis:
bar <subcommand>
Options:
--help: Show help.
Subcommands:
foo: Foo sums three numbers
help: Show help.
version: Show version information.
Show help for each subcommand by calling bar <subcommand> help or bar
<subcommand> --help.`
execTest(
t,
testCase{impl: Version(Group("bar", Command("foo", testlib.Foo)), "v1"), command: "bar help"},
"",
expect,
)
})
}
func TestMan(t *testing.T) {
t.Run("basic", func(t *testing.T) {
const expect = `.TH "foo" 1 "December 2025" "2025-12-03-abcd123" "User Commands"
.br
.sp 1v
.in 0
.ti 0
\fBName:\fR
.br
.sp 1v
.in 4
.ti 4
foo - Foo sums three numbers
.br
.sp 1v
.in 0
.ti 0
\fBSynopsis:\fR
.br
.sp 1v
.nf
foo [options]... [--] <a int> <b int> <c int>
foo <subcommand>
.fi
.br
.sp 1v
.in 0
.ti 0
\fBDescription:\fR
.br
.sp 1v
.in 4
.ti 4
Foo sums three numbers. It prints the sum to stdout.
.br
.sp 1v
.in 4
.ti 4
The input numbers can be any integer.
.br
.sp 1v
.in 0
.ti 0
\fBOptions:\fR
.br
.sp 1v
.in 12
.ti 4
--help:\~Show help.
.br
.sp 1v
.in 0
.ti 0
\fBSubcommands:\fR
.br
.sp 1v
.in 10
.ti 4
help:\~Show help.`
execTest(
t,
testCase{
impl: testlib.Foo,
command: "foo",
env: "_wandgenerate=man;_wandgeneratedate=2025-12-03;_wandgenerateversion=2025-12-03-abcd123",
},
"",
expect,
)
})
t.Run("with subcommand", func(t *testing.T) {
const expect = `.TH "foo" 1 "December 2025" "2025-12-03-abcd123" "User Commands"
.br
.sp 1v
.in 0
.ti 0
\fBName:\fR
.br
.sp 1v
.in 4
.ti 4
foo
.br
.sp 1v
.in 0
.ti 0
\fBSynopsis:\fR
.br
.sp 1v
.nf
foo [options]...
foo <subcommand>
.fi
.br
.sp 1v
.in 0
.ti 0
\fBOptions:\fR
.br
.sp 1v
.in 12
.ti 4
--help:\~Show help.
.br
.sp 1v
.in 0
.ti 0
\fBSubcommands:\fR
.br
.sp 1v
.in 0
.ti 0
Show help for each subcommand by calling <command> help or <command> --help.
.br
.sp 1v
.in 2
.ti 2
\fBfoo bar\fR
.br
.sp 1v
.in 4
.ti 4
\fBSynopsis:\fR
.br
.sp 1v
.nf
foo bar [options]...
foo bar <subcommand>
.fi
.br
.sp 1v
.in 4
.ti 4
\fBOptions:\fR
.br
.sp 1v
.in 12
.ti 4
--help:\~Show help.`
execTest(
t,
testCase{
impl: Command("foo", func() {}, Command("bar", func() {})),
command: "foo",
env: "_wandgenerate=man;_wandgeneratedate=2025-12-03;_wandgenerateversion=2025-12-03-abcd123",
},
"",
expect,
)
})
t.Run("types of subcommand", func(t *testing.T) {
const expect = `.TH "foo" 1 "December 2025" "2025-12-03-abcd123" "User Commands"
.br
.sp 1v
.in 0
.ti 0
\fBName:\fR
.br
.sp 1v
.in 4
.ti 4
foo
.br
.sp 1v
.in 0
.ti 0
\fBSynopsis:\fR
.br
.sp 1v
.nf
foo [options]...
foo <subcommand>
.fi
.br
.sp 1v
.in 0
.ti 0
\fBOptions:\fR
.br
.sp 1v
.in 12
.ti 4
--help:\~Show help.
.br
.sp 1v
.in 0
.ti 0
\fBSubcommands:\fR
.br
.sp 1v
.in 0
.ti 0
Show help for each subcommand by calling <command> help or <command> --help.
.br
.sp 1v
.in 2
.ti 2
\fBfoo bar (default)\fR
.br
.sp 1v
.in 4
.ti 4
\fBSynopsis:\fR
.br
.sp 1v
.nf
foo bar [options]...
foo bar <subcommand>
.fi
.br
.sp 1v
.in 4
.ti 4
\fBOptions:\fR
.br
.sp 1v
.in 12
.ti 4
--help:\~Show help.
.br
.sp 1v
.in 2
.ti 2
\fBfoo real-foo\fR
.br
.sp 1v
.in 4
.ti 4
\fBSynopsis:\fR
.br
.sp 1v
.nf
foo real-foo [options]... [--] <a int> <b int> <c int>
foo real-foo <subcommand>
.fi
.br
.sp 1v
.in 4
.ti 4
\fBDescription:\fR
.br
.sp 1v
.in 4
.ti 4
Foo sums three numbers. It prints the sum to stdout.
.br
.sp 1v
.in 4
.ti 4
The input numbers can be any integer.
.br
.sp 1v
.in 4
.ti 4
\fBOptions:\fR
.br
.sp 1v
.in 12
.ti 4
--help:\~Show help.
.br
.sp 1v
.in 2
.ti 2
\fBfoo version\fR
.br
.sp 1v
.in 4
.ti 4
Show version.`
execTest(
t,
testCase{
impl: Version(
Group("foo", Default(Command("bar", func() {})), Command("real-foo", testlib.Foo)),
"v1",
),
command: "foo",
env: "_wandgenerate=man;_wandgeneratedate=2025-12-03;_wandgenerateversion=2025-12-03-abcd123",
},
"",
expect,
)
})
t.Run("sub-subcommands", func(t *testing.T) {
const expect = `.TH "foo" 1 "December 2025" "2025-12-03-abcd123" "User Commands"
.br
.sp 1v
.in 0
.ti 0
\fBName:\fR
.br
.sp 1v
.in 4
.ti 4
foo
.br
.sp 1v
.in 0
.ti 0
\fBSynopsis:\fR
.br
.sp 1v
.nf
foo [options]...
foo <subcommand>
.fi
.br
.sp 1v
.in 0
.ti 0
\fBOptions:\fR
.br
.sp 1v
.in 12
.ti 4
--help:\~Show help.
.br
.sp 1v
.in 0
.ti 0
\fBSubcommands:\fR
.br
.sp 1v
.in 0
.ti 0
Show help for each subcommand by calling <command> help or <command> --help.
.br
.sp 1v
.in 2
.ti 2
\fBfoo bar\fR
.br
.sp 1v
.in 4
.ti 4
\fBSynopsis:\fR
.br
.sp 1v
.nf
foo bar [options]...
foo bar <subcommand>
.fi
.br
.sp 1v
.in 4
.ti 4
\fBOptions:\fR
.br
.sp 1v
.in 12
.ti 4
--help:\~Show help.
.br
.sp 1v
.in 2
.ti 2
\fBfoo bar baz\fR
.br
.sp 1v
.in 4
.ti 4
\fBSynopsis:\fR
.br
.sp 1v
.nf
foo bar baz [options]...
foo bar baz <subcommand>
.fi
.br
.sp 1v
.in 4
.ti 4
\fBOptions:\fR
.br
.sp 1v
.in 12
.ti 4
--help:\~Show help.`
execTest(
t,
testCase{
impl: Command("foo", func() {}, Command("bar", func() {}, Command("baz", func() {}))),
command: "foo",
env: "_wandgenerate=man;_wandgeneratedate=2025-12-03;_wandgenerateversion=2025-12-03-abcd123",
},
"",
expect,
)
})
t.Run("options", func(t *testing.T) {
const expect = `.TH "foo" 1 "December 2025" "2025-12-03-abcd123" "User Commands"
.br
.sp 1v
.in 0
.ti 0
\fBName:\fR
.br
.sp 1v
.in 4
.ti 4
foo
.br
.sp 1v
.in 0
.ti 0
\fBSynopsis:\fR
.br
.sp 1v
.nf
foo [options]...
foo <subcommand>
.fi
.br
.sp 1v
.in 0
.ti 0
\fBOptions:\fR
.br
.sp 1v
.in 25
.ti 4
--bar string [*]:\~\~\~\~Bars, any number.
.br
.in 25
.ti 4
--duration duration:\~Duration is another option.
.br
.in 25
.ti 4
--foo int:\~\~\~\~\~\~\~\~\~\~\~Foo is an option.
.br
.in 25
.ti 4
--time time:\~\~\~\~\~\~\~\~\~Time is the third option here.
.br
.in 25
.ti 4
--config:\~\~\~\~\~\~\~\~\~\~\~\~Configuration file.
.br
.in 25
.ti 4
--help:\~\~\~\~\~\~\~\~\~\~\~\~\~\~Show help.
.br
.sp 1v
.in 4
.ti 4
Hints:
.br
.sp 1v
.in 6
.ti 4
\(bu\~Options marked with [*] accept any number of instances.
.br
.in 6
.ti 4
\(bu\~Option values can be set both via = or just separated by space.
.br
.sp 1v
.in 0
.ti 0
\fBSubcommands:\fR
.br
.sp 1v
.in 0
.ti 0
Show help for each subcommand by calling <command> help or <command> --help.
.br
.sp 1v
.in 2
.ti 2
\fBfoo bar\fR
.br
.sp 1v
.in 4
.ti 4
\fBSynopsis:\fR
.br
.sp 1v
.nf
foo bar [options]...
foo bar <subcommand>
.fi
.br
.sp 1v
.in 4
.ti 4
\fBOptions:\fR
.br
.sp 1v
.in 15
.ti 4
--bar int:\~
.br
.in 15
.ti 4
--config:\~\~Configuration file.
.br
.in 15
.ti 4
--help:\~\~\~\~Show help.
.br
.sp 1v
.in 0
.ti 0
\fBEnvironment variables:\fR
.br
.sp 1v
.in 4
.ti 4
Every command line option's value can also be provided as an environment variable. Environment variable names need to use snake casing like myapp_foo_bar_baz or MYAPP_FOO_BAR_BAZ, or other casing that doesn't include the '-' dash character, and they need to be prefixed with the name of the application, as in the base name of the command.
.br
.sp 1v
.in 4
.ti 4
When both the environment variable and the command line option is defined, the command line option overrides the environment variable. Multiple values for the same environment variable can be defined by concatenating the values with the ':' separator character. When overriding multiple values with command line options, all the environment values of the same field are dropped.
.br
.sp 1v
.in 4
.ti 4
\fBExample environment variable:\fR
.br
.sp 1v
.nf
FOO_FOO=42
.fi
.br
.sp 1v
.in 0
.ti 0
\fBConfiguration:\fR
.br
.sp 1v
.in 4
.ti 4
Every command line option's value can also be provided as an entry in a configuration file.
.br
.sp 1v
.in 4
.ti 4
Configuration file entries can use keys with different casings, e.g. snake case foo_bar_baz, or kebab case foo-bar-baz. The keys of the entries can use a limited set of characters: [a-zA-Z0-9_-], and the first character needs to be one of [a-zA-Z_]. Entry values can consist of any characters, except for newline, control characters, " (quote) and \\ (backslash), or the values can be quoted, in which case they can consist of any characters, spanning multiple lines, and only the " (quote) and \\ (backslash) characters need to be escaped by the \\ (backslash) character.
.br
.sp 1v
.in 4
.ti 4
Configuration files allow multiple entries with the same key, when if the associated command line option also allows multiple instances (marked with [*]). When an entry is defined multiple configuration files, the effective value is overridden in the order of the definition of the possible config files (see the listing order below). To discard values defined in the overridden config files without defining new ones, we can set entries with only the key, omitting the = key/value separator. Entries in the config files are overridden by the environment variables, when defined, and by the command line options when defined.
.br
.sp 1v
.in 4
.ti 4
Config files marked as optional don't need to be present in the file system, but if they exist, then they must contain valid configuration syntax which is wand's flavor of .ini files (https://code.squareroundforest.org/arpio/wand/src/branch/main/ini.treerack).
.br
.sp 1v
.in 4
.ti 4
\fBConfiguration files:\fR
.br
.sp 1v
.in 10
.ti 8
\(bu\~zero or more configuration files defined by the --config option
.br
.sp 1v
.in 4
.ti 4
\fBExample configuration entry:\fR
.br
.sp 1v
.nf
# Default value for --foo:
foo = 42
.fi
.br
.sp 1v
.in 4
.ti 4
\fBExample for discarding an inherited entry:\fR
.br
.sp 1v
.nf
# Discarding an inherited entry:
foo
.fi`
execTest(
t,
testCase{
impl: Command("foo", testlib.Baz, Command("bar", func(struct{ Bar int }) {})),
command: "foo",
env: "_wandgenerate=man;_wandgeneratedate=2025-12-03;_wandgenerateversion=2025-12-03-abcd123",
mergeConfTyped: []Config{ConfigFromOption()},
},
"",
expect,
)
})
t.Run("short form grouping", func(t *testing.T) {
const expect = `.TH "foo" 1 "December 2025" "2025-12-03-abcd123" "User Commands"
.br
.sp 1v
.in 0
.ti 0
\fBName:\fR
.br
.sp 1v
.in 4
.ti 4
foo
.br
.sp 1v
.in 0
.ti 0
\fBSynopsis:\fR
.br
.sp 1v
.nf
foo [options]...
foo <subcommand>
.fi
.br
.sp 1v
.in 0
.ti 0
\fBOptions:\fR
.br
.sp 1v
.in 20
.ti 4
--bar, -b int:\~\~
.br
.in 20
.ti 4
--foo, -f bool:\~
.br
.in 20
.ti 4
--help:\~\~\~\~\~\~\~\~\~Show help.
.br
.sp 1v
.in 4
.ti 4
Hints:
.br
.sp 1v
.in 6
.ti 4
\(bu\~Bool options can be used with implicit true values.
.br
.in 6
.ti 4
\(bu\~The short form of bool options can be combined. The last short form does not need to be a bool option. E.g. -abc=42.
.br
.in 6
.ti 4
\(bu\~Option values can be set both via = or just separated by space.
.br
.sp 1v
.in 0
.ti 0
\fBSubcommands:\fR
.br
.sp 1v
.in 10
.ti 4
help:\~Show help.
.br
.sp 1v
.in 0
.ti 0
\fBEnvironment variables:\fR
.br
.sp 1v
.in 4
.ti 4
Every command line option's value can also be provided as an environment variable. Environment variable names need to use snake casing like myapp_foo_bar_baz or MYAPP_FOO_BAR_BAZ, or other casing that doesn't include the '-' dash character, and they need to be prefixed with the name of the application, as in the base name of the command.
.br
.sp 1v
.in 4
.ti 4
When both the environment variable and the command line option is defined, the command line option overrides the environment variable. Multiple values for the same environment variable can be defined by concatenating the values with the ':' separator character. When overriding multiple values with command line options, all the environment values of the same field are dropped.
.br
.sp 1v
.in 4
.ti 4
\fBExample environment variable:\fR
.br
.sp 1v
.nf
FOO_FOO=true
.fi`
execTest(
t,
testCase{
impl: ShortForm(Command("foo", func(struct {
Foo bool
Bar int
}) {}), "f", "foo", "b", "bar"),
command: "foo",
env: "_wandgenerate=man;_wandgeneratedate=2025-12-03;_wandgenerateversion=2025-12-03-abcd123",
},
"",
expect,
)
})
t.Run("command name unsupported by env", func(t *testing.T) {
const expect = `.TH "foo.bar" 1 "December 2025" "2025-12-03-abcd123" "User Commands"
.br
.sp 1v
.in 0
.ti 0
\fBName:\fR
.br
.sp 1v
.in 4
.ti 4
foo.bar
.br
.sp 1v
.in 0
.ti 0
\fBSynopsis:\fR
.br
.sp 1v
.nf
foo.bar [options]...
foo.bar <subcommand>
.fi
.br
.sp 1v
.in 0
.ti 0
\fBOptions:\fR
.br
.sp 1v
.in 12
.ti 4
--help:\~Show help.
.br
.sp 1v
.in 0
.ti 0
\fBSubcommands:\fR
.br
.sp 1v
.in 10
.ti 4
help:\~Show help.`
execTest(
t,
testCase{
impl: func() {},
command: "foo.bar",
env: "_wandgenerate=man;_wandgeneratedate=2025-12-03;_wandgenerateversion=2025-12-03-abcd123",
},
"",
expect,
)
})
t.Run("standard config", func(t *testing.T) {
const expect = `.TH "foo" 1 "December 2025" "2025-12-03-abcd123" "User Commands"
.br
.sp 1v
.in 0
.ti 0
\fBName:\fR
.br
.sp 1v
.in 4
.ti 4
foo
.br
.sp 1v
.in 0
.ti 0
\fBSynopsis:\fR
.br
.sp 1v
.nf
foo [options]...
foo <subcommand>
.fi
.br
.sp 1v
.in 0
.ti 0
\fBOptions:\fR
.br
.sp 1v
.in 15
.ti 4
--foo int:\~
.br
.in 15
.ti 4
--config:\~\~Configuration file.
.br
.in 15
.ti 4
--help:\~\~\~\~Show help.
.br
.sp 1v
.in 4
.ti 4
Option values can be set both via = or just separated by space.
.br
.sp 1v
.in 0
.ti 0
\fBSubcommands:\fR
.br
.sp 1v
.in 10
.ti 4
help:\~Show help.
.br
.sp 1v
.in 0
.ti 0
\fBEnvironment variables:\fR
.br
.sp 1v
.in 4
.ti 4
Every command line option's value can also be provided as an environment variable. Environment variable names need to use snake casing like myapp_foo_bar_baz or MYAPP_FOO_BAR_BAZ, or other casing that doesn't include the '-' dash character, and they need to be prefixed with the name of the application, as in the base name of the command.
.br
.sp 1v
.in 4
.ti 4
When both the environment variable and the command line option is defined, the command line option overrides the environment variable. Multiple values for the same environment variable can be defined by concatenating the values with the ':' separator character. When overriding multiple values with command line options, all the environment values of the same field are dropped.
.br
.sp 1v
.in 4
.ti 4
\fBExample environment variable:\fR
.br
.sp 1v
.nf
FOO_FOO=42
.fi
.br
.sp 1v
.in 0
.ti 0
\fBConfiguration:\fR
.br
.sp 1v
.in 4
.ti 4
Every command line option's value can also be provided as an entry in a configuration file.
.br
.sp 1v
.in 4
.ti 4
Configuration file entries can use keys with different casings, e.g. snake case foo_bar_baz, or kebab case foo-bar-baz. The keys of the entries can use a limited set of characters: [a-zA-Z0-9_-], and the first character needs to be one of [a-zA-Z_]. Entry values can consist of any characters, except for newline, control characters, " (quote) and \\ (backslash), or the values can be quoted, in which case they can consist of any characters, spanning multiple lines, and only the " (quote) and \\ (backslash) characters need to be escaped by the \\ (backslash) character.
.br
.sp 1v
.in 4
.ti 4
Configuration files allow multiple entries with the same key, when if the associated command line option also allows multiple instances (marked with [*]). When an entry is defined multiple configuration files, the effective value is overridden in the order of the definition of the possible config files (see the listing order below). To discard values defined in the overridden config files without defining new ones, we can set entries with only the key, omitting the = key/value separator. Entries in the config files are overridden by the environment variables, when defined, and by the command line options when defined.
.br
.sp 1v
.in 4
.ti 4
Config files marked as optional don't need to be present in the file system, but if they exist, then they must contain valid configuration syntax which is wand's flavor of .ini files (https://code.squareroundforest.org/arpio/wand/src/branch/main/ini.treerack).
.br
.sp 1v
.in 4
.ti 4
\fBConfiguration files:\fR
.br
.sp 1v
.in 10
.ti 8
\(bu\~/etc/foo/config (optional)
.br
.in 10
.ti 8
\(bu\~/Users/arpio/.foo/config (optional)
.br
.in 10
.ti 8
\(bu\~/Users/arpio/.config/foo/config (optional)
.br
.in 10
.ti 8
\(bu\~zero or more configuration files defined by the --config option
.br
.sp 1v
.in 4
.ti 4
\fBExample configuration entry:\fR
.br
.sp 1v
.nf
# Default value for --foo:
foo = 42
.fi
.br
.sp 1v
.in 4
.ti 4
\fBExample for discarding an inherited entry:\fR
.br
.sp 1v
.nf
# Discarding an inherited entry:
foo
.fi`
execTest(
t,
testCase{
impl: func(struct{ Foo int }) {},
command: "foo",
env: "_wandgenerate=man;_wandgeneratedate=2025-12-03;_wandgenerateversion=2025-12-03-abcd123",
mergeConfTyped: []Config{SystemConfig()},
},
"",
expect,
)
})
t.Run("bool option in env and config", func(t *testing.T) {
const expect = `.TH "foo" 1 "December 2025" "2025-12-03-abcd123" "User Commands"
.br
.sp 1v
.in 0
.ti 0
\fBName:\fR
.br
.sp 1v
.in 4
.ti 4
foo
.br
.sp 1v
.in 0
.ti 0
\fBSynopsis:\fR
.br
.sp 1v
.nf
foo [options]...
foo <subcommand>
.fi
.br
.sp 1v
.in 0
.ti 0
\fBOptions:\fR
.br
.sp 1v
.in 16
.ti 4
--foo bool:\~
.br
.in 16
.ti 4
--config:\~\~\~Configuration file.
.br
.in 16
.ti 4
--help:\~\~\~\~\~Show help.
.br
.sp 1v
.in 4
.ti 4
Hints:
.br
.sp 1v
.in 6
.ti 4
\(bu\~Bool options can be used with implicit true values.
.br
.in 6
.ti 4
\(bu\~Option values can be set both via = or just separated by space.
.br
.sp 1v
.in 0
.ti 0
\fBSubcommands:\fR
.br
.sp 1v
.in 10
.ti 4
help:\~Show help.
.br
.sp 1v
.in 0
.ti 0
\fBEnvironment variables:\fR
.br
.sp 1v
.in 4
.ti 4
Every command line option's value can also be provided as an environment variable. Environment variable names need to use snake casing like myapp_foo_bar_baz or MYAPP_FOO_BAR_BAZ, or other casing that doesn't include the '-' dash character, and they need to be prefixed with the name of the application, as in the base name of the command.
.br
.sp 1v
.in 4
.ti 4
When both the environment variable and the command line option is defined, the command line option overrides the environment variable. Multiple values for the same environment variable can be defined by concatenating the values with the ':' separator character. When overriding multiple values with command line options, all the environment values of the same field are dropped.
.br
.sp 1v
.in 4
.ti 4
\fBExample environment variable:\fR
.br
.sp 1v
.nf
FOO_FOO=true
.fi
.br
.sp 1v
.in 0
.ti 0
\fBConfiguration:\fR
.br
.sp 1v
.in 4
.ti 4
Every command line option's value can also be provided as an entry in a configuration file.
.br
.sp 1v
.in 4
.ti 4
Configuration file entries can use keys with different casings, e.g. snake case foo_bar_baz, or kebab case foo-bar-baz. The keys of the entries can use a limited set of characters: [a-zA-Z0-9_-], and the first character needs to be one of [a-zA-Z_]. Entry values can consist of any characters, except for newline, control characters, " (quote) and \\ (backslash), or the values can be quoted, in which case they can consist of any characters, spanning multiple lines, and only the " (quote) and \\ (backslash) characters need to be escaped by the \\ (backslash) character.
.br
.sp 1v
.in 4
.ti 4
Configuration files allow multiple entries with the same key, when if the associated command line option also allows multiple instances (marked with [*]). When an entry is defined multiple configuration files, the effective value is overridden in the order of the definition of the possible config files (see the listing order below). To discard values defined in the overridden config files without defining new ones, we can set entries with only the key, omitting the = key/value separator. Entries in the config files are overridden by the environment variables, when defined, and by the command line options when defined.
.br
.sp 1v
.in 4
.ti 4
Config files marked as optional don't need to be present in the file system, but if they exist, then they must contain valid configuration syntax which is wand's flavor of .ini files (https://code.squareroundforest.org/arpio/wand/src/branch/main/ini.treerack).
.br
.sp 1v
.in 4
.ti 4
\fBConfiguration files:\fR
.br
.sp 1v
.in 10
.ti 8
\(bu\~/etc/foo/config (optional)
.br
.in 10
.ti 8
\(bu\~/Users/arpio/.foo/config (optional)
.br
.in 10
.ti 8
\(bu\~/Users/arpio/.config/foo/config (optional)
.br
.in 10
.ti 8
\(bu\~zero or more configuration files defined by the --config option
.br
.sp 1v
.in 4
.ti 4
\fBExample configuration entry:\fR
.br
.sp 1v
.nf
# Default value for --foo:
foo = true
.fi
.br
.sp 1v
.in 4
.ti 4
\fBExample for discarding an inherited entry:\fR
.br
.sp 1v
.nf
# Discarding an inherited entry:
foo
.fi`
execTest(
t,
testCase{
impl: func(struct{ Foo bool }) {},
command: "foo",
env: "_wandgenerate=man;_wandgeneratedate=2025-12-03;_wandgenerateversion=2025-12-03-abcd123",
mergeConfTyped: []Config{SystemConfig()},
},
"",
expect,
)
})
}
func TestMarkdown(t *testing.T) {
t.Run("basic", func(t *testing.T) {
const expect = `# foo - Foo sums three numbers
## Synopsis:
` + "```" + `
foo [options]... [--] <a int> <b int> <c int>
foo <subcommand>
` + "```" + `
## Description:
Foo sums three numbers. It prints the sum to stdout.
The input numbers can be any integer.
## Options:
- --help: Show help.
## Subcommands:
- help: Show help.`
execTest(
t,
testCase{
impl: testlib.Foo,
command: "foo",
env: "_wandgenerate=markdown;_wandgeneratedate=2025-12-03;_wandgenerateversion=2025-12-03-abcd123",
},
"",
expect,
)
})
t.Run("with subcommand", func(t *testing.T) {
const expect = `# foo
## Synopsis:
` + "```" + `
foo [options]...
foo <subcommand>
` + "```" + `
## Options:
- --help: Show help.
## Subcommands:
Show help for each subcommand by calling \<command\> help or \<command\> --help.
### foo bar
#### Synopsis:
` + "```" + `
foo bar [options]...
foo bar <subcommand>
` + "```" + `
#### Options:
- --help: Show help.`
execTest(
t,
testCase{
impl: Command("foo", func() {}, Command("bar", func() {})),
command: "foo",
env: "_wandgenerate=markdown;_wandgeneratedate=2025-12-03;_wandgenerateversion=2025-12-03-abcd123",
},
"",
expect,
)
})
t.Run("options", func(t *testing.T) {
const expect = `# foo
## Synopsis:
` + "```" + `
foo [options]...
foo <subcommand>
` + "```" + `
## Options:
- --bar string \[\*\]: Bars, any number.
- --duration duration: Duration is another option.
- --foo int: Foo is an option.
- --time time: Time is the third option here.
- --config: Configuration file.
- --help: Show help.
Hints:
- Options marked with \[\*\] accept any number of instances.
- Option values can be set both via = or just separated by space.
## Subcommands:
- help: Show help.
## Environment variables:
Every command line option's value can also be provided as an environment variable. Environment variable names
need to use snake casing like myapp\_foo\_bar\_baz or MYAPP\_FOO\_BAR\_BAZ, or other casing that doesn't include the
'-' dash character, and they need to be prefixed with the name of the application, as in the base name of the
command.
When both the environment variable and the command line option is defined, the command line option overrides the
environment variable. Multiple values for the same environment variable can be defined by concatenating the
values with the ':' separator character. When overriding multiple values with command line options, all the
environment values of the same field are dropped.
### Example environment variable:
` + "```" + `
FOO_FOO=42
` + "```" + `
## Configuration:
Every command line option's value can also be provided as an entry in a configuration file.
Configuration file entries can use keys with different casings, e.g. snake case foo\_bar\_baz, or kebab case
foo-bar-baz. The keys of the entries can use a limited set of characters: \[a-zA-Z0-9\_-\], and the first character
needs to be one of \[a-zA-Z\_\]. Entry values can consist of any characters, except for newline, control
characters, " (quote) and \\ (backslash), or the values can be quoted, in which case they can consist of any
characters, spanning multiple lines, and only the " (quote) and \\ (backslash) characters need to be escaped by
the \\ (backslash) character.
Configuration files allow multiple entries with the same key, when if the associated command line option also
allows multiple instances (marked with \[\*\]). When an entry is defined multiple configuration files, the
effective value is overridden in the order of the definition of the possible config files (see the listing order
below). To discard values defined in the overridden config files without defining new ones, we can set entries
with only the key, omitting the = key/value separator. Entries in the config files are overridden by the
environment variables, when defined, and by the command line options when defined.
Config files marked as optional don't need to be present in the file system, but if they exist, then they must
contain valid configuration syntax which is wand's flavor of .ini files
(https://code.squareroundforest.org/arpio/wand/src/branch/main/ini.treerack).
### Configuration files:
- zero or more configuration files defined by the --config option
### Example configuration entry:
` + "```" + `
# Default value for --foo:
foo = 42
` + "```" + `
### Example for discarding an inherited entry:
` + "```" + `
# Discarding an inherited entry:
foo
` + "```"
execTest(
t,
testCase{
impl: testlib.Baz,
command: "foo",
env: "_wandgenerate=markdown;_wandgeneratedate=2025-12-03;_wandgenerateversion=2025-12-03-abcd123",
mergeConfTyped: []Config{ConfigFromOption()},
},
"",
expect,
)
})
}
func TestVersion(t *testing.T) {
const expect = `v1`
execTest(
t,
testCase{
impl: Version(Command("foo", func() {}), "v1"),
command: "foo version",
},
"",
expect,
)
}