cli API

cli

package

API reference for the cli package.

S
struct

Base

Base is a struct that can be embedded in commands to provide common functionality.

pkg/cli/base.go:11-15
type Base struct

Fields

Name Type Description
Logger log.Logger internal:"ignore"
Ctx context.Context internal:"ignore"
Container *di.Container internal:"ignore"
F
function

listCommands

listCommands recursively lists commands space-separated for bash completion.

Parameters

prefix
string

Returns

string
pkg/cli/completion.go:71-81
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, " ")
}
F
function

writeFishCompletions

writeFishCompletions recursively writes fish completion entries.

Parameters

name
string
parentPrefix
string

Returns

error
pkg/cli/completion.go:84-122
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
}
I
interface

Runner

Runner is an interface for commands that can be run.

pkg/cli/interfaces.go:4-6
type Runner interface

Methods

Run
Method

Returns

error
func Run(...)
I
interface

BeforeRunner

BeforeRunner is an interface for commands that run before the main run.

pkg/cli/interfaces.go:9-11
type BeforeRunner interface

Methods

Before
Method

Returns

error
func Before(...)
I
interface

AfterRunner

AfterRunner is an interface for commands that run after the main run.

pkg/cli/interfaces.go:14-16
type AfterRunner interface

Methods

After
Method

Returns

error
func After(...)
F
function

applyBindings

applyBindings binds flags and args to the struct fields using the external binder library.

Parameters

flags
map[string]string
args
[]string
effectiveFlags
map[string]*parser.FlagMetadata

Returns

error
pkg/cli/app.go:27-103
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
				})
			}
		}
	}

	for name, meta := range effectiveFlags {
		var passedVal *string
		if v, ok := flags[name]; ok {
			passedVal = &v
		}

		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)
		}

		if valToBind != "" {
			if err := b.Run(name, []string{valToBind}); err != nil {
				return fmt.Errorf("invalid value for flag --%s: %w", name, err)
			}
		}
	}

	return bindArgs(node, args)
}
S
struct

App

App represents a CLI application.

pkg/cli/app.go:106-115
type App struct

Methods

GenBashCompletion writes a bash completion script for the app.

Parameters

Returns

error
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

error
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

error
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, "")
}
SetName
Method

SetName sets the name of the root command.

Parameters

name string
func (*App) SetName(name string)
{
	if a.RootNode != nil {
		a.RootNode.Name = name
	}
}
Reload
Method

Reload re-parses the root struct to pick up dynamic changes (e.g. map entries).

Returns

error
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)
}
Example
err := app.Reload()
AddCommand
Method

AddCommand adds a dynamic command to the application.

Parameters

name string
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
Method

SetTranslator sets the translator for the application.

Parameters

func (*App) SetTranslator(tr help.Translator)
{
	a.Translator = tr
}
SetVersion
Method

SetVersion sets the version string for the application.

Parameters

v string
func (*App) SetVersion(v string)
{
	a.version = v
}
SetContext
Method

SetContext sets the context for the application.

Parameters

func (*App) SetContext(ctx context.Context)
{
	a.ctx = ctx
}
Run
Method

Run executes the application.

Returns

runErr error
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 allFlags {
		if arg == "-h" || arg == "--help" {
			fmt.Print(help.GenerateHelp(targetNode, a.Translator))
			return nil
		}
	}

	for i, arg := range allFlags {
		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 errs := a.Validator.Validate(val.Interface()); len(errs) > 0 {
			for _, e := range errs {
				fmt.Fprintf(os.Stderr, "validation error: %s\n", e.Error())
			}
			fmt.Print(help.GenerateHelp(targetNode, a.Translator))
			return errutil.Wrap(errs)
		}
	}

	for _, node := range path {
		injectDependencies(node, a.ctx, a.Container)
	}

	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
}

Fields

Name Type Description
RootNode *parser.CommandNode
Translator help.Translator
Container *di.Container
Validator *validation.Validator
ctx context.Context
version string
mu sync.Mutex
panicRecovery bool
T
type

AppOption

AppOption is a functional option for App configuration.

pkg/cli/app.go:118-118
type AppOption options.Option[App]
F
function

WithVersion

WithVersion sets the application version.

Parameters

v
string

Returns

pkg/cli/app.go:121-123
func WithVersion(v string) AppOption

{
	return func(a *App) { a.version = v }
}
F
function

WithContext

WithContext sets the application context.

Parameters

Returns

pkg/cli/app.go:126-128
func WithContext(ctx context.Context) AppOption

{
	return func(a *App) { a.ctx = ctx }
}
F
function

WithTranslator

WithTranslator sets the help translator.

Parameters

Returns

pkg/cli/app.go:131-133
func WithTranslator(tr help.Translator) AppOption

