cli
packageAPI reference for the cli
package.
Imports
(23)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/v2/internal/binder
INT
github.com/mirkobrombin/go-cli-builder/v2/pkg/help
INT
github.com/mirkobrombin/go-cli-builder/v2/pkg/log
INT
github.com/mirkobrombin/go-cli-builder/v2/pkg/parser
INT
github.com/mirkobrombin/go-cli-builder/v2/pkg/resolver
PKG
github.com/mirkobrombin/go-foundation/pkg/di
PKG
github.com/mirkobrombin/go-foundation/pkg/errutil
PKG
github.com/mirkobrombin/go-foundation/pkg/options
PKG
github.com/mirkobrombin/go-foundation/pkg/reflectutil
PKG
github.com/mirkobrombin/go-foundation/pkg/validation
STD
errors
STD
testing
STD
io
STD
sort
applyBindings
applyBindings binds flags and args to the struct fields using the external binder library.
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
})
}
}
}
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)
}
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 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
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 | *di.Container | |
| Validator | *validation.Validator | |
| ctx | context.Context | |
| version | string | |
| mu | sync.Mutex | |
| panicRecovery | bool |
AppOption
AppOption is a functional option for App configuration.
type AppOption options.Option[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.
Parameters
Returns
func WithContainer(container *di.Container) AppOption
{
return func(a *App) { a.Container = container }
}
Uses
WithValidator
WithValidator sets the struct validator for command validation.
Parameters
Returns
func WithValidator(validator *validation.Validator) AppOption
{
return func(a *App) { a.Validator = validator }
}
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}
options.Apply(app, opts...)
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)
}
}
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 {
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.
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 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
}
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
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)
}
}
injectDIFields
injectDIFields uses the DI container to inject dependencies tagged with inject.
Parameters
func injectDIFields(val reflect.Value, container *di.Container)
{
if val.Kind() == reflect.Struct && val.CanAddr() {
container.Inject(val.Addr().Interface())
}
}
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"}
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")
}
}
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 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)
}
}
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 parsed["name"] != "hello" {
t.Errorf("got %q, 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 parsed["verbose"] != "true" {
t.Errorf("got %q, 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 parsed["verbose"] != "true" {
t.Errorf("got %q, want 'true' for bool flag", 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 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)
}
}
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))
injectDependencies(node, ctx, nil)
if v.Ctx == nil {
t.Error("expected non-nil Ctx 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
injectDependencies(node, ctx, nil)
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.Errorf("Run with unknown command should print help, got error: %v", err)
}
}
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(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(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(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{})
}
Base
Base is a struct that can be embedded in commands to provide common functionality.
type Base struct
Fields
| Name | Type | Description |
|---|---|---|
| Logger | log.Logger | internal:"ignore" |
| Ctx | context.Context | internal:"ignore" |
| Container | *di.Container | internal:"ignore" |
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
}
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