cli
packageAPI reference for the cli
package.
Imports
(22)context
STD
fmt
STD
maps
STD
os
STD
os/signal
STD
reflect
STD
strings
STD
sync
STD
time
INT
github.com/mirkobrombin/go-cli-builder/v3/internal/binder
INT
github.com/mirkobrombin/go-cli-builder/v3/pkg/help
INT
github.com/mirkobrombin/go-cli-builder/v3/pkg/log
INT
github.com/mirkobrombin/go-cli-builder/v3/pkg/parser
INT
github.com/mirkobrombin/go-cli-builder/v3/pkg/resolver
PKG
github.com/mirkobrombin/go-foundation/v2/core/errutil
PKG
github.com/mirkobrombin/go-foundation/v2/core/reflectutil
STD
errors
STD
testing
PKG
github.com/mirkobrombin/go-foundation/v2/app/di
PKG
github.com/mirkobrombin/go-foundation/v2/core/validation
STD
io
STD
sort
Runner
Runner is an interface for commands that can be run.
type Runner interface
Methods
BeforeRunner
BeforeRunner is an interface for commands that run before the main run.
type BeforeRunner interface
Methods
AfterRunner
AfterRunner is an interface for commands that run after the main run.
type AfterRunner interface
Methods
Container
Container resolves named dependencies and populates fields tagged with
inject. It is declared here rather than imported so that a CLI never has
to name a type from the dependency injection library it happens to use.
*di.Container from github.com/mirkobrombin/go-foundation/v2/app/di satisfies
this interface as it is, so it can be passed straight to WithContainer.
type Container interface
Example
container, err := builder.Build()
cli.Run(app, cli.WithContainer(container))
Validator
Validator checks a command struct after its flags and arguments have been
bound and before it runs. A non-nil error aborts the run and prints the help
for the command.
Example, adapting the validator from go-foundation:
v := validation.New()
cli.Run(app, cli.WithValidator(cli.ValidatorFunc(func(target any) error {
if errs := v.Validate(target); len(errs) > 0 {
return errs
}
return nil
})))
type Validator interface
Methods
ValidatorFunc
ValidatorFunc adapts a plain function to the Validator interface.
type ValidatorFunc func(target any) error
applyBindings
applyBindings binds flags and args to the struct fields using the external binder library.
flags holds every occurrence of each flag in command line order. A scalar
field takes the last one, a slice field takes them all.
Parameters
Returns
func applyBindings(node *parser.CommandNode, flags map[string][]string, args []string, effectiveFlags map[string]*parser.FlagMetadata) error
{
val := node.Value
if val.Kind() != reflect.Ptr && val.CanAddr() {
val = val.Addr()
}
b, err := binder.NewBinder(val.Interface())
if err != nil {
return err
}
for name, meta := range effectiveFlags {
m := meta
switch m.Field.Kind() {
case reflect.Bool:
b.AddBool(name, func(v bool) error {
m.Field.SetBool(v)
return nil
})
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
if m.Field.Type() == reflect.TypeOf(time.Duration(0)) {
b.AddDuration(name, func(v time.Duration) error {
m.Field.SetInt(int64(v))
return nil
})
} else {
b.AddInt(name, func(v int64) error {
m.Field.SetInt(v)
return nil
})
}
case reflect.String:
b.AddStrings(name, func(v []string) error {
if len(v) > 0 {
m.Field.SetString(v[0])
}
return nil
})
default:
if m.Field.Kind() == reflect.Slice && m.Field.Type().Elem().Kind() == reflect.String {
b.AddStrings(name, func(v []string) error {
s := reflect.MakeSlice(m.Field.Type(), len(v), len(v))
for i, val := range v {
s.Index(i).SetString(val)
}
m.Field.Set(s)
return nil
})
break
}
// Every other kind reflectutil understands, float and uint among
// them. Without this a field of one of those types is parsed, is
// listed in the help, and then fails to bind.
b.AddStrings(name, func(v []string) error {
if len(v) == 0 {
return nil
}
return reflectutil.Bind(m.Field, v[0])
})
}
}
for name, meta := range effectiveFlags {
passed := flags[name]
var passedVal *string
if len(passed) > 0 {
last := passed[len(passed)-1]
passedVal = &last
}
valToBind, err := resolver.GetValue(passedVal, meta.Env, meta.Default, meta.Field.Kind() == reflect.Bool)
if err != nil {
return err
}
if meta.Required && valToBind == "" && meta.Field.Kind() != reflect.Bool {
return fmt.Errorf("missing required flag: --%s", name)
}
toBind := []string{valToBind}
if meta.Field.Kind() == reflect.Slice && len(passed) > 0 {
toBind = passed
}
if valToBind != "" {
if err := b.Run(name, toBind); err != nil {
return fmt.Errorf("invalid value for flag --%s: %w", name, err)
}
}
}
return bindArgs(node, args)
}
App
App represents a CLI application.
type App struct
Methods
SetName sets the name of the root command.
Parameters
func (*App) SetName(name string)
{
if a.RootNode != nil {
a.RootNode.Name = name
}
}
Reload re-parses the root struct to pick up dynamic changes (e.g. map entries).
Returns
func (*App) Reload() error
{
a.mu.Lock()
defer a.mu.Unlock()
if a.RootNode == nil {
return nil
}
val := a.RootNode.Value
if val.Kind() != reflect.Ptr && val.CanAddr() {
val = val.Addr()
}
return parser.ParseStruct(a.RootNode, val)
}
err := app.Reload()
AddCommand adds a dynamic command to the application.
Parameters
func (*App) AddCommand(name string, cmd *parser.CommandNode)
{
a.mu.Lock()
defer a.mu.Unlock()
if a.RootNode.Children == nil {
a.RootNode.Children = make(map[string]*parser.CommandNode)
}
a.RootNode.Children[name] = cmd
}
SetTranslator sets the translator for the application.
Parameters
func (*App) SetTranslator(tr help.Translator)
{
a.Translator = tr
}
SetVersion sets the version string for the application.
Parameters
func (*App) SetVersion(v string)
{
a.version = v
}
SetContext sets the context for the application.
Parameters
func (*App) SetContext(ctx context.Context)
{
a.ctx = ctx
}
Run executes the application.
Returns
func (*App) Run() (runErr error)
{
if a.panicRecovery {
defer func() {
if r := recover(); r != nil {
runErr = errutil.Wrap(fmt.Errorf("panic: %v", r))
}
}()
}
args := os.Args[1:]
targetNode, allFlags, err := resolveCommand(a.RootNode, args)
if err != nil {
fmt.Println(help.GenerateHelp(a.RootNode, a.Translator))
return errutil.Wrap(err)
}
path := getPathToNode(a.RootNode, targetNode)
effectiveFlags := make(map[string]*parser.FlagMetadata)
for _, node := range path {
maps.Copy(effectiveFlags, node.Flags)
}
for _, arg := range argsBeforeSeparator(allFlags) {
if arg == "-h" || arg == "--help" {
fmt.Print(help.GenerateHelp(targetNode, a.Translator))
return nil
}
}
controlArgs := argsBeforeSeparator(allFlags)
for i, arg := range controlArgs {
if arg == "--version" && a.version != "" {
fmt.Println(a.version)
return nil
}
if arg == "--completion" && i+1 < len(allFlags) {
shell := allFlags[i+1]
switch shell {
case "bash":
return a.GenBashCompletion(os.Stdout)
case "zsh":
return a.GenZshCompletion(os.Stdout)
case "fish":
return a.GenFishCompletion(os.Stdout)
default:
return errutil.Wrap(fmt.Errorf("unknown shell for completion: %s (supported: bash, zsh, fish)", shell))
}
}
}
parsedFlags, positionalArgs, err := parseArgs(allFlags, effectiveFlags)
if err != nil {
fmt.Printf("Error: %v\n\n", err)
fmt.Print(help.GenerateHelp(targetNode, a.Translator))
return errutil.Wrap(err)
}
if err := applyBindings(targetNode, parsedFlags, positionalArgs, effectiveFlags); err != nil {
fmt.Printf("Error: %v\n\n", err)
fmt.Print(help.GenerateHelp(targetNode, a.Translator))
return errutil.Wrap(err)
}
if a.Validator != nil {
val := targetNode.Value
if val.Kind() != reflect.Ptr && val.CanAddr() {
val = val.Addr()
}
if err := a.Validator.Validate(val.Interface()); err != nil {
fmt.Fprintf(os.Stderr, "validation error: %s\n", err.Error())
fmt.Print(help.GenerateHelp(targetNode, a.Translator))
return errutil.Wrap(err)
}
}
for _, node := range path {
if err := injectDependencies(node, a.ctx, a.logger, a.Container); err != nil {
return errutil.Wrap(err)
}
}
for _, node := range path {
if beforeRunner, ok := node.Value.Interface().(BeforeRunner); ok {
if err := beforeRunner.Before(); err != nil {
return errutil.Wrap(err)
}
} else if node.Value.CanAddr() {
if beforeRunner, ok := node.Value.Addr().Interface().(BeforeRunner); ok {
if err := beforeRunner.Before(); err != nil {
return errutil.Wrap(err)
}
}
}
}
executed := false
if runner, ok := targetNode.Value.Interface().(Runner); ok {
if err := runner.Run(); err != nil {
return errutil.Wrap(err)
}
executed = true
} else if targetNode.Value.CanAddr() {
if runner, ok := targetNode.Value.Addr().Interface().(Runner); ok {
if err := runner.Run(); err != nil {
return errutil.Wrap(err)
}
executed = true
}
}
if !executed {
fmt.Print(help.GenerateHelp(targetNode, a.Translator))
}
for i := len(path) - 1; i >= 0; i-- {
node := path[i]
if afterRunner, ok := node.Value.Interface().(AfterRunner); ok {
if err := afterRunner.After(); err != nil {
return errutil.Wrap(err)
}
} else if node.Value.CanAddr() {
if afterRunner, ok := node.Value.Addr().Interface().(AfterRunner); ok {
if err := afterRunner.After(); err != nil {
return errutil.Wrap(err)
}
}
}
}
return nil
}
GenBashCompletion writes a bash completion script for the app.
Parameters
Returns
func (*App) GenBashCompletion(w io.Writer) error
{
name := a.RootNode.Name
if name == "" {
name = "app"
}
_, err := fmt.Fprintf(w, `_%s_completions()
{
local cur prev words cword
_init_completion || return
COMPREPLY=($(compgen -W "%s" -- "$cur"))
}
complete -F _%s_completions %s
`, name, listCommands(a.RootNode, ""), name, name)
return err
}
GenZshCompletion writes a zsh completion script for the app.
Parameters
Returns
func (*App) GenZshCompletion(w io.Writer) error
{
name := a.RootNode.Name
if name == "" {
name = "app"
}
cmds := listCommands(a.RootNode, "")
_, err := fmt.Fprintf(w, `#compdef %s
_%s() {
local -a subcommands
subcommands=(%s)
_describe 'command' subcommands
}
_%s
`, name, name, cmds, name)
return err
}
GenFishCompletion writes a fish completion script for the app.
Parameters
Returns
func (*App) GenFishCompletion(w io.Writer) error
{
name := a.RootNode.Name
if name == "" {
name = "app"
}
_, err := fmt.Fprintf(w, `complete -c %s -f
`, name)
if err != nil {
return err
}
return writeFishCompletions(w, a.RootNode, name, "")
}
Fields
| Name | Type | Description |
|---|---|---|
| RootNode | *parser.CommandNode | |
| Translator | help.Translator | |
| Container | Container | |
| Validator | Validator | |
| ctx | context.Context | |
| logger | log.Logger | |
| version | string | |
| mu | sync.Mutex | |
| panicRecovery | bool |
AppOption
AppOption is a functional option for App configuration.
type AppOption func(*App)
WithVersion
WithVersion sets the application version.
Parameters
Returns
func WithVersion(v string) AppOption
{
return func(a *App) { a.version = v }
}
Uses
WithContext
WithContext sets the application context.
Parameters
Returns
func WithContext(ctx context.Context) AppOption
{
return func(a *App) { a.ctx = ctx }
}
Uses
WithTranslator
WithTranslator sets the help translator.
Parameters
Returns
func WithTranslator(tr help.Translator) AppOption
{
return func(a *App) { a.Translator = tr }
}
Uses
WithContainer
WithContainer sets the DI container for dependency injection in commands.
Any value satisfying Container is accepted, including *di.Container from
go-foundation/v2/app/di.
func WithContainer(container Container) AppOption
{
return func(a *App) { a.Container = container }
}
WithValidator
WithValidator sets the struct validator for command validation.
Use ValidatorFunc to adapt a validator with a different signature.
func WithValidator(validator Validator) AppOption
{
return func(a *App) { a.Validator = validator }
}
WithLogger
WithLogger sets the logger handed to every command through Base.
Without it commands get log.New(), which writes readable lines to the
terminal. Pass fdn.NewStructured to emit JSON instead, or fdn.Wrap to reuse
a logger the application already built.
Parameters
Returns
func WithLogger(logger log.Logger) AppOption
{
return func(a *App) { a.logger = logger }
}
Uses
WithPanicRecovery
WithPanicRecovery enables panic recovery in Run.
Returns
func WithPanicRecovery() AppOption
{
return func(a *App) { a.panicRecovery = true }
}
Uses
New
New creates a new App from a root struct.
Parameters
Returns
func New(root any, opts ...AppOption) (*App, error)
{
rootNode, err := parser.Parse("root", root)
if err != nil {
return nil, fmt.Errorf("parse error: %w", err)
}
ctx, _ := signal.NotifyContext(context.Background(), os.Interrupt)
app := &App{RootNode: rootNode, ctx: ctx}
for _, opt := range opts {
if opt != nil {
opt(app)
}
}
if app.logger == nil {
app.logger = log.New()
}
return app, nil
}
Example
app, err := cli.New(&CLI{}, cli.WithVersion("1.0.0"))
Run
Run executes the application based on the provided root struct.
It parses the CLI arguments, resolves commands, binds flags, infuses dependencies, and runs lifecycle hooks.
Parameters
Returns
func Run(root any, opts ...AppOption) error
{
app, err := New(root, opts...)
if err != nil {
return err
}
return app.Run()
}
Example
func main() {
app := &CLI{}
if err := cli.Run(app); err != nil {
log.Fatal(err)
}
}
argsBeforeSeparator
Parameters
Returns
func argsBeforeSeparator(args []string) []string
{
for i, arg := range args {
if arg == "--" {
return args[:i]
}
}
return args
}
resolveCommand
resolveCommand traverses the tree, skipping flags to find subcommands.
Parameters
Returns
func resolveCommand(root *parser.CommandNode, args []string) (*parser.CommandNode, []string, error)
{
current := root
remaining := []string{}
parsingCmds := true
for i := range args {
arg := args[i]
if parsingCmds {
if strings.HasPrefix(arg, "-") {
remaining = append(remaining, arg)
} else {
if child, ok := current.Children[arg]; ok {
current = child
} else {
// A command group (has subcommands, takes no positional
// args of its own) cannot accept this token: it is an
// unknown command, not a positional argument.
if len(current.Children) > 0 && len(current.Args) == 0 {
return current, remaining, fmt.Errorf("unknown command: %q", arg)
}
parsingCmds = false
remaining = append(remaining, arg)
}
}
} else {
remaining = append(remaining, arg)
}
}
return current, remaining, nil
}
getPathToNode
getPathToNode reconstructs path from root to target.
Parameters
Returns
func getPathToNode(root, target *parser.CommandNode) []*parser.CommandNode
{
if root == target {
return []*parser.CommandNode{root}
}
for _, child := range root.Children {
path := getPathToNode(child, target)
if path != nil {
return append([]*parser.CommandNode{root}, path...)
}
}
return nil
}
parseArgs
parseArgs parses flags based on effective metadata.
A flag given more than once keeps every value, in command line order, so a
slice field can receive them all.
Parameters
Returns
func parseArgs(args []string, effectiveFlags map[string]*parser.FlagMetadata) (map[string][]string, []string, error)
{
flags := make(map[string][]string)
positionals := []string{}
shortMap := make(map[string]string)
for name, meta := range effectiveFlags {
if meta.Short != "" {
shortMap[meta.Short] = name
}
}
i := 0
for i < len(args) {
arg := args[i]
if arg == "--" {
positionals = append(positionals, args[i+1:]...)
break
}
if strings.HasPrefix(arg, "--") {
name := arg[2:]
value := ""
hasValue := false
if strings.Contains(name, "=") {
parts := strings.SplitN(name, "=", 2)
name = parts[0]
value = parts[1]
hasValue = true
}
meta, ok := effectiveFlags[name]
if !ok {
return nil, nil, fmt.Errorf("unknown flag: --%s", name)
}
if hasValue {
flags[name] = append(flags[name], value)
} else {
if meta.Field.Kind() == reflect.Bool {
flags[name] = append(flags[name], "true")
} else {
if i+1 < len(args) {
flags[name] = append(flags[name], args[i+1])
i++
} else {
return nil, nil, fmt.Errorf("flag needs an argument: --%s", name)
}
}
}
} else if strings.HasPrefix(arg, "-") {
shorthand := arg[1:]
name := shorthand
value := ""
hasValue := false
if strings.Contains(name, "=") {
parts := strings.SplitN(name, "=", 2)
sName := parts[0]
value = parts[1]
hasValue = true
if lName, ok := shortMap[sName]; ok {
name = lName
} else {
return nil, nil, fmt.Errorf("unknown shorthand flag: -%s", sName)
}
} else {
if lName, ok := shortMap[name]; ok {
name = lName
} else {
if _, ok := effectiveFlags[name]; !ok {
return nil, nil, fmt.Errorf("unknown shorthand flag: -%s", shorthand)
}
}
}
meta := effectiveFlags[name]
if hasValue {
flags[name] = append(flags[name], value)
} else {
if meta.Field.Kind() == reflect.Bool {
flags[name] = append(flags[name], "true")
} else {
if i+1 < len(args) {
flags[name] = append(flags[name], args[i+1])
i++
} else {
return nil, nil, fmt.Errorf("flag needs an argument: -%s", shorthand)
}
}
}
} else {
positionals = append(positionals, arg)
}
i++
}
return flags, positionals, nil
}
bindArgs
bindArgs binds positional arguments to the struct fields using reflectutil.
Parameters
Returns
func bindArgs(node *parser.CommandNode, args []string) error
{
argIdx := 0
for _, meta := range node.Args {
if meta.IsGreedy {
if len(args) > argIdx {
for _, v := range args[argIdx:] {
if err := reflectutil.Bind(meta.Field, v); err != nil {
return err
}
}
} else if meta.Required {
return fmt.Errorf("missing required positional arguments: %s", meta.Description)
}
break
} else {
if argIdx < len(args) {
if err := reflectutil.Bind(meta.Field, args[argIdx]); err != nil {
return err
}
argIdx++
} else if meta.Required {
return fmt.Errorf("missing required argument: %s", meta.Description)
}
}
}
return nil
}
injectDependencies
injectDependencies injects the logger, context and DI container into the command struct if it embeds the Base struct.
Parameters
Returns
func injectDependencies(node *parser.CommandNode, ctx context.Context, logger log.Logger, container Container) error
{
if logger == nil {
logger = log.New()
}
val := node.Value
if val.Kind() == reflect.Ptr {
val = val.Elem()
}
for i := 0; i < val.NumField(); i++ {
field := val.Field(i)
fieldType := val.Type().Field(i)
if fieldType.Type == reflect.TypeFor[Base]() {
if field.CanSet() {
base := Base{
Logger: logger,
Ctx: ctx,
Container: container,
}
field.Set(reflect.ValueOf(base))
}
}
}
if container != nil {
return injectDIFields(val, container)
}
return nil
}
Uses
injectDIFields
injectDIFields uses the DI container to inject dependencies tagged with inject.
A missing or mistyped dependency is reported instead of being swallowed: in
Foundation v2 the container answers with an error rather than leaving the
field at its zero value.
Parameters
Returns
func injectDIFields(val reflect.Value, container Container) error
{
if val.Kind() != reflect.Struct || !val.CanAddr() {
return nil
}
if err := container.Inject(val.Addr().Interface()); err != nil {
return fmt.Errorf("dependency injection failed: %w", err)
}
return nil
}
Uses
foundationValidator
foundationValidator adapts the Foundation validator to the Validator
interface, the way a consumer is expected to.
Parameters
Returns
func foundationValidator(v *validation.Validator) Validator
{
return ValidatorFunc(func(target any) error {
if errs := v.Validate(target); len(errs) > 0 {
return errs
}
return nil
})
}
Uses
testRootCmd
type testRootCmd struct
Methods
Fields
| Name | Type | Description |
|---|---|---|
| Verbose | bool | cli:"verbose,v" help:"Enable verbose" |
| Name | string | cli:"name" help:"Your name" default:"world" |
| Add | testAddCmd | cmd:"" help:"Add an item" |
| Remove | testRemoveCmd | cmd:"" help:"Remove an item" |
testSimpleCmd
type testSimpleCmd struct
Fields
| Name | Type | Description |
|---|---|---|
| Value | int | cli:"value" help:"A value" |
makeNode
Parameters
Returns
func makeNode(name, desc string, v any, opts ...func(*parser.CommandNode)) *parser.CommandNode
{
rv := reflect.ValueOf(v)
rv2 := rv
for rv2.Kind() == reflect.Ptr {
rv2 = rv2.Elem()
}
node := parser.NewCommandNode(name, desc, rv)
for _, o := range opts {
o(node)
}
return node
}
TestNew_ValidRoot
Parameters
func TestNew_ValidRoot(t *testing.T)
{
app, err := New(&testRootCmd{})
if err != nil {
t.Fatalf("New failed: %v", err)
}
if app.RootNode == nil {
t.Fatal("expected non-nil RootNode")
}
if app.RootNode.Name != "root" {
t.Errorf("got name %q, want %q", app.RootNode.Name, "root")
}
if len(app.RootNode.Children) != 2 {
t.Errorf("expected 2 children, got %d", len(app.RootNode.Children))
}
}
TestNew_InvalidRoot
Parameters
func TestNew_InvalidRoot(t *testing.T)
{
app, err := New(42)
if err != nil {
t.Fatalf("New should not error even for non-struct: %v", err)
}
if app.RootNode == nil {
t.Fatal("expected non-nil RootNode")
}
}
TestSetName
Parameters
func TestSetName(t *testing.T)
{
app, err := New(&testRootCmd{})
if err != nil {
t.Fatalf("New failed: %v", err)
}
app.SetName("myapp")
if app.RootNode.Name != "myapp" {
t.Errorf("got name %q, want %q", app.RootNode.Name, "myapp")
}
}
TestAddCommand
Parameters
func TestAddCommand(t *testing.T)
{
app, err := New(&testRootCmd{})
if err != nil {
t.Fatalf("New failed: %v", err)
}
child := makeNode("list", "List items", &testSimpleCmd{})
app.AddCommand("list", child)
if _, ok := app.RootNode.Children["list"]; !ok {
t.Fatal("expected 'list' in children")
}
}
TestResolveCommand_Direct
Parameters
func TestResolveCommand_Direct(t *testing.T)
{
app, _ := New(&testRootCmd{})
args := []string{"add", "myitem"}
node, remaining, err := resolveCommand(app.RootNode, args)
if err != nil {
t.Fatalf("resolveCommand failed: %v", err)
}
if node.Name != "add" {
t.Errorf("got node %q, want %q", node.Name, "add")
}
if len(remaining) != 1 || remaining[0] != "myitem" {
t.Errorf("got remaining %v, want [myitem]", remaining)
}
}
TestResolveCommand_Nested
Parameters
func TestResolveCommand_Nested(t *testing.T)
{
type nested struct {
Inner struct {
AddCmd testAddCmd `cmd:"add" help:"Add command"`
} `cmd:"inner" help:"Inner commands"`
}
app, _ := New(&nested{})
args := []string{"inner", "add", "val"}
node, _, err := resolveCommand(app.RootNode, args)
if err != nil {
t.Fatalf("resolveCommand failed: %v", err)
}
if node.Name != "add" {
t.Errorf("got node %q, want %q", node.Name, "add")
}
}
TestResolveCommand_FlagsFirst
Parameters
func TestResolveCommand_FlagsFirst(t *testing.T)
{
app, _ := New(&testRootCmd{})
args := []string{"--verbose", "add", "myitem"}
node, remaining, err := resolveCommand(app.RootNode, args)
if err != nil {
t.Fatalf("resolveCommand failed: %v", err)
}
if node.Name != "add" {
t.Errorf("got node %q, want %q", node.Name, "add")
}
if len(remaining) < 2 {
t.Errorf("expected at least 2 remaining (flag+arg), got %v", remaining)
}
}
TestResolveCommand_UnknownCommand
Parameters
func TestResolveCommand_UnknownCommand(t *testing.T)
{
app, _ := New(&testRootCmd{})
args := []string{"unknown"}
// A command group must reject an unknown subcommand rather than swallowing
// it as a positional and falling back to root help.
if _, _, err := resolveCommand(app.RootNode, args); err == nil {
t.Fatal("expected error for unknown command, got nil")
}
}
flagValue
flagValue returns the one value parsed for name, and fails when the flag was
seen any other number of times.
Parameters
Returns
func flagValue(t *testing.T, parsed map[string][]string, name string) string
{
t.Helper()
v := parsed[name]
if len(v) != 1 {
t.Fatalf("flag %s: got %d values %v, want 1", name, len(v), v)
}
return v[0]
}
TestParseArgs_LongFlag
Parameters
func TestParseArgs_LongFlag(t *testing.T)
{
flags := map[string]*parser.FlagMetadata{
"name": {
Name: "name",
Field: reflect.ValueOf(""),
},
}
parsed, pos, err := parseArgs([]string{"--name", "hello"}, flags)
if err != nil {
t.Fatalf("parseArgs failed: %v", err)
}
if flagValue(t, parsed, "name") != "hello" {
t.Errorf("got %v, want %q", parsed["name"], "hello")
}
if len(pos) != 0 {
t.Errorf("expected no positional args, got %v", pos)
}
}
TestParseArgs_LongFlagWithEquals
Parameters
func TestParseArgs_LongFlagWithEquals(t *testing.T)
{
flags := map[string]*parser.FlagMetadata{
"name": {
Name: "name",
Field: reflect.ValueOf(""),
},
}
parsed, _, err := parseArgs([]string{"--name=hello"}, flags)
if err != nil {
t.Fatalf("parseArgs failed: %v", err)
}
if flagValue(t, parsed, "name") != "hello" {
t.Errorf("got %v, want %q", parsed["name"], "hello")
}
}
TestParseArgs_ShortFlag
Parameters
func TestParseArgs_ShortFlag(t *testing.T)
{
flags := map[string]*parser.FlagMetadata{
"verbose": {
Name: "verbose",
Short: "v",
Field: reflect.ValueOf(true),
},
}
node := makeNode("root", "", &testSimpleCmd{})
node.Flags = flags
node.ShortFlags["v"] = "verbose"
parsed, _, err := parseArgs([]string{"-v", "true"}, flags)
if err != nil {
t.Fatalf("parseArgs failed: %v", err)
}
if flagValue(t, parsed, "verbose") != "true" {
t.Errorf("got %v, want %q", parsed["verbose"], "true")
}
}
TestParseArgs_BoolFlag
Parameters
func TestParseArgs_BoolFlag(t *testing.T)
{
flags := map[string]*parser.FlagMetadata{
"verbose": {
Name: "verbose",
Field: reflect.ValueOf(true),
},
}
parsed, _, err := parseArgs([]string{"--verbose"}, flags)
if err != nil {
t.Fatalf("parseArgs failed: %v", err)
}
if flagValue(t, parsed, "verbose") != "true" {
t.Errorf("got %q, want 'true' for bool flag", flagValue(t, parsed, "verbose"))
}
}
TestParseArgs_UnknownFlag
Parameters
func TestParseArgs_UnknownFlag(t *testing.T)
{
flags := map[string]*parser.FlagMetadata{}
_, _, err := parseArgs([]string{"--unknown"}, flags)
if err == nil {
t.Error("expected error for unknown flag")
}
}
TestParseArgs_MissingValue
Parameters
func TestParseArgs_MissingValue(t *testing.T)
{
flags := map[string]*parser.FlagMetadata{
"name": {
Name: "name",
Field: reflect.ValueOf(""),
},
}
_, _, err := parseArgs([]string{"--name"}, flags)
if err == nil {
t.Error("expected error for missing flag value")
}
}
TestParseArgs_PositionalArgs
Parameters
func TestParseArgs_PositionalArgs(t *testing.T)
{
flags := map[string]*parser.FlagMetadata{
"verbose": {
Name: "verbose",
Field: reflect.ValueOf(true),
},
}
parsed, pos, err := parseArgs([]string{"--verbose", "file1", "file2"}, flags)
if err != nil {
t.Fatalf("parseArgs failed: %v", err)
}
if flagValue(t, parsed, "verbose") != "true" {
t.Errorf("expected verbose=true, got %v", flagValue(t, parsed, "verbose"))
}
if len(pos) != 2 || pos[0] != "file1" || pos[1] != "file2" {
t.Errorf("expected [file1 file2], got %v", pos)
}
}
TestBindArgs_Required
Parameters
func TestBindArgs_Required(t *testing.T)
{
var val string
node := parser.NewCommandNode("add", "", reflect.ValueOf(&struct {
Item string
}{}))
node.Args = append(node.Args, &parser.ArgMetadata{
Description: "item",
Required: true,
Field: reflect.ValueOf(&val).Elem(),
})
if err := bindArgs(node, []string{"myitem"}); err != nil {
t.Fatalf("bindArgs failed: %v", err)
}
if val != "myitem" {
t.Errorf("got %q, want %q", val, "myitem")
}
}
TestBindArgs_MissingRequired
Parameters
func TestBindArgs_MissingRequired(t *testing.T)
{
var val string
node := parser.NewCommandNode("add", "", reflect.ValueOf(&struct {
Item string
}{}))
node.Args = append(node.Args, &parser.ArgMetadata{
Description: "item",
Required: true,
Field: reflect.ValueOf(&val).Elem(),
})
if err := bindArgs(node, []string{}); err == nil {
t.Error("expected error for missing required arg")
}
}
TestBindArgs_GreedySlice
Parameters
func TestBindArgs_GreedySlice(t *testing.T)
{
var val []string
node := parser.NewCommandNode("add", "", reflect.ValueOf(&struct {
Items []string
}{}))
sliceField := reflect.ValueOf(&val).Elem()
node.Args = append(node.Args, &parser.ArgMetadata{
Description: "files",
Required: true,
IsGreedy: true,
Field: sliceField,
})
if err := bindArgs(node, []string{"a", "b", "c"}); err != nil {
t.Fatalf("bindArgs failed: %v", err)
}
if len(val) != 3 || val[0] != "a" || val[1] != "b" || val[2] != "c" {
t.Errorf("got %v, want [a b c]", val)
}
}
TestBindArgs_MultiplePositional
Parameters
func TestBindArgs_MultiplePositional(t *testing.T)
{
type args struct {
From string
To string
}
a := args{}
node := parser.NewCommandNode("copy", "", reflect.ValueOf(&a))
node.Args = append(node.Args,
&parser.ArgMetadata{Description: "from", Required: true, Field: reflect.ValueOf(&a.From).Elem()},
&parser.ArgMetadata{Description: "to", Required: true, Field: reflect.ValueOf(&a.To).Elem()},
)
if err := bindArgs(node, []string{"src", "dst"}); err != nil {
t.Fatalf("bindArgs failed: %v", err)
}
if a.From != "src" || a.To != "dst" {
t.Errorf("got %+v, want {From:src To:dst}", a)
}
}
TestReload
Parameters
func TestReload(t *testing.T)
{
app, _ := New(&testRootCmd{})
if err := app.Reload(); err != nil {
t.Fatalf("Reload failed: %v", err)
}
if app.RootNode == nil {
t.Fatal("RootNode should not be nil after reload")
}
}
TestGetPathToNode
Parameters
func TestGetPathToNode(t *testing.T)
{
app, _ := New(&testRootCmd{})
addNode := app.RootNode.Children["add"]
if addNode == nil {
t.Fatal("expected add child node")
}
path := getPathToNode(app.RootNode, addNode)
if len(path) != 2 {
t.Fatalf("expected path length 2, got %d", len(path))
}
if path[0] != app.RootNode {
t.Error("path[0] should be root")
}
if path[1] != addNode {
t.Error("path[1] should be add")
}
}
TestGetPathToNode_SameNode
Parameters
func TestGetPathToNode_SameNode(t *testing.T)
{
node := makeNode("root", "", &testRootCmd{})
path := getPathToNode(node, node)
if len(path) != 1 || path[0] != node {
t.Error("path to self should be just [self]")
}
}
TestInjectDependencies
Parameters
func TestInjectDependencies(t *testing.T)
{
type cmdWithBase struct {
Base
}
v := &cmdWithBase{}
ctx := context.Background()
node := parser.NewCommandNode("test", "", reflect.ValueOf(v))
if err := injectDependencies(node, ctx, nil, nil); err != nil {
t.Fatalf("injectDependencies failed: %v", err)
}
if v.Ctx == nil {
t.Error("expected non-nil Ctx after injectDependencies")
}
if v.Logger == nil {
t.Error("expected non-nil Logger after injectDependencies")
}
}
TestInjectDependencies_NoBase
Parameters
func TestInjectDependencies_NoBase(t *testing.T)
{
type cmdWithoutBase struct {
Name string
}
v := &cmdWithoutBase{Name: "test"}
ctx := context.Background()
node := parser.NewCommandNode("test", "", reflect.ValueOf(v))
originalName := v.Name
if err := injectDependencies(node, ctx, nil, nil); err != nil {
t.Fatalf("injectDependencies failed: %v", err)
}
if v.Name != originalName {
t.Errorf("Name should not change: got %q, want %q", v.Name, originalName)
}
}
TestRunFunction
Parameters
func TestRunFunction(t *testing.T)
{
oldArgs := os.Args
defer func() { os.Args = oldArgs }()
os.Args = []string{"app", "add", "testitem"}
err := Run(&testRootCmd{})
if err != nil {
t.Fatalf("Run failed: %v", err)
}
}
lifecycleCmd
type lifecycleCmd struct
Methods
Fields
| Name | Type | Description |
|---|---|---|
| Ran | bool | |
| BeforeRan | bool | |
| AfterRan | bool | |
| Sub | lifecycleSubCmd | cmd:"" help:"sub" |
lifecycleSubCmd
type lifecycleSubCmd struct
Methods
Fields
| Name | Type | Description |
|---|---|---|
| Ran | bool | |
| BeforeRan | bool | |
| AfterRan | bool |
TestRun_LifecycleOrder
Parameters
func TestRun_LifecycleOrder(t *testing.T)
{
oldArgs := os.Args
defer func() { os.Args = oldArgs }()
cmd := &lifecycleCmd{}
os.Args = []string{"app", "sub"}
if err := Run(cmd); err != nil {
t.Fatalf("Run failed: %v", err)
}
if !cmd.BeforeRan {
t.Error("expected root Before to be called")
}
if !cmd.Sub.BeforeRan {
t.Error("expected sub Before to be called")
}
if !cmd.Sub.Ran {
t.Error("expected sub Run to be called")
}
if !cmd.Sub.AfterRan {
t.Error("expected sub After to be called")
}
if !cmd.AfterRan {
t.Error("expected root After to be called")
}
}
beforeErrorCmd
type beforeErrorCmd struct
Methods
Fields
| Name | Type | Description |
|---|---|---|
| ShouldFail | bool |
TestRun_HelpFlag
Parameters
func TestRun_HelpFlag(t *testing.T)
{
oldArgs := os.Args
defer func() { os.Args = oldArgs }()
os.Args = []string{"app", "--help"}
if err := Run(&runWithHelpCmd{}); err != nil {
t.Fatalf("Run with --help failed: %v", err)
}
}
TestRun_ShortHelp
Parameters
func TestRun_ShortHelp(t *testing.T)
{
oldArgs := os.Args
defer func() { os.Args = oldArgs }()
os.Args = []string{"app", "-h"}
if err := Run(&runWithHelpCmd{}); err != nil {
t.Fatalf("Run with -h failed: %v", err)
}
}
TestRun_BeforeError
Parameters
func TestRun_BeforeError(t *testing.T)
{
oldArgs := os.Args
defer func() { os.Args = oldArgs }()
cmd := &beforeErrorCmd{ShouldFail: true}
os.Args = []string{"app"}
if err := Run(cmd); err == nil {
t.Error("expected error from Before")
}
}
TestRun_UnknownCommand
Parameters
func TestRun_UnknownCommand(t *testing.T)
{
oldArgs := os.Args
defer func() { os.Args = oldArgs }()
os.Args = []string{"app", "unknown"}
if err := Run(&testRootCmd{}); err == nil {
t.Error("Run with unknown command should return a non-nil error")
}
}
rootWithBeforeAndAfter
type rootWithBeforeAndAfter struct
Methods
Returns
func (*rootWithBeforeAndAfter) Before() error
{
c.BeforeCalled = true
return nil
}
Returns
func (*rootWithBeforeAndAfter) Run() error
{
c.RunCalled = true
return nil
}
Returns
func (*rootWithBeforeAndAfter) After() error
{
c.AfterCalled = true
return nil
}
Fields
| Name | Type | Description |
|---|---|---|
| BeforeCalled | bool | |
| AfterCalled | bool | |
| RunCalled | bool |
TestRun_RootLifecycle
Parameters
func TestRun_RootLifecycle(t *testing.T)
{
oldArgs := os.Args
defer func() { os.Args = oldArgs }()
cmd := &rootWithBeforeAndAfter{}
os.Args = []string{"app"}
if err := Run(cmd); err != nil {
t.Fatalf("Run failed: %v", err)
}
if !cmd.BeforeCalled {
t.Error("expected Before to be called")
}
if !cmd.RunCalled {
t.Error("expected Run to be called")
}
if !cmd.AfterCalled {
t.Error("expected After to be called")
}
}
afterErrorCmd
type afterErrorCmd struct
TestRun_AfterError
Parameters
func TestRun_AfterError(t *testing.T)
{
oldArgs := os.Args
defer func() { os.Args = oldArgs }()
os.Args = []string{"app"}
if err := Run(&afterErrorCmd{}); err == nil {
t.Error("expected error from After")
}
}
TestNew_WithOptions
Parameters
func TestNew_WithOptions(t *testing.T)
{
app, err := New(&testRootCmd{},
WithVersion("1.0.0"),
WithPanicRecovery(),
)
if err != nil {
t.Fatalf("New with options failed: %v", err)
}
if app.version != "1.0.0" {
t.Errorf("got version %q, want %q", app.version, "1.0.0")
}
if !app.panicRecovery {
t.Error("expected panicRecovery to be true")
}
}
TestNew_WithContainer
Parameters
func TestNew_WithContainer(t *testing.T)
{
builder := di.NewBuilder()
container, err := builder.Build()
if err != nil {
t.Fatalf("Build failed: %v", err)
}
app, err := New(&testRootCmd{}, WithContainer(container))
if err != nil {
t.Fatalf("New with container failed: %v", err)
}
if app.Container == nil {
t.Error("expected non-nil Container")
}
}
TestNew_WithValidator
Parameters
func TestNew_WithValidator(t *testing.T)
{
v := validation.New()
app, err := New(&testRootCmd{}, WithValidator(foundationValidator(v)))
if err != nil {
t.Fatalf("New with validator failed: %v", err)
}
if app.Validator == nil {
t.Error("expected non-nil Validator")
}
}
TestRun_WithValidation
Parameters
func TestRun_WithValidation(t *testing.T)
{
oldArgs := os.Args
defer func() { os.Args = oldArgs }()
v := validation.New()
os.Args = []string{"app", "--email", "not-an-email"}
err := Run(&validationCmd{}, WithValidator(foundationValidator(v)))
if err == nil {
t.Error("expected validation error")
}
}
TestRun_WithValidationPass
Parameters
func TestRun_WithValidationPass(t *testing.T)
{
oldArgs := os.Args
defer func() { os.Args = oldArgs }()
v := validation.New()
os.Args = []string{"app", "--email", "[email protected]"}
err := Run(&validationCmd{}, WithValidator(foundationValidator(v)))
if err != nil {
t.Errorf("expected no validation error, got: %v", err)
}
}
TestRun_WithPanicRecovery
Parameters
func TestRun_WithPanicRecovery(t *testing.T)
{
oldArgs := os.Args
defer func() { os.Args = oldArgs }()
os.Args = []string{"app"}
err := Run(&panicCmd{}, WithPanicRecovery())
if err == nil {
t.Error("expected error from panic recovery")
}
}
TestRun_WithoutPanicRecovery
Parameters
func TestRun_WithoutPanicRecovery(t *testing.T)
{
defer func() {
if r := recover(); r == nil {
t.Error("expected panic to propagate without recovery")
}
}()
oldArgs := os.Args
defer func() { os.Args = oldArgs }()
os.Args = []string{"app"}
Run(&panicCmd{})
}
TestResolveCommand_PositionalAllowed
Parameters
func TestResolveCommand_PositionalAllowed(t *testing.T)
{
root := parser.NewCommandNode("app", "", reflect.ValueOf(struct{}{}))
leaf := parser.NewCommandNode("greet", "", reflect.ValueOf(struct{}{}))
leaf.Args = []*parser.ArgMetadata{{Description: "name"}}
root.Children["greet"] = leaf
n, rem, err := resolveCommand(root, []string{"greet", "alice"})
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if n.Name != "greet" {
t.Fatalf("expected greet node, got %q", n.Name)
}
if len(rem) != 1 || rem[0] != "alice" {
t.Fatalf("expected positional [alice], got %v", rem)
}
}
testTypedCmd
type testTypedCmd struct
Methods
Fields
| Name | Type | Description |
|---|---|---|
| Threshold | float64 | cli:"threshold" default:"0.6" help:"A float" |
| Ratio | float32 | cli:"ratio" help:"A 32 bit float" |
| Count | uint | cli:"count" help:"An unsigned int" |
| If | []string | cli:"if" help:"A repeatable premise" |
| Name | string | cli:"name" help:"A scalar" |
bindFlags
bindFlags runs argv through the real parse and bind path.
Parameters
func bindFlags(t *testing.T, cmd any, argv []string)
{
t.Helper()
node, err := parser.Parse("root", cmd)
if err != nil {
t.Fatalf("parse: %v", err)
}
parsed, pos, err := parseArgs(argv, node.Flags)
if err != nil {
t.Fatalf("parseArgs: %v", err)
}
if err := applyBindings(node, parsed, pos, node.Flags); err != nil {
t.Fatalf("applyBindings: %v", err)
}
}
TestApplyBindings_FloatAndUintFlags
A float or unsigned field used to appear in the help and then fail to bind,
because applyBindings registered a handler only for the kinds it named.
Parameters
func TestApplyBindings_FloatAndUintFlags(t *testing.T)
{
cmd := &testTypedCmd{}
bindFlags(t, cmd, []string{"--threshold", "0.9", "--ratio", "0.25", "--count", "7"})
if cmd.Threshold != 0.9 {
t.Errorf("threshold: got %v, want 0.9", cmd.Threshold)
}
if cmd.Ratio != 0.25 {
t.Errorf("ratio: got %v, want 0.25", cmd.Ratio)
}
if cmd.Count != 7 {
t.Errorf("count: got %v, want 7", cmd.Count)
}
}
TestApplyBindings_FloatFlagUsesDefault
Parameters
func TestApplyBindings_FloatFlagUsesDefault(t *testing.T)
{
cmd := &testTypedCmd{}
bindFlags(t, cmd, nil)
if cmd.Threshold != 0.6 {
t.Errorf("threshold: got %v, want the declared default 0.6", cmd.Threshold)
}
}
TestApplyBindings_RepeatedFlagFillsSlice
Repeating a flag used to keep only its last occurrence, which left a slice
field unable to ever hold more than one value.
Parameters
func TestApplyBindings_RepeatedFlagFillsSlice(t *testing.T)
{
cmd := &testTypedCmd{}
bindFlags(t, cmd, []string{"--if", "a,b,c", "--if", "d,e,f"})
want := []string{"a,b,c", "d,e,f"}
if !reflect.DeepEqual(cmd.If, want) {
t.Errorf("if: got %v, want %v", cmd.If, want)
}
}
TestApplyBindings_RepeatedScalarFlagTakesLast
A scalar keeps the conventional meaning of a repeated flag: the last wins.
Parameters
func TestApplyBindings_RepeatedScalarFlagTakesLast(t *testing.T)
{
cmd := &testTypedCmd{}
bindFlags(t, cmd, []string{"--name", "first", "--name", "second"})
if cmd.Name != "second" {
t.Errorf("name: got %q, want %q", cmd.Name, "second")
}
}
TestParseArgs_RepeatedFlagKeepsEveryOccurrence
Parameters
func TestParseArgs_RepeatedFlagKeepsEveryOccurrence(t *testing.T)
{
flags := map[string]*parser.FlagMetadata{
"if": {
Name: "if",
Field: reflect.ValueOf([]string{}),
},
}
parsed, _, err := parseArgs([]string{"--if", "one", "--if=two", "--if", "three"}, flags)
if err != nil {
t.Fatalf("parseArgs failed: %v", err)
}
want := []string{"one", "two", "three"}
if !reflect.DeepEqual(parsed["if"], want) {
t.Errorf("got %v, want %v", parsed["if"], want)
}
}
TestParseArgs_SeparatorForwardsFlagsAsPositionals
Parameters
func TestParseArgs_SeparatorForwardsFlagsAsPositionals(t *testing.T)
{
parsed, positionals, err := parseArgs(
[]string{"value", "--", "--version", "-v", "a b"},
map[string]*parser.FlagMetadata{},
)
if err != nil {
t.Fatalf("parseArgs failed: %v", err)
}
if len(parsed) != 0 {
t.Fatalf("unexpected parsed flags: %v", parsed)
}
want := []string{"value", "--version", "-v", "a b"}
if !reflect.DeepEqual(positionals, want) {
t.Fatalf("positionals: got %v, want %v", positionals, want)
}
}
TestArgsBeforeSeparator
Parameters
func TestArgsBeforeSeparator(t *testing.T)
{
args := []string{"run", "origin", "--", "--version"}
want := []string{"run", "origin"}
if got := argsBeforeSeparator(args); !reflect.DeepEqual(got, want) {
t.Fatalf("got %v, want %v", got, want)
}
}
Base
Base is a struct that can be embedded in commands to provide common functionality.
Container is nil unless the application was built with WithContainer.
type Base struct
Fields
| Name | Type | Description |
|---|---|---|
| Logger | log.Logger | internal:"ignore" |
| Ctx | context.Context | internal:"ignore" |
| Container | Container | internal:"ignore" |
Uses
listCommands
listCommands recursively lists commands space-separated for bash completion.
Parameters
Returns
func listCommands(node *parser.CommandNode, prefix string) string
{
names := make([]string, 0, len(node.Children))
for name, child := range node.Children {
if child.Name == name {
fullName := prefix + name
names = append(names, fullName)
}
}
sort.Strings(names)
return strings.Join(names, " ")
}
writeFishCompletions
writeFishCompletions recursively writes fish completion entries.
Parameters
Returns
func writeFishCompletions(w io.Writer, node *parser.CommandNode, name string, parentPrefix string) error
{
for flagName, meta := range node.Flags {
desc := meta.Description
if desc == "" {
desc = flagName
}
short := ""
if meta.Short != "" {
short = fmt.Sprintf("-s %s", meta.Short)
}
_, err := fmt.Fprintf(w, "complete -c %s -n '__fish_seen_subcommand_from %s' -l %s %s -d '%s'\n",
name, parentPrefix, flagName, short, desc)
if err != nil {
return err
}
}
for childName, child := range node.Children {
if child.Name != childName {
continue
}
newPrefix := parentPrefix
if newPrefix != "" {
newPrefix += " "
}
newPrefix += childName
_, err := fmt.Fprintf(w, "complete -c %s -n '__fish_seen_subcommand_from %s' -a %s -d '%s'\n",
name, parentPrefix, childName, child.Description)
if err != nil {
return err
}
if err := writeFishCompletions(w, child, name, newPrefix); err != nil {
return err
}
}
return nil
}