cli API

cli

package

API reference for the cli package.

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

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
}

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

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-395
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 {
					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:398-409
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:412-510
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:513-539
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:542-569
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:572-576
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-165
func TestResolveCommand_UnknownCommand(t *testing.T)

{
	app, _ := New(&testRootCmd{})
	args := []string{"unknown"}
	node, _, err := resolveCommand(app.RootNode, args)
	if err != nil {
		t.Fatalf("resolveCommand failed: %v", err)
	}
	if node.Name != "root" {
		t.Errorf("got node %q, want %q (root fallback)", node.Name, "root")
	}
}
F
function

TestParseArgs_LongFlag

Parameters

pkg/cli/app_test.go:167-184
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:186-200
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:202-221
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:223-237
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:239-245
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:247-258
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:260-277
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:279-296
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:298-312
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:314-333
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:335-353
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:355-363
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:365-381
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:383-389
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:391-402
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:404-416
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:418-426
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:428-434
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:451-455
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:472-496
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:498-500
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:511-513
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:517-525
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:527-535
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:537-546
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:548-556
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.Errorf("Run with unknown command should print help, got error: %v", err)
	}
}
S
struct

rootWithBeforeAndAfter

pkg/cli/app_test.go:558-562
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:579-597
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:599-599
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:605-613
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:615-629
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:631-644
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:646-655
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:657-659
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:663-673
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:675-685
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:687-687
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:693-702
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:704-715
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{})
}
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(...)