Skip to content
Merged
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
4 changes: 3 additions & 1 deletion command.go
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,8 @@ type Command struct {
versionFlag Flag
// whether this is a completion command
isCompletionCommand bool
// whether this is the built-in help command
builtInHelp bool
}

func (cmd *Command) Command(name string) *Command {
Expand Down Expand Up @@ -419,7 +421,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 {
Expand Down
20 changes: 17 additions & 3 deletions command_run.go
Original file line number Diff line number Diff line change
Expand Up @@ -194,7 +194,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)
}
}
Expand Down Expand Up @@ -248,7 +248,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
}
Expand Down Expand Up @@ -328,7 +335,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
}
Expand Down
177 changes: 177 additions & 0 deletions command_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1966,6 +1966,183 @@ 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 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() {
Expand Down
13 changes: 7 additions & 6 deletions help.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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
}
Expand Down
Loading