From ffd2ee9193c7dbf690e4362aef7568af8112f222 Mon Sep 17 00:00:00 2001 From: Shirong Lu <73147033+happysnaker@users.noreply.github.com> Date: Thu, 2 Jul 2026 15:10:19 +0800 Subject: [PATCH 1/2] fix: ignore non-command args after help flag Signed-off-by: Shirong Lu <73147033+happysnaker@users.noreply.github.com> --- command_run.go | 2 ++ command_test.go | 83 +++++++++++++++++++++++++++++++++++++++++++++++++ help.go | 16 ++++++---- 3 files changed, 95 insertions(+), 6 deletions(-) diff --git a/command_run.go b/command_run.go index 8d5907151e..849e35b94d 100644 --- a/command_run.go +++ b/command_run.go @@ -10,6 +10,7 @@ import ( ) type helpShownKey struct{} +type helpFlagKey struct{} func (cmd *Command) parseArgsFromStdin() ([]string, error) { type state int @@ -214,6 +215,7 @@ func (cmd *Command) run(ctx context.Context, osArgs []string) (_ context.Context if cmd.checkHelp() { ctx = context.WithValue(ctx, helpShownKey{}, true) + ctx = context.WithValue(ctx, helpFlagKey{}, true) return ctx, helpCommandAction(ctx, cmd) } else { tracef("no help is wanted (cmd=%[1]q)", cmd.Name) diff --git a/command_test.go b/command_test.go index cad70b7801..a98ce82364 100644 --- a/command_test.go +++ b/command_test.go @@ -2687,6 +2687,89 @@ func TestCommand_Run_Help(t *testing.T) { } } +func TestCommand_Run_HelpFlagIgnoresNonCommandArguments(t *testing.T) { + t.Run("root command ignores positional args after help flag", func(t *testing.T) { + r := require.New(t) + var buf bytes.Buffer + var errBuf bytes.Buffer + + cmd := &Command{ + Writer: &buf, + ErrWriter: &errBuf, + Name: "myCLI", + Usage: "My Usage", + Commands: []*Command{ + { + Name: "command", + Arguments: []Argument{ + &StringArg{ + Name: "arg1", + UsageText: "ARG1", + }, + }, + Usage: "Show the version of My CLI", + Action: func(context.Context, *Command) error { + return nil + }, + }, + }, + Action: func(context.Context, *Command) error { + return nil + }, + } + + err := cmd.Run(buildTestContext(t), []string{"myCLI", "--help", "ciao"}) + r.NoError(err) + r.Contains(buf.String(), "NAME:") + r.Contains(buf.String(), "myCLI - My Usage") + r.NotContains(buf.String(), "No help topic for") + r.NotContains(errBuf.String(), "Incorrect Usage") + }) + + for _, argv := range [][]string{ + {"myCLI", "command", "ciao", "--help"}, + {"myCLI", "command", "--help", "ciao"}, + } { + t.Run(fmt.Sprintf("subcommand ignores positional args after help flag: %v", argv), func(t *testing.T) { + r := require.New(t) + var buf bytes.Buffer + var errBuf bytes.Buffer + + cmd := &Command{ + Writer: &buf, + ErrWriter: &errBuf, + Name: "myCLI", + Usage: "My Usage", + Commands: []*Command{ + { + Name: "command", + Arguments: []Argument{ + &StringArg{ + Name: "arg1", + UsageText: "ARG1", + }, + }, + Usage: "Show the version of My CLI", + Action: func(context.Context, *Command) error { + return nil + }, + }, + }, + Action: func(context.Context, *Command) error { + return nil + }, + } + + err := cmd.Run(buildTestContext(t), argv) + r.NoError(err) + r.Contains(buf.String(), "NAME:") + r.Contains(buf.String(), "myCLI command - Show the version of My CLI") + r.NotContains(buf.String(), "No help topic for") + r.NotContains(errBuf.String(), "Incorrect Usage") + }) + } +} + func TestCommand_Run_Version(t *testing.T) { versionArguments := [][]string{{"boom", "--version"}, {"boom", "-v"}} diff --git a/help.go b/help.go index 4bedf87d5d..d67d8ee95e 100644 --- a/help.go +++ b/help.go @@ -83,6 +83,8 @@ func buildHelpCommand(withAction bool) *Command { func helpCommandAction(ctx context.Context, cmd *Command) error { args := cmd.Args() firstArg := args.First() + explicitHelpCommand := cmd.builtInHelp + helpFromFlag := ctx.Value(helpFlagKey{}) != nil tracef("doing help for cmd %[1]q with args %[2]q", cmd, args) @@ -98,7 +100,8 @@ func helpCommandAction(ctx context.Context, cmd *Command) error { // $ app --help / -h # flag; show root help (ignores subsequent args) // $ app help / h # subcommand; show root help // $ app help / h foo # subcommand; show help for subcommand "foo" - // $ app --help / -h foo # flag; show help for subcommand "foo" + // $ app --help / -h foo # flag; show help for subcommand "foo" if it + // # matches a child command; otherwise ignore it // $ app foo --help / -h # flag on subcommand; show help for "foo" // $ app foo help / h # subcommand on subcommand; show help for "foo" // $ app foo (no action) # default action on subcommand; show help for "foo" @@ -117,11 +120,12 @@ func helpCommandAction(ctx context.Context, cmd *Command) error { // Case 4. $ app help foo // foo is the command for which help needs to be shown if firstArg != "" { - /* if firstArg == "--" { - return nil - }*/ - tracef("returning ShowCommandHelp with %[1]q", firstArg) - return ShowCommandHelp(ctx, cmd, firstArg) + if explicitHelpCommand || !helpFromFlag || cmd.Command(firstArg) != nil { + tracef("returning ShowCommandHelp with %[1]q", firstArg) + return ShowCommandHelp(ctx, cmd, firstArg) + } + + tracef("ignoring positional help argument %[1]q that is not a child command", firstArg) } // Case 1 & 2 From 9aa667bfea311e2047fbeac4b3d51ded6bffc37b Mon Sep 17 00:00:00 2001 From: Shirong Lu <73147033+happysnaker@users.noreply.github.com> Date: Thu, 2 Jul 2026 15:59:31 +0800 Subject: [PATCH 2/2] test: wire short option handling in help regression --- command_run.go | 6 ++++-- command_test.go | 23 ++++++++++++----------- 2 files changed, 16 insertions(+), 13 deletions(-) diff --git a/command_run.go b/command_run.go index 849e35b94d..e2046feba5 100644 --- a/command_run.go +++ b/command_run.go @@ -9,8 +9,10 @@ import ( "unicode" ) -type helpShownKey struct{} -type helpFlagKey struct{} +type ( + helpShownKey struct{} + helpFlagKey struct{} +) func (cmd *Command) parseArgsFromStdin() ([]string, error) { type state int diff --git a/command_test.go b/command_test.go index a98ce82364..eace83df32 100644 --- a/command_test.go +++ b/command_test.go @@ -165,22 +165,23 @@ func TestCommandFlagParsing(t *testing.T) { }{ // Test normal "not ignoring flags" flow {testArgs: []string{"test-cmd", "-break", "blah", "blah"}, skipFlagParsing: false, useShortOptionHandling: false, expectedErr: "flag provided but not defined: -break"}, - {testArgs: []string{"test-cmd", "blah", "blah"}, skipFlagParsing: true, useShortOptionHandling: false}, // Test SkipFlagParsing without any args that look like flags - {testArgs: []string{"test-cmd", "blah", "-break"}, skipFlagParsing: true, useShortOptionHandling: false}, // Test SkipFlagParsing with random flag arg - {testArgs: []string{"test-cmd", "blah", "-help"}, skipFlagParsing: true, useShortOptionHandling: false}, // Test SkipFlagParsing with "special" help flag arg - {testArgs: []string{"test-cmd", "blah", "-h"}, skipFlagParsing: false, useShortOptionHandling: true, expectedErr: "No help topic for 'blah'"}, // Test UseShortOptionHandling + {testArgs: []string{"test-cmd", "blah", "blah"}, skipFlagParsing: true, useShortOptionHandling: false}, // Test SkipFlagParsing without any args that look like flags + {testArgs: []string{"test-cmd", "blah", "-break"}, skipFlagParsing: true, useShortOptionHandling: false}, // Test SkipFlagParsing with random flag arg + {testArgs: []string{"test-cmd", "blah", "-help"}, skipFlagParsing: true, useShortOptionHandling: false}, // Test SkipFlagParsing with "special" help flag arg + {testArgs: []string{"test-cmd", "blah", "-h"}, skipFlagParsing: false, useShortOptionHandling: true}, // Test help flag swallowing trailing positional args } for _, c := range cases { t.Run(strings.Join(c.testArgs, " "), func(t *testing.T) { cmd := &Command{ - Writer: io.Discard, - Name: "test-cmd", - Aliases: []string{"tc"}, - Usage: "this is for testing", - Description: "testing", - Action: func(context.Context, *Command) error { return nil }, - SkipFlagParsing: c.skipFlagParsing, + Writer: io.Discard, + Name: "test-cmd", + Aliases: []string{"tc"}, + Usage: "this is for testing", + Description: "testing", + Action: func(context.Context, *Command) error { return nil }, + SkipFlagParsing: c.skipFlagParsing, + UseShortOptionHandling: c.useShortOptionHandling, } ctx, cancel := context.WithTimeout(context.Background(), 100*time.Millisecond)