{
	return func(a *App) { a.Translator = tr }
}
F
function

WithContainer

WithContainer sets the DI container for dependency injection in commands.

Parameters

container

Returns

pkg/cli/app.go:136-138
func WithContainer(container *di.Container) AppOption

{
	return func(a *App) { a.Container = container }
}
F
function

WithValidator

WithValidator sets the struct validator for command validation.

Parameters

Returns

pkg/cli/app.go:141-143
func WithValidator(validator *validation.Validator) AppOption

{
	return func(a *App) { a.Validator = validator }
}
F
function

WithPanicRecovery

WithPanicRecovery enables panic recovery in Run.

Returns

pkg/cli/app.go:146-148
func WithPanicRecovery() AppOption

{
	return func(a *App) { a.panicRecovery = true }
}
F
function

New

New creates a new App from a root struct.

Parameters

root
any
opts
...AppOption

Returns

error
pkg/cli/app.go:155-164
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}
	options.Apply(app, opts...)
	return app, nil
}

Example

app, err := cli.New(&CLI{}, cli.WithVersion("1.0.0"))
F
function

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

root
any
opts
...AppOption

Returns

error
pkg/cli/app.go:227-233
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)
	}
}
F
function

resolveCommand

resolveCommand traverses the tree, skipping flags to find subcommands.

Parameters

args
[]string

Returns

[]string
error
pkg/cli/app.go:370-401
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
}
F
function

getPathToNode

getPathToNode reconstructs path from root to target.

Parameters

Returns

pkg/cli/app.go:404-415
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
}
F
function

parseArgs

parseArgs parses flags based on effective metadata.

Parameters

args
[]string
effectiveFlags
map[string]*parser.FlagMetadata

Returns

map[string]string
[]string
error
pkg/cli/app.go:418-516
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 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] = value
			} else {
				if meta.Field.Kind() == reflect.Bool {
					flags[name] = "true"
				} else {
					if i+1 < len(args) {
						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] = value
			} else {
				if meta.Field.Kind() == reflect.Bool {
					flags[name] = "true"
				} else {
					if i+1 < len(args) {
						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
}
F
function

bindArgs

bindArgs binds positional arguments to the struct fields using reflectutil.

Parameters

args
[]string

Returns

error
pkg/cli/app.go:519-545
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
}
F
function

injectDependencies

injectDependencies injects the logger, context and DI container into the command struct if it embeds the Base struct.

Parameters

pkg/cli/app.go:548-575
func injectDependencies(node *parser.CommandNode, ctx context.Context, container *di.Container)

{
	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 {
		injectDIFields(val, container)
	}
}
F
function

injectDIFields

injectDIFields uses the DI container to inject dependencies tagged with inject.

Parameters

container
pkg/cli/app.go:578-582
func injectDIFields(val reflect.Value, container *di.Container)

{
	if val.Kind() == reflect.Struct && val.CanAddr() {
		container.Inject(val.Addr().Interface())
	}
}
S
struct
Implements: Runner

testRootCmd

pkg/cli/app_test.go:20-26
type testRootCmd struct

Methods

Run
Method

Returns

error
func (*testRootCmd) Run() error
{ return nil }

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"
S
struct
Implements: Runner

testAddCmd

pkg/cli/app_test.go:30-32
type testAddCmd struct

Methods

Run
Method

Returns

error
func (*testAddCmd) Run() error
{ return nil }

Fields

Name Type Description
Item string arg:"" required:"true" help:"Item to add"
S
struct
Implements: Runner

testRemoveCmd

pkg/cli/app_test.go:36-38
type testRemoveCmd struct

Methods

Run
Method

Returns

error
func (*testRemoveCmd) Run() error
{ return nil }

Fields

Name Type Description
Item string arg:"" required:"true" help:"Item to remove"
S
struct

testSimpleCmd

pkg/cli/app_test.go:42-44
type testSimpleCmd struct

Fields

Name Type Description
Value int cli:"value" help:"A value"
F
function

makeNode

Parameters

name
string
desc
string
v
any
opts
...func(*parser.CommandNode)
pkg/cli/app_test.go:46-57
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
}
F
function

TestNew_ValidRoot

Parameters

pkg/cli/app_test.go:59-73
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))
	}
}
F
function

TestNew_InvalidRoot

Parameters

pkg/cli/app_test.go:75-83
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")
	}
}
F
function

TestSetName

Parameters

pkg/cli/app_test.go:85-94
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")
	}
}
F
function

TestAddCommand

Parameters

pkg/cli/app_test.go:96-106
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")
	}
}
F
function

TestResolveCommand_Direct

Parameters

pkg/cli/app_test.go:108-121
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)
	}
}
F
function

TestResolveCommand_Nested

