From 4f2c8a73cbbba64b755fb59bc7686bb67b7c44b1 Mon Sep 17 00:00:00 2001 From: Naveen Gogineni Date: Sun, 7 Jun 2026 21:59:08 -0400 Subject: [PATCH 1/4] fix: resolve remaining help invocation inconsistencies - Add builtInHelp marker to distinguish the built-in help command from user-defined commands with the same name - Fix helpCommandAction to use builtInHelp instead of name matching - Fix checkAllRequiredFlags to use builtInHelp instead of name matching - Add 'Incorrect Usage' prefix to required flag and mutually exclusive flag error paths for consistency with parse error handling - Fix template selection for error paths: use ShowCommandHelp (via parent) instead of ShowSubcommandHelp, so leaf commands get CommandHelpTemplate with their OPTIONS section shown - Fix parse error path (Path D) subcommand case: look up help from parent.Commands instead of cmd.Commands, which was always failing --- command.go | 4 +++- command_run.go | 20 +++++++++++++++++--- help.go | 13 +++++++------ 3 files changed, 27 insertions(+), 10 deletions(-) diff --git a/command.go b/command.go index 1729b8b29f..9df1cf4ae7 100644 --- a/command.go +++ b/command.go @@ -163,6 +163,8 @@ type Command struct { globaVersionFlagAdded bool // whether this is a completion command isCompletionCommand bool + // whether this is the built-in help command + builtInHelp bool } // FullName returns the full name of the command. @@ -430,7 +432,7 @@ func (cmd *Command) checkAllRequiredFlags() requiredFlagsErr { // The help and completion commands are allowed to run without // enforcement of required flags, since they do not invoke user // actions that depend on those flag values. - if cmd.Name == helpName || cmd.isCompletionCommand { + if cmd.builtInHelp || cmd.isCompletionCommand { return nil } for pCmd := cmd; pCmd != nil; pCmd = pCmd.parent { diff --git a/command_run.go b/command_run.go index e095ff40e9..d3cbc62abc 100644 --- a/command_run.go +++ b/command_run.go @@ -189,7 +189,7 @@ func (cmd *Command) run(ctx context.Context, osArgs []string) (_ context.Context } } else { tracef("running ShowCommandHelp with %[1]q", cmd.Name) - if err := ShowCommandHelp(ctx, cmd, cmd.Name); err != nil { + if err := ShowCommandHelp(ctx, cmd.parent, cmd.Name); err != nil { tracef("SILENTLY IGNORING ERROR running ShowCommandHelp with %[1]q %[2]v", cmd.Name, err) } } @@ -243,7 +243,14 @@ func (cmd *Command) run(ctx context.Context, osArgs []string) (_ context.Context if cmd.OnUsageError != nil { err = cmd.OnUsageError(ctx, cmd, err, cmd.parent != nil) } else { - _ = ShowSubcommandHelp(cmd) + fmt.Fprintf(cmd.Root().ErrWriter, "Incorrect Usage: %s\n\n", err.Error()) + if cmd.parent == nil { + _ = ShowRootCommandHelp(cmd) + } else { + if err := ShowCommandHelp(ctx, cmd.parent, cmd.Name); err != nil { + _ = ShowSubcommandHelp(cmd) + } + } } return ctx, err } @@ -334,7 +341,14 @@ func (cmd *Command) run(ctx context.Context, osArgs []string) (_ context.Context if cmd.OnUsageError != nil { err = cmd.OnUsageError(ctx, cmd, err, cmd.parent != nil) } else { - _ = ShowSubcommandHelp(cmd) + fmt.Fprintf(cmd.Root().ErrWriter, "Incorrect Usage: %s\n\n", err.Error()) + if cmd.parent == nil { + _ = ShowRootCommandHelp(cmd) + } else { + if err := ShowCommandHelp(ctx, cmd.parent, cmd.Name); err != nil { + _ = ShowSubcommandHelp(cmd) + } + } } return ctx, err } diff --git a/help.go b/help.go index 1fba6edf0c..feb2b33711 100644 --- a/help.go +++ b/help.go @@ -65,11 +65,12 @@ var ArgsUsageCommandHelp = "[command]" func buildHelpCommand(withAction bool) *Command { cmd := &Command{ - Name: helpName, - Aliases: []string{helpAlias}, - Usage: UsageCommandHelp, - ArgsUsage: ArgsUsageCommandHelp, - HideHelp: true, + Name: helpName, + Aliases: []string{helpAlias}, + Usage: UsageCommandHelp, + ArgsUsage: ArgsUsageCommandHelp, + HideHelp: true, + builtInHelp: true, } if withAction { @@ -100,7 +101,7 @@ func helpCommandAction(ctx context.Context, cmd *Command) error { // to // $ app foo // which will then be handled as case 3 - if cmd.parent != nil && (cmd.HasName(helpName) || cmd.HasName(helpAlias)) { + if cmd.parent != nil && cmd.builtInHelp { tracef("setting cmd to cmd.parent") cmd = cmd.parent } From acb1b0e37cc568485fbe669486a9f650dc91461c Mon Sep 17 00:00:00 2001 From: Naveen Gogineni Date: Mon, 8 Jun 2026 05:51:39 -0400 Subject: [PATCH 2/4] style: fix gofumpt formatting in buildHelpCommand --- help.go | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/help.go b/help.go index feb2b33711..4953181e31 100644 --- a/help.go +++ b/help.go @@ -65,11 +65,11 @@ var ArgsUsageCommandHelp = "[command]" func buildHelpCommand(withAction bool) *Command { cmd := &Command{ - Name: helpName, - Aliases: []string{helpAlias}, - Usage: UsageCommandHelp, - ArgsUsage: ArgsUsageCommandHelp, - HideHelp: true, + Name: helpName, + Aliases: []string{helpAlias}, + Usage: UsageCommandHelp, + ArgsUsage: ArgsUsageCommandHelp, + HideHelp: true, builtInHelp: true, } From 083c64df549ae5516bba66c39e68962451e7a522 Mon Sep 17 00:00:00 2001 From: Naveen Gogineni Date: Mon, 8 Jun 2026 05:56:36 -0400 Subject: [PATCH 3/4] test: add coverage for Incorrect Usage display on required flags and mutex violations --- command_test.go | 106 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 106 insertions(+) diff --git a/command_test.go b/command_test.go index 46f4340bce..0fa5761682 100644 --- a/command_test.go +++ b/command_test.go @@ -1966,6 +1966,112 @@ func TestRequiredFlagCommandRunBehavior(t *testing.T) { } } +func TestCommand_IncorrectUsageOnRequiredFlagsViaRun(t *testing.T) { + t.Run("root command missing required flag", func(t *testing.T) { + r := require.New(t) + var buf bytes.Buffer + var errBuf bytes.Buffer + + cmd := &Command{ + Writer: &buf, + ErrWriter: &errBuf, + Flags: []Flag{ + &StringFlag{Name: "requiredFlag", Required: true}, + }, + } + + _ = cmd.Run(buildTestContext(t), []string{"command"}) + r.Contains(errBuf.String(), "Incorrect Usage") + r.Contains(buf.String(), "NAME:") + r.Contains(buf.String(), "command") + }) + + t.Run("subcommand missing required flag", func(t *testing.T) { + r := require.New(t) + var buf bytes.Buffer + var errBuf bytes.Buffer + + cmd := &Command{ + Writer: &buf, + ErrWriter: &errBuf, + Commands: []*Command{ + { + Name: "sub", + Flags: []Flag{ + &StringFlag{Name: "requiredFlag", Required: true}, + }, + Action: func(ctx context.Context, cmd *Command) error { + return nil + }, + }, + }, + } + + _ = cmd.Run(buildTestContext(t), []string{"command", "sub"}) + r.Contains(errBuf.String(), "Incorrect Usage") + r.Contains(buf.String(), "NAME:") + r.Contains(buf.String(), "sub") + }) +} + +func TestCommand_IncorrectUsageOnMutuallyExclusiveFlagsViaRun(t *testing.T) { + t.Run("root command with mutex violation", func(t *testing.T) { + r := require.New(t) + var buf bytes.Buffer + var errBuf bytes.Buffer + + cmd := &Command{ + Writer: &buf, + ErrWriter: &errBuf, + MutuallyExclusiveFlags: []MutuallyExclusiveFlags{ + { + Flags: [][]Flag{ + {&StringFlag{Name: "foo1"}}, + {&StringFlag{Name: "foo2"}}, + }, + }, + }, + } + + _ = cmd.Run(buildTestContext(t), []string{"command", "--foo1", "v1", "--foo2", "v2"}) + r.Contains(errBuf.String(), "Incorrect Usage") + r.Contains(buf.String(), "NAME:") + r.Contains(buf.String(), "command") + }) + + t.Run("subcommand with mutex violation", func(t *testing.T) { + r := require.New(t) + var buf bytes.Buffer + var errBuf bytes.Buffer + + cmd := &Command{ + Writer: &buf, + ErrWriter: &errBuf, + Action: func(ctx context.Context, cmd *Command) error { + return nil + }, + Commands: []*Command{ + { + Name: "sub", + MutuallyExclusiveFlags: []MutuallyExclusiveFlags{ + { + Flags: [][]Flag{ + {&StringFlag{Name: "foo1"}}, + {&StringFlag{Name: "foo2"}}, + }, + }, + }, + }, + }, + } + + _ = cmd.Run(buildTestContext(t), []string{"command", "sub", "--foo1", "v1", "--foo2", "v2"}) + r.Contains(errBuf.String(), "Incorrect Usage") + r.Contains(buf.String(), "NAME:") + r.Contains(buf.String(), "sub") + }) +} + func TestCommandHelpPrinter(t *testing.T) { oldPrinter := HelpPrinter defer func() { From d866892348e05555ba736d72056773dbe08be536 Mon Sep 17 00:00:00 2001 From: Naveen Gogineni Date: Mon, 8 Jun 2026 06:01:16 -0400 Subject: [PATCH 4/4] test: cover ShowSubcommandHelp fallback paths for required flags and mutex errors --- command_test.go | 71 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 71 insertions(+) diff --git a/command_test.go b/command_test.go index 0fa5761682..2fc621c7a8 100644 --- a/command_test.go +++ b/command_test.go @@ -2072,6 +2072,77 @@ func TestCommand_IncorrectUsageOnMutuallyExclusiveFlagsViaRun(t *testing.T) { }) } +func TestCommand_IncorrectUsageOnSubcommandWithFailingShowCommandHelp(t *testing.T) { + t.Run("required flags fallback to ShowSubcommandHelp", func(t *testing.T) { + oldShowCommandHelp := ShowCommandHelp + defer func() { ShowCommandHelp = oldShowCommandHelp }() + ShowCommandHelp = func(_ context.Context, _ *Command, _ string) error { + return errors.New("forced error") + } + + r := require.New(t) + var buf bytes.Buffer + var errBuf bytes.Buffer + + cmd := &Command{ + Writer: &buf, + ErrWriter: &errBuf, + Commands: []*Command{ + { + Name: "sub", + Flags: []Flag{ + &StringFlag{Name: "requiredFlag", Required: true}, + }, + Action: func(ctx context.Context, cmd *Command) error { + return nil + }, + }, + }, + } + + _ = cmd.Run(buildTestContext(t), []string{"command", "sub"}) + r.Contains(errBuf.String(), "Incorrect Usage") + r.Contains(buf.String(), "sub") + }) + + t.Run("mutex flags fallback to ShowSubcommandHelp", func(t *testing.T) { + oldShowCommandHelp := ShowCommandHelp + defer func() { ShowCommandHelp = oldShowCommandHelp }() + ShowCommandHelp = func(_ context.Context, _ *Command, _ string) error { + return errors.New("forced error") + } + + r := require.New(t) + var buf bytes.Buffer + var errBuf bytes.Buffer + + cmd := &Command{ + Writer: &buf, + ErrWriter: &errBuf, + Action: func(ctx context.Context, cmd *Command) error { + return nil + }, + Commands: []*Command{ + { + Name: "sub", + MutuallyExclusiveFlags: []MutuallyExclusiveFlags{ + { + Flags: [][]Flag{ + {&StringFlag{Name: "foo1"}}, + {&StringFlag{Name: "foo2"}}, + }, + }, + }, + }, + }, + } + + _ = cmd.Run(buildTestContext(t), []string{"command", "sub", "--foo1", "v1", "--foo2", "v2"}) + r.Contains(errBuf.String(), "Incorrect Usage") + r.Contains(buf.String(), "sub") + }) +} + func TestCommandHelpPrinter(t *testing.T) { oldPrinter := HelpPrinter defer func() {