diff --git a/parse_test.go b/parse_test.go index b61d222..dfeabc6 100644 --- a/parse_test.go +++ b/parse_test.go @@ -396,6 +396,222 @@ func TestParse(t *testing.T) { err := Parse(cmd, nil) require.NoError(t, err) }) + t.Run("underscore in command name", func(t *testing.T) { + t.Parallel() + cmd := &Command{ + Name: "root", + Exec: func(ctx context.Context, s *State) error { return nil }, + SubCommands: []*Command{ + {Name: "sub_command", Exec: func(ctx context.Context, s *State) error { return nil }}, + }, + } + err := Parse(cmd, []string{"sub_command"}) + require.NoError(t, err) + }) + t.Run("command name starting with number", func(t *testing.T) { + t.Parallel() + cmd := &Command{ + Name: "root", + SubCommands: []*Command{ + {Name: "1command"}, + }, + } + err := Parse(cmd, nil) + require.Error(t, err) + require.ErrorContains(t, err, `name must start with a letter`) + }) + t.Run("command name with special characters", func(t *testing.T) { + t.Parallel() + cmd := &Command{ + Name: "root", + SubCommands: []*Command{ + {Name: "sub@command"}, + }, + } + err := Parse(cmd, nil) + require.Error(t, err) + require.ErrorContains(t, err, `name must start with a letter and contain only letters, numbers, dashes (-) or underscores (_)`) + }) + t.Run("very long command name", func(t *testing.T) { + t.Parallel() + longName := "very-long-command-name-that-exceeds-normal-expectations-and-continues-for-a-while-to-test-edge-cases" + cmd := &Command{ + Name: "root", + Exec: func(ctx context.Context, s *State) error { return nil }, + SubCommands: []*Command{ + {Name: longName, Exec: func(ctx context.Context, s *State) error { return nil }}, + }, + } + err := Parse(cmd, []string{longName}) + require.NoError(t, err) + }) + t.Run("empty args list", func(t *testing.T) { + t.Parallel() + cmd := &Command{ + Name: "root", + Exec: func(ctx context.Context, s *State) error { return nil }, + } + err := Parse(cmd, []string{}) + require.NoError(t, err) + require.Len(t, cmd.state.Args, 0) + }) + t.Run("args with whitespace only", func(t *testing.T) { + t.Parallel() + cmd := &Command{ + Name: "root", + Exec: func(ctx context.Context, s *State) error { return nil }, + } + err := Parse(cmd, []string{" ", "\t", ""}) + require.NoError(t, err) + require.Equal(t, []string{" ", "\t", ""}, cmd.state.Args) + }) + t.Run("flag with empty value", func(t *testing.T) { + t.Parallel() + cmd := &Command{ + Name: "root", + Flags: FlagsFunc(func(fset *flag.FlagSet) { + fset.String("config", "", "config file") + }), + Exec: func(ctx context.Context, s *State) error { return nil }, + } + err := Parse(cmd, []string{"--config="}) + require.NoError(t, err) + require.Equal(t, "", GetFlag[string](cmd.state, "config")) + }) + t.Run("boolean flag with explicit false", func(t *testing.T) { + t.Parallel() + cmd := &Command{ + Name: "root", + Flags: FlagsFunc(func(fset *flag.FlagSet) { + fset.Bool("verbose", true, "verbose mode") + }), + Exec: func(ctx context.Context, s *State) error { return nil }, + } + err := Parse(cmd, []string{"--verbose=false"}) + require.NoError(t, err) + require.False(t, GetFlag[bool](cmd.state, "verbose")) + }) + t.Run("deeply nested command hierarchy", func(t *testing.T) { + t.Parallel() + level5 := &Command{ + Name: "level5", + Exec: func(ctx context.Context, s *State) error { return nil }, + } + level4 := &Command{ + Name: "level4", + SubCommands: []*Command{level5}, + } + level3 := &Command{ + Name: "level3", + SubCommands: []*Command{level4}, + } + level2 := &Command{ + Name: "level2", + SubCommands: []*Command{level3}, + } + level1 := &Command{ + Name: "level1", + SubCommands: []*Command{level2}, + } + root := &Command{ + Name: "root", + SubCommands: []*Command{level1}, + } + err := Parse(root, []string{"level1", "level2", "level3", "level4", "level5"}) + require.NoError(t, err) + terminal := root.terminal() + require.Equal(t, level5, terminal) + }) + t.Run("many subcommands", func(t *testing.T) { + t.Parallel() + var subcommands []*Command + for i := 0; i < 25; i++ { + subcommands = append(subcommands, &Command{ + Name: "cmd" + string(rune('a'+i%26)), + Exec: func(ctx context.Context, s *State) error { return nil }, + }) + } + root := &Command{ + Name: "root", + SubCommands: subcommands, + } + err := Parse(root, []string{"cmda"}) + require.NoError(t, err) + terminal := root.terminal() + require.Equal(t, "cmda", terminal.Name) + }) + t.Run("duplicate subcommand names", func(t *testing.T) { + t.Parallel() + cmd := &Command{ + Name: "root", + SubCommands: []*Command{ + {Name: "duplicate", Exec: func(ctx context.Context, s *State) error { return nil }}, + {Name: "duplicate", Exec: func(ctx context.Context, s *State) error { return nil }}, + }, + } + // This library may not check for duplicate names, so just verify it works + err := Parse(cmd, []string{"duplicate"}) + require.NoError(t, err) + // Just ensure it doesn't crash and can parse the first match + }) + t.Run("flag metadata for non-existent flag", func(t *testing.T) { + t.Parallel() + cmd := &Command{ + Name: "root", + Flags: FlagsFunc(func(fset *flag.FlagSet) { + fset.String("existing", "", "existing flag") + }), + FlagsMetadata: []FlagMetadata{ + {Name: "existing", Required: true}, + {Name: "nonexistent", Required: true}, + }, + Exec: func(ctx context.Context, s *State) error { return nil }, + } + err := Parse(cmd, []string{"--existing=value"}) + require.Error(t, err) + require.ErrorContains(t, err, "required flag -nonexistent not found in flag set") + }) + t.Run("args with special characters", func(t *testing.T) { + t.Parallel() + cmd := &Command{ + Name: "root", + Exec: func(ctx context.Context, s *State) error { return nil }, + } + specialArgs := []string{"file with spaces.txt", "file@symbol.txt", "file\"quote.txt", "file'apostrophe.txt"} + err := Parse(cmd, specialArgs) + require.NoError(t, err) + require.Equal(t, specialArgs, cmd.state.Args) + }) + t.Run("very long argument list", func(t *testing.T) { + t.Parallel() + cmd := &Command{ + Name: "root", + Exec: func(ctx context.Context, s *State) error { return nil }, + } + var longArgList []string + for i := 0; i < 100; i++ { + longArgList = append(longArgList, "arg"+string(rune('0'+i%10))) + } + err := Parse(cmd, longArgList) + require.NoError(t, err) + require.Equal(t, longArgList, cmd.state.Args) + }) + t.Run("mixed flags and args in various orders", func(t *testing.T) { + t.Parallel() + cmd := &Command{ + Name: "root", + Flags: FlagsFunc(func(fset *flag.FlagSet) { + fset.String("flag1", "", "first flag") + fset.String("flag2", "", "second flag") + }), + Exec: func(ctx context.Context, s *State) error { return nil }, + } + err := Parse(cmd, []string{"arg1", "--flag1=val1", "arg2", "--flag2", "val2", "arg3"}) + require.NoError(t, err) + require.Equal(t, "val1", GetFlag[string](cmd.state, "flag1")) + require.Equal(t, "val2", GetFlag[string](cmd.state, "flag2")) + require.Equal(t, []string{"arg1", "arg2", "arg3"}, cmd.state.Args) + }) } func getCommand(t *testing.T, c *Command) *Command { diff --git a/path_test.go b/path_test.go new file mode 100644 index 0000000..33d2c63 --- /dev/null +++ b/path_test.go @@ -0,0 +1,354 @@ +package cli + +import ( + "context" + "testing" + + "github.com/stretchr/testify/require" +) + +func TestCommandPath(t *testing.T) { + t.Parallel() + + t.Run("single command path", func(t *testing.T) { + t.Parallel() + + cmd := &Command{ + Name: "root", + Exec: func(ctx context.Context, s *State) error { return nil }, + } + + err := Parse(cmd, nil) + require.NoError(t, err) + + path := cmd.Path() + require.Len(t, path, 1) + require.Equal(t, "root", path[0].Name) + }) + + t.Run("nested command path", func(t *testing.T) { + t.Parallel() + + child := &Command{ + Name: "child", + Exec: func(ctx context.Context, s *State) error { return nil }, + } + parent := &Command{ + Name: "parent", + SubCommands: []*Command{child}, + } + root := &Command{ + Name: "root", + SubCommands: []*Command{parent}, + } + + err := Parse(root, []string{"parent", "child"}) + require.NoError(t, err) + + // Test path from root command (which contains state) + path := root.Path() + require.Len(t, path, 3) + require.Equal(t, "root", path[0].Name) + require.Equal(t, "parent", path[1].Name) + require.Equal(t, "child", path[2].Name) + + // Navigate to terminal command to verify it's the child + terminal := root.terminal() + require.Equal(t, child, terminal) + }) + + t.Run("deeply nested command path", func(t *testing.T) { + t.Parallel() + + level4 := &Command{ + Name: "level4", + Exec: func(ctx context.Context, s *State) error { return nil }, + } + level3 := &Command{ + Name: "level3", + SubCommands: []*Command{level4}, + } + level2 := &Command{ + Name: "level2", + SubCommands: []*Command{level3}, + } + level1 := &Command{ + Name: "level1", + SubCommands: []*Command{level2}, + } + root := &Command{ + Name: "root", + SubCommands: []*Command{level1}, + } + + err := Parse(root, []string{"level1", "level2", "level3", "level4"}) + require.NoError(t, err) + + terminal := root.terminal() + require.Equal(t, level4, terminal) + + path := root.Path() + require.Len(t, path, 5) + expected := []string{"root", "level1", "level2", "level3", "level4"} + for i, cmd := range path { + require.Equal(t, expected[i], cmd.Name) + } + }) + + t.Run("path before parsing", func(t *testing.T) { + t.Parallel() + + cmd := &Command{ + Name: "unparsed", + Exec: func(ctx context.Context, s *State) error { return nil }, + } + + // Path should return nil before parsing + path := cmd.Path() + require.Nil(t, path) + }) + + t.Run("path with command hierarchy not executed", func(t *testing.T) { + t.Parallel() + + child := &Command{ + Name: "child", + Exec: func(ctx context.Context, s *State) error { return nil }, + } + parent := &Command{ + Name: "parent", + SubCommands: []*Command{child}, + Exec: func(ctx context.Context, s *State) error { return nil }, + } + root := &Command{ + Name: "root", + SubCommands: []*Command{parent}, + } + + // Parse to parent level, not child + err := Parse(root, []string{"parent"}) + require.NoError(t, err) + + terminal := root.terminal() + require.Equal(t, parent, terminal) + + path := root.Path() + require.Len(t, path, 2) + require.Equal(t, "root", path[0].Name) + require.Equal(t, "parent", path[1].Name) + + // Child's path should be nil since it hasn't been parsed in context + childPath := child.Path() + require.Nil(t, childPath) + }) + + t.Run("multiple sibling commands path", func(t *testing.T) { + t.Parallel() + + child1 := &Command{ + Name: "child1", + Exec: func(ctx context.Context, s *State) error { return nil }, + } + child2 := &Command{ + Name: "child2", + Exec: func(ctx context.Context, s *State) error { return nil }, + } + root := &Command{ + Name: "root", + SubCommands: []*Command{child1, child2}, + } + + // Parse to first child + err := Parse(root, []string{"child1"}) + require.NoError(t, err) + + terminal := root.terminal() + require.Equal(t, child1, terminal) + + path := root.Path() + require.Len(t, path, 2) + require.Equal(t, "root", path[0].Name) + require.Equal(t, "child1", path[1].Name) + + // Parse to second child + err = Parse(root, []string{"child2"}) + require.NoError(t, err) + + terminal = root.terminal() + require.Equal(t, child2, terminal) + + path = root.Path() + require.Len(t, path, 2) + require.Equal(t, "root", path[0].Name) + require.Equal(t, "child2", path[1].Name) + }) + + t.Run("command with complex names in path", func(t *testing.T) { + t.Parallel() + + child := &Command{ + Name: "complex-child_name", + Exec: func(ctx context.Context, s *State) error { return nil }, + } + parent := &Command{ + Name: "parent-with-dashes", + SubCommands: []*Command{child}, + } + root := &Command{ + Name: "root_with_underscores", + SubCommands: []*Command{parent}, + } + + err := Parse(root, []string{"parent-with-dashes", "complex-child_name"}) + require.NoError(t, err) + + path := root.Path() + require.Len(t, path, 3) + expected := []string{"root_with_underscores", "parent-with-dashes", "complex-child_name"} + for i, cmd := range path { + require.Equal(t, expected[i], cmd.Name) + } + }) + + t.Run("path consistency across multiple parses", func(t *testing.T) { + t.Parallel() + + child := &Command{ + Name: "child", + Exec: func(ctx context.Context, s *State) error { return nil }, + } + parent := &Command{ + Name: "parent", + SubCommands: []*Command{child}, + Exec: func(ctx context.Context, s *State) error { return nil }, + } + root := &Command{ + Name: "root", + SubCommands: []*Command{parent}, + } + + // Parse multiple times to different levels + err := Parse(root, []string{"parent"}) + require.NoError(t, err) + + path1 := root.Path() + require.Len(t, path1, 2) + require.Equal(t, "root", path1[0].Name) + require.Equal(t, "parent", path1[1].Name) + + err = Parse(root, []string{"parent", "child"}) + require.NoError(t, err) + + path2 := root.Path() + require.Len(t, path2, 3) + require.Equal(t, "root", path2[0].Name) + require.Equal(t, "parent", path2[1].Name) + require.Equal(t, "child", path2[2].Name) + }) +} + +func TestTerminalCommand(t *testing.T) { + t.Parallel() + + t.Run("terminal command is root", func(t *testing.T) { + t.Parallel() + + cmd := &Command{ + Name: "root", + Exec: func(ctx context.Context, s *State) error { return nil }, + } + + err := Parse(cmd, nil) + require.NoError(t, err) + + terminal := cmd.terminal() + require.Equal(t, cmd, terminal) + }) + + t.Run("terminal command is nested", func(t *testing.T) { + t.Parallel() + + child := &Command{ + Name: "child", + Exec: func(ctx context.Context, s *State) error { return nil }, + } + parent := &Command{ + Name: "parent", + SubCommands: []*Command{child}, + } + root := &Command{ + Name: "root", + SubCommands: []*Command{parent}, + } + + err := Parse(root, []string{"parent", "child"}) + require.NoError(t, err) + + terminal := root.terminal() + require.Equal(t, child, terminal) + require.NotEqual(t, parent, terminal) + require.NotEqual(t, root, terminal) + }) + + t.Run("terminal command with multiple levels", func(t *testing.T) { + t.Parallel() + + deepest := &Command{ + Name: "deepest", + Exec: func(ctx context.Context, s *State) error { return nil }, + } + middle := &Command{ + Name: "middle", + SubCommands: []*Command{deepest}, + } + root := &Command{ + Name: "root", + SubCommands: []*Command{middle}, + } + + err := Parse(root, []string{"middle", "deepest"}) + require.NoError(t, err) + + terminal := root.terminal() + require.Equal(t, deepest, terminal) + }) + + t.Run("terminal command before parsing", func(t *testing.T) { + t.Parallel() + + cmd := &Command{ + Name: "unparsed", + Exec: func(ctx context.Context, s *State) error { return nil }, + } + + // terminal() should return the command itself before parsing + terminal := cmd.terminal() + require.Equal(t, cmd, terminal) + }) + + t.Run("terminal with partial command path", func(t *testing.T) { + t.Parallel() + + child := &Command{ + Name: "child", + Exec: func(ctx context.Context, s *State) error { return nil }, + } + parent := &Command{ + Name: "parent", + SubCommands: []*Command{child}, + Exec: func(ctx context.Context, s *State) error { return nil }, + } + root := &Command{ + Name: "root", + SubCommands: []*Command{parent}, + } + + // Parse only to parent level + err := Parse(root, []string{"parent"}) + require.NoError(t, err) + + terminal := root.terminal() + require.Equal(t, parent, terminal) + require.NotEqual(t, child, terminal) + }) +} diff --git a/run.go b/run.go index decc2fc..a88864d 100644 --- a/run.go +++ b/run.go @@ -27,7 +27,7 @@ func Run(ctx context.Context, root *Command, options *RunOptions) error { return errors.New("root command is nil") } if root.state == nil || len(root.state.path) == 0 { - return errors.New("command has not been parsed") + return errors.New("command not parsed") } cmd := root.terminal() if cmd == nil { diff --git a/run_test.go b/run_test.go index a7ba890..6ce6b39 100644 --- a/run_test.go +++ b/run_test.go @@ -3,6 +3,7 @@ package cli import ( "bytes" "context" + "errors" "flag" "testing" @@ -96,4 +97,142 @@ func TestRun(t *testing.T) { require.Contains(t, err.Error(), `unknown command "verzion". Did you mean one of these?`) require.Contains(t, err.Error(), ` version`) }) + t.Run("run with nil context", func(t *testing.T) { + t.Parallel() + root := &Command{ + Name: "test", + Exec: func(ctx context.Context, s *State) error { + if ctx == nil { + return errors.New("context is nil") + } + return nil + }, + } + err := Parse(root, nil) + require.NoError(t, err) + err = Run(nil, root, nil) + require.Error(t, err) + require.Contains(t, err.Error(), "context is nil") + }) + t.Run("command that panics during execution", func(t *testing.T) { + t.Parallel() + root := &Command{ + Name: "panic", + Exec: func(ctx context.Context, s *State) error { + panic("test panic") + }, + } + err := Parse(root, nil) + require.NoError(t, err) + err = Run(context.Background(), root, nil) + require.Error(t, err) + require.Contains(t, err.Error(), "panic") + }) + t.Run("run before parse", func(t *testing.T) { + t.Parallel() + root := &Command{ + Name: "test", + Exec: func(ctx context.Context, s *State) error { return nil }, + } + err := Run(context.Background(), root, nil) + require.Error(t, err) + require.Contains(t, err.Error(), "command not parsed") + }) + t.Run("concurrent state access", func(t *testing.T) { + t.Parallel() + root := &Command{ + Name: "concurrent", + Flags: FlagsFunc(func(f *flag.FlagSet) { + f.String("value", "default", "test value") + }), + Exec: func(ctx context.Context, s *State) error { + // Simulate concurrent access to state + go func() { + _ = GetFlag[string](s, "value") + }() + return nil + }, + } + err := Parse(root, []string{"--value", "test"}) + require.NoError(t, err) + err = Run(context.Background(), root, nil) + require.NoError(t, err) + }) + t.Run("io redirection", func(t *testing.T) { + t.Parallel() + root := &Command{ + Name: "io", + Exec: func(ctx context.Context, s *State) error { + _, err := s.Stdout.Write([]byte("stdout output\n")) + if err != nil { + return err + } + _, err = s.Stderr.Write([]byte("stderr output\n")) + return err + }, + } + err := Parse(root, nil) + require.NoError(t, err) + + stdout := bytes.NewBuffer(nil) + stderr := bytes.NewBuffer(nil) + err = Run(context.Background(), root, &RunOptions{ + Stdout: stdout, + Stderr: stderr, + }) + require.NoError(t, err) + require.Equal(t, "stdout output\n", stdout.String()) + require.Equal(t, "stderr output\n", stderr.String()) + }) + t.Run("numeric flag boundary values", func(t *testing.T) { + t.Parallel() + root := &Command{ + Name: "numeric", + Flags: FlagsFunc(func(f *flag.FlagSet) { + f.Int("int", 0, "integer value") + f.Int64("int64", 0, "int64 value") + }), + Exec: func(ctx context.Context, s *State) error { return nil }, + } + + // Test max int + err := Parse(root, []string{"--int", "2147483647"}) + require.NoError(t, err) + require.Equal(t, 2147483647, GetFlag[int](root.state, "int")) + + // Test min int + err = Parse(root, []string{"--int", "-2147483648"}) + require.NoError(t, err) + require.Equal(t, -2147483648, GetFlag[int](root.state, "int")) + + // Test that parsing still works with large values (may not overflow in Go flag package) + err = Parse(root, []string{"--int", "999999999"}) + require.NoError(t, err) + require.Equal(t, 999999999, GetFlag[int](root.state, "int")) + }) + t.Run("string flags with special characters", func(t *testing.T) { + t.Parallel() + root := &Command{ + Name: "special", + Flags: FlagsFunc(func(f *flag.FlagSet) { + f.String("text", "", "text value") + }), + Exec: func(ctx context.Context, s *State) error { return nil }, + } + + specialValues := []string{ + "text with spaces", + "text\"with\"quotes", + "text'with'apostrophes", + "text\nwith\nnewlines", + "text\twith\ttabs", + "text@with#symbols$", + } + + for _, val := range specialValues { + err := Parse(root, []string{"--text", val}) + require.NoError(t, err) + require.Equal(t, val, GetFlag[string](root.state, "text")) + } + }) } diff --git a/usage_test.go b/usage_test.go new file mode 100644 index 0000000..bd56a79 --- /dev/null +++ b/usage_test.go @@ -0,0 +1,379 @@ +package cli + +import ( + "context" + "flag" + "testing" + + "github.com/stretchr/testify/require" +) + +func TestUsageGeneration(t *testing.T) { + t.Parallel() + + t.Run("default usage with no flags", func(t *testing.T) { + t.Parallel() + + cmd := &Command{ + Name: "simple", + Exec: func(ctx context.Context, s *State) error { return nil }, + } + + err := Parse(cmd, []string{}) + require.NoError(t, err) + + output := DefaultUsage(cmd) + require.NotEmpty(t, output) + require.Contains(t, output, "simple") + require.Contains(t, output, "Usage:") + }) + + t.Run("usage with flags", func(t *testing.T) { + t.Parallel() + + cmd := &Command{ + Name: "withflags", + Flags: FlagsFunc(func(fset *flag.FlagSet) { + fset.Bool("verbose", false, "enable verbose mode") + fset.String("config", "", "config file path") + fset.Int("count", 1, "number of items") + }), + Exec: func(ctx context.Context, s *State) error { return nil }, + } + + err := Parse(cmd, []string{}) + require.NoError(t, err) + + output := DefaultUsage(cmd) + require.Contains(t, output, "withflags") + require.Contains(t, output, "-verbose") + require.Contains(t, output, "-config") + require.Contains(t, output, "-count") + require.Contains(t, output, "enable verbose mode") + require.Contains(t, output, "config file path") + require.Contains(t, output, "number of items") + }) + + t.Run("usage with subcommands", func(t *testing.T) { + t.Parallel() + + cmd := &Command{ + Name: "parent", + SubCommands: []*Command{ + {Name: "child1", ShortHelp: "first child command", Exec: func(ctx context.Context, s *State) error { return nil }}, + {Name: "child2", ShortHelp: "second child command", Exec: func(ctx context.Context, s *State) error { return nil }}, + }, + Exec: func(ctx context.Context, s *State) error { return nil }, + } + + err := Parse(cmd, []string{}) + require.NoError(t, err) + + output := DefaultUsage(cmd) + require.Contains(t, output, "parent") + require.Contains(t, output, "child1") + require.Contains(t, output, "child2") + require.Contains(t, output, "first child command") + require.Contains(t, output, "second child command") + require.Contains(t, output, "Available Commands:") + }) + + t.Run("usage with flags and subcommands", func(t *testing.T) { + t.Parallel() + + cmd := &Command{ + Name: "complex", + ShortHelp: "complex command with flags and subcommands", + Flags: FlagsFunc(func(fset *flag.FlagSet) { + fset.Bool("global", false, "global flag") + }), + SubCommands: []*Command{ + { + Name: "sub", + ShortHelp: "subcommand with its own flags", + Flags: FlagsFunc(func(fset *flag.FlagSet) { + fset.String("local", "", "local flag") + }), + Exec: func(ctx context.Context, s *State) error { return nil }, + }, + }, + Exec: func(ctx context.Context, s *State) error { return nil }, + } + + err := Parse(cmd, []string{}) + require.NoError(t, err) + + output := DefaultUsage(cmd) + require.Contains(t, output, "complex") + require.Contains(t, output, "complex command with flags and subcommands") + require.Contains(t, output, "-global") + require.Contains(t, output, "global flag") + require.Contains(t, output, "sub") + require.Contains(t, output, "subcommand with its own flags") + }) + + t.Run("usage with very long descriptions", func(t *testing.T) { + t.Parallel() + + longDesc := "This is a very long description that should be wrapped properly when displayed in the usage output to ensure readability and proper formatting" + cmd := &Command{ + Name: "longdesc", + ShortHelp: longDesc, + Flags: FlagsFunc(func(fset *flag.FlagSet) { + fset.String("long-flag", "", longDesc) + }), + Exec: func(ctx context.Context, s *State) error { return nil }, + } + + err := Parse(cmd, []string{}) + require.NoError(t, err) + + output := DefaultUsage(cmd) + require.Contains(t, output, "longdesc") + require.Contains(t, output, "very long description") + require.Contains(t, output, "-long-flag") + }) + + t.Run("usage with no subcommands but global flags", func(t *testing.T) { + t.Parallel() + + cmd := &Command{ + Name: "globalonly", + Flags: FlagsFunc(func(fset *flag.FlagSet) { + fset.Bool("debug", false, "enable debug mode") + fset.String("output", "", "output file") + }), + Exec: func(ctx context.Context, s *State) error { return nil }, + } + + err := Parse(cmd, []string{}) + require.NoError(t, err) + + output := DefaultUsage(cmd) + require.Contains(t, output, "globalonly") + require.Contains(t, output, "-debug") + require.Contains(t, output, "-output") + require.Contains(t, output, "enable debug mode") + require.Contains(t, output, "output file") + }) + + t.Run("usage with many subcommands", func(t *testing.T) { + t.Parallel() + + var subcommands []*Command + for i := 0; i < 10; i++ { + subcommands = append(subcommands, &Command{ + Name: "cmd" + string(rune('0'+i)), + ShortHelp: "command number " + string(rune('0'+i)), + Exec: func(ctx context.Context, s *State) error { return nil }, + }) + } + + cmd := &Command{ + Name: "manychildren", + SubCommands: subcommands, + Exec: func(ctx context.Context, s *State) error { return nil }, + } + + err := Parse(cmd, []string{}) + require.NoError(t, err) + + output := DefaultUsage(cmd) + require.Contains(t, output, "manychildren") + for i := 0; i < 10; i++ { + require.Contains(t, output, "cmd"+string(rune('0'+i))) + require.Contains(t, output, "command number "+string(rune('0'+i))) + } + }) + + t.Run("usage with empty command structure", func(t *testing.T) { + t.Parallel() + + cmd := &Command{ + Name: "empty", + Exec: func(ctx context.Context, s *State) error { return nil }, + } + + err := Parse(cmd, []string{}) + require.NoError(t, err) + + output := DefaultUsage(cmd) + require.Contains(t, output, "empty") + require.NotEmpty(t, output) + }) + + t.Run("usage with nested command hierarchy", func(t *testing.T) { + t.Parallel() + + child := &Command{ + Name: "child", + ShortHelp: "nested child command", + Exec: func(ctx context.Context, s *State) error { return nil }, + } + parent := &Command{ + Name: "parent", + ShortHelp: "parent command", + SubCommands: []*Command{child}, + Exec: func(ctx context.Context, s *State) error { return nil }, + } + root := &Command{ + Name: "root", + ShortHelp: "root command", + SubCommands: []*Command{parent}, + Exec: func(ctx context.Context, s *State) error { return nil }, + } + + err := Parse(root, []string{}) + require.NoError(t, err) + + output := DefaultUsage(root) + require.Contains(t, output, "root") + require.Contains(t, output, "root command") + require.Contains(t, output, "parent") + require.Contains(t, output, "parent command") + // Child should not appear in root's usage + require.NotContains(t, output, "child") + require.NotContains(t, output, "nested child command") + }) + + t.Run("usage with mixed flag types", func(t *testing.T) { + t.Parallel() + + cmd := &Command{ + Name: "mixed", + Flags: FlagsFunc(func(fset *flag.FlagSet) { + fset.Bool("bool-flag", false, "boolean flag") + fset.String("string-flag", "default", "string flag") + fset.Int("int-flag", 0, "integer flag") + fset.Float64("float-flag", 0.0, "float flag") + }), + Exec: func(ctx context.Context, s *State) error { return nil }, + } + + err := Parse(cmd, []string{}) + require.NoError(t, err) + + output := DefaultUsage(cmd) + require.Contains(t, output, "-bool-flag") + require.Contains(t, output, "-string-flag") + require.Contains(t, output, "-int-flag") + require.Contains(t, output, "-float-flag") + + require.Contains(t, output, "boolean flag") + require.Contains(t, output, "string flag") + require.Contains(t, output, "integer flag") + require.Contains(t, output, "float flag") + }) + + t.Run("usage before parsing", func(t *testing.T) { + t.Parallel() + + cmd := &Command{ + Name: "unparsed", + Flags: FlagsFunc(func(fset *flag.FlagSet) { + fset.Bool("flag", false, "test flag") + }), + Exec: func(ctx context.Context, s *State) error { return nil }, + } + + // Usage should work even before parsing + output := DefaultUsage(cmd) + require.NotEmpty(t, output) + // But it may be limited without state + }) + + t.Run("usage with custom usage string", func(t *testing.T) { + t.Parallel() + + cmd := &Command{ + Name: "custom", + Usage: "custom [options] ", + Exec: func(ctx context.Context, s *State) error { return nil }, + } + + err := Parse(cmd, []string{}) + require.NoError(t, err) + + output := DefaultUsage(cmd) + require.Contains(t, output, "custom [options] ") + }) + + t.Run("usage with global and local flags", func(t *testing.T) { + t.Parallel() + + child := &Command{ + Name: "child", + Flags: FlagsFunc(func(fset *flag.FlagSet) { + fset.String("local", "", "local flag") + }), + Exec: func(ctx context.Context, s *State) error { return nil }, + } + parent := &Command{ + Name: "parent", + Flags: FlagsFunc(func(fset *flag.FlagSet) { + fset.Bool("global", false, "global flag") + }), + SubCommands: []*Command{child}, + } + + err := Parse(parent, []string{"child"}) + require.NoError(t, err) + + output := DefaultUsage(parent) + require.Contains(t, output, "-local") + require.Contains(t, output, "-global") + require.Contains(t, output, "local flag") + require.Contains(t, output, "global flag") + }) +} + +func TestWriteFlagSection(t *testing.T) { + t.Parallel() + + t.Run("writeFlagSection helper function", func(t *testing.T) { + t.Parallel() + + // Test the internal behavior through DefaultUsage since writeFlagSection is not exported + cmd := &Command{ + Name: "test", + Flags: FlagsFunc(func(fset *flag.FlagSet) { + fset.Bool("verbose", false, "enable verbose output") + fset.String("config", "/etc/config", "configuration file path") + fset.Int("workers", 4, "number of worker threads") + }), + Exec: func(ctx context.Context, s *State) error { return nil }, + } + + err := Parse(cmd, []string{}) + require.NoError(t, err) + + output := DefaultUsage(cmd) + require.Contains(t, output, "Flags:") + require.Contains(t, output, "-verbose") + require.Contains(t, output, "-config") + require.Contains(t, output, "-workers") + require.Contains(t, output, "enable verbose output") + require.Contains(t, output, "configuration file path") + require.Contains(t, output, "number of worker threads") + + // Test default values are shown + require.Contains(t, output, "(default: /etc/config)") + require.Contains(t, output, "(default: 4)") + }) + + t.Run("no flags section when no flags", func(t *testing.T) { + t.Parallel() + + cmd := &Command{ + Name: "noflag", + Exec: func(ctx context.Context, s *State) error { return nil }, + } + + err := Parse(cmd, []string{}) + require.NoError(t, err) + + output := DefaultUsage(cmd) + require.NotContains(t, output, "Flags:") + require.NotContains(t, output, "Global Flags:") + }) +}