Parameters

pkg/cli/app_test.go:123-138
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")
	}
}
F
function

TestResolveCommand_FlagsFirst

Parameters

pkg/cli/app_test.go:140-153
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)
	}
}
F
function

TestResolveCommand_UnknownCommand

Parameters

pkg/cli/app_test.go:155-163
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")
	}
}
F
function

TestParseArgs_LongFlag

Parameters

pkg/cli/app_test.go:165-182
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 parsed["name"] != "hello" {
		t.Errorf("got %q, want %q", parsed["name"], "hello")
	}
	if len(pos) != 0 {
		t.Errorf("expected no positional args, got %v", pos)
	}
}
F
function

TestParseArgs_LongFlagWithEquals

Parameters

pkg/cli/app_test.go:184-198
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 parsed["name"] != "hello" {
		t.Errorf("got %q, want %q", parsed["name"], "hello")
	}
}
F
function

TestParseArgs_ShortFlag

Parameters

pkg/cli/app_test.go:200-219
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 parsed["verbose"] != "true" {
		t.Errorf("got %q, want %q", parsed["verbose"], "true")
	}
}
F
function

TestParseArgs_BoolFlag

Parameters

pkg/cli/app_test.go:221-235
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 parsed["verbose"] != "true" {
		t.Errorf("got %q, want 'true' for bool flag", parsed["verbose"])
	}
}
F
function

TestParseArgs_UnknownFlag

Parameters

pkg/cli/app_test.go:237-243
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")
	}
}
F
function

TestParseArgs_MissingValue

Parameters

pkg/cli/app_test.go:245-256
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")
	}
}
F
function

TestParseArgs_PositionalArgs

Parameters

pkg/cli/app_test.go:258-275
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 parsed["verbose"] != "true" {
		t.Errorf("expected verbose=true, got %v", parsed["verbose"])
	}
	if len(pos) != 2 || pos[0] != "file1" || pos[1] != "file2" {
		t.Errorf("expected [file1 file2], got %v", pos)
	}
}
F
function

TestBindArgs_Required

Parameters

pkg/cli/app_test.go:277-294
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")
	}
}
F
function

TestBindArgs_MissingRequired

Parameters

pkg/cli/app_test.go:296-310
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")
	}
}
F
function

TestBindArgs_GreedySlice

Parameters

pkg/cli/app_test.go:312-331
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)
	}
}
F
function

TestBindArgs_MultiplePositional

Parameters

pkg/cli/app_test.go:333-351
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)
	}
}
F
function

TestReload

Parameters

pkg/cli/app_test.go:353-361
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")
	}
}
F
function

TestGetPathToNode

Parameters

pkg/cli/app_test.go:363-379
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")
	}
}
F
function

TestGetPathToNode_SameNode

Parameters

pkg/cli/app_test.go:381-387
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]")
	}
}
F
function

TestInjectDependencies

Parameters

pkg/cli/app_test.go:389-400
func TestInjectDependencies(t *testing.T)

{
	type cmdWithBase struct {
		Base
	}
	v := &cmdWithBase{}
	ctx := context.Background()
	node := parser.NewCommandNode("test", "", reflect.ValueOf(v))
	injectDependencies(node, ctx, nil)
	if v.Ctx == nil {
		t.Error("expected non-nil Ctx after injectDependencies")
	}
}
F
function

TestInjectDependencies_NoBase

Parameters

pkg/cli/app_test.go:402-414
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
	injectDependencies(node, ctx, nil)
	if v.Name != originalName {
		t.Errorf("Name should not change: got %q, want %q", v.Name, originalName)
	}
}
F
function

TestRunFunction

Parameters

pkg/cli/app_test.go:416-424
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)
	}
}
S
struct

lifecycleCmd

pkg/cli/app_test.go:426-432
type lifecycleCmd struct

Methods

Before
Method

Returns

error
func (*lifecycleCmd) Before() error
{
	c.BeforeRan = true
	return nil
}
Run
Method

Returns

error
func (*lifecycleCmd) Run() error
{
	c.Ran = true
	return nil
}
After
Method

Returns

error
func (*lifecycleCmd) After() error
{
	c.AfterRan = true
	return nil
}

Fields

Name Type Description
Ran bool
BeforeRan bool
AfterRan bool
Sub lifecycleSubCmd cmd:"" help:"sub"
S
struct

lifecycleSubCmd

pkg/cli/app_test.go:449-453
type lifecycleSubCmd struct

Methods

Before
Method

Returns

error
func (*lifecycleSubCmd) Before() error
{
	c.BeforeRan = true
	return nil
}
Run
Method

Returns

error
func (*lifecycleSubCmd) Run() error
{
	c.Ran = true
	return nil
}
After
Method

