Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion command_run.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,10 @@ import (
"unicode"
)

type helpShownKey struct{}
type (
helpShownKey struct{}
helpFlagKey struct{}
)

func (cmd *Command) parseArgsFromStdin() ([]string, error) {
type state int
Expand Down Expand Up @@ -214,6 +217,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)
Expand Down
106 changes: 95 additions & 11 deletions command_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -2687,6 +2688,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"}}

Expand Down
16 changes: 10 additions & 6 deletions help.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand All @@ -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"
Expand All @@ -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
Expand Down