Returns

error
func (*lifecycleSubCmd) After() error
{
	c.AfterRan = true
	return nil
}

Fields

Name Type Description
Ran bool
BeforeRan bool
AfterRan bool
F
function

TestRun_LifecycleOrder

Parameters

pkg/cli/app_test.go:470-494
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")
	}
}
S
struct
Implements: Runner BeforeRunner

beforeErrorCmd

pkg/cli/app_test.go:496-498
type beforeErrorCmd struct

Methods

Before
Method

Returns

error
func (*beforeErrorCmd) Before() error
{
	if c.ShouldFail {
		return errBeforeFailed
	}
	return nil
}
Run
Method

Returns

error
func (*beforeErrorCmd) Run() error
{ return nil }

Fields

Name Type Description
ShouldFail bool
S
struct
Implements: Runner

runWithHelpCmd

pkg/cli/app_test.go:509-511
type runWithHelpCmd struct

Methods

Run
Method

Returns

error
func (*runWithHelpCmd) Run() error
{ return nil }

Fields

Name Type Description
Name string cli:"name" help:"Your name"
F
function

TestRun_HelpFlag

Parameters

pkg/cli/app_test.go:515-523
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)
	}
}
F
function

TestRun_ShortHelp

Parameters

pkg/cli/app_test.go:525-533
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)
	}
}
F
function

TestRun_BeforeError

Parameters

pkg/cli/app_test.go:535-544
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")
	}
}
F
function

TestRun_UnknownCommand

Parameters

pkg/cli/app_test.go:546-554
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")
	}
}
S
struct

rootWithBeforeAndAfter

pkg/cli/app_test.go:556-560
type rootWithBeforeAndAfter struct

Methods

Before
Method

Returns

error
func (*rootWithBeforeAndAfter) Before() error
{
	c.BeforeCalled = true
	return nil
}
Run
Method

Returns

error
func (*rootWithBeforeAndAfter) Run() error
{
	c.RunCalled = true
	return nil
}
After
Method

Returns

error
func (*rootWithBeforeAndAfter) After() error
{
	c.AfterCalled = true
	return nil
}

Fields

Name Type Description
BeforeCalled bool
AfterCalled bool
RunCalled bool
F
function

TestRun_RootLifecycle

Parameters

pkg/cli/app_test.go:577-595
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")
	}
}
S
struct

afterErrorCmd

pkg/cli/app_test.go:597-597
type afterErrorCmd struct

Methods

Before
Method

Returns

error
func (*afterErrorCmd) Before() error
{ return nil }
Run
Method

Returns

error
func (*afterErrorCmd) Run() error
{ return nil }
After
Method

Returns

error
func (*afterErrorCmd) After() error
{ return errAfterFailed }
F
function

TestRun_AfterError

Parameters

pkg/cli/app_test.go:603-611
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")
	}
}
F
function

TestNew_WithOptions

Parameters

pkg/cli/app_test.go:613-627
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")
	}
}
F
function

TestNew_WithContainer

Parameters

pkg/cli/app_test.go:629-642
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")
	}
}
F
function

TestNew_WithValidator

Parameters

pkg/cli/app_test.go:644-653
func TestNew_WithValidator(t *testing.T)

{
	v := validation.New()
	app, err := New(&testRootCmd{}, WithValidator(v))
	if err != nil {
		t.Fatalf("New with validator failed: %v", err)
	}
	if app.Validator == nil {
		t.Error("expected non-nil Validator")
	}
}
S
struct
Implements: Runner

validationCmd

pkg/cli/app_test.go:655-657
type validationCmd struct

Methods

Run
Method

Returns

error
func (*validationCmd) Run() error
{ return nil }

Fields

Name Type Description
Email string cli:"email" help:"Email address" validate:"email"
F
function

TestRun_WithValidation

Parameters

pkg/cli/app_test.go:661-671
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(v))
	if err == nil {
		t.Error("expected validation error")
	}
}
F
function

TestRun_WithValidationPass

Parameters

pkg/cli/app_test.go:673-683
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(v))
	if err != nil {
		t.Errorf("expected no validation error, got: %v", err)
	}
}
S
struct
Implements: Runner

panicCmd

pkg/cli/app_test.go:685-685
type panicCmd struct

Methods

Run
Method

Returns

error
func (*panicCmd) Run() error
{
	panic("something went wrong")
}
F
function

TestRun_WithPanicRecovery

Parameters

pkg/cli/app_test.go:691-700
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")
	}
}
F
function

TestRun_WithoutPanicRecovery

Parameters

pkg/cli/app_test.go:702-713
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{})
}
F
function

TestResolveCommand_PositionalAllowed

Parameters

pkg/cli/app_test.go:715-731
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)
	}
}