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
13 changes: 13 additions & 0 deletions command.go
Original file line number Diff line number Diff line change
Expand Up @@ -185,6 +185,19 @@ func (cmd *Command) checkHelp() bool {
return HelpFlag != nil && slices.ContainsFunc(HelpFlag.Names(), cmd.Bool)
}

func (cmd *Command) checkDuplicateFlagNames() error {
seen := map[string]struct{}{}
for _, fl := range cmd.Flags {
for _, name := range fl.Names() {
if _, ok := seen[name]; ok {
return fmt.Errorf("flag %q defined multiple times", name)
}
seen[name] = struct{}{}
}
}
return nil
}

func (cmd *Command) allFlags() []Flag {
var flags []Flag
flags = append(flags, cmd.Flags...)
Expand Down
3 changes: 3 additions & 0 deletions command_run.go
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,9 @@ func (cmd *Command) Run(ctx context.Context, osArgs []string) (deferErr error) {
func (cmd *Command) run(ctx context.Context, osArgs []string) (_ context.Context, deferErr error) {
tracef("running with arguments %[1]q (cmd=%[2]q)", osArgs, cmd.Name)
cmd.setupDefaults(osArgs)
if err := cmd.checkDuplicateFlagNames(); err != nil {
return ctx, err
}

// Validate StopOnNthArg
if cmd.StopOnNthArg != nil && *cmd.StopOnNthArg < 0 {
Expand Down
8 changes: 5 additions & 3 deletions command_setup.go
Original file line number Diff line number Diff line change
Expand Up @@ -250,9 +250,11 @@ func (cmd *Command) ensureHelp() {
localHelpFlag = HelpFlag
}

tracef("appending HelpFlag (cmd=%[1]q)", cmd.Name)
cmd.appendFlag(localHelpFlag)
cmd.globaHelpFlagAdded = true
if !flagNamesInUse(cmd.allFlags(), localHelpFlag.Names()) {
tracef("appending HelpFlag (cmd=%[1]q)", cmd.Name)
cmd.appendFlag(localHelpFlag)
cmd.globaHelpFlagAdded = true
}
} else {
tracef("HelpFlag already added, skip (cmd=%[1]q)", cmd.Name)
}
Expand Down
65 changes: 65 additions & 0 deletions command_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (
"io"
"net/mail"
"os"
"slices"
"sort"
"strconv"
"strings"
Expand Down Expand Up @@ -3987,6 +3988,70 @@ func TestFlagDuplicates(t *testing.T) {
}
}

func TestDuplicateFlagNamesAreRejected(t *testing.T) {
tests := []struct {
name string
flags []Flag
}{
{
name: "duplicate flag names",
flags: []Flag{
&BoolFlag{Name: "config"},
&StringFlag{Name: "config"},
},
},
{
name: "duplicate flag aliases",
flags: []Flag{
&BoolFlag{Name: "verbose", Aliases: []string{"v"}},
&StringFlag{Name: "value", Aliases: []string{"v"}},
},
},
}

for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
cmd := &Command{
Flags: test.flags,
Action: func(context.Context, *Command) error {
return nil
},
}

err := cmd.Run(buildTestContext(t), []string{"foo"})
require.Error(t, err)
require.Contains(t, err.Error(), "defined multiple times")
})
}
}

func TestUserDefinedHelpFlagOverridesBuiltin(t *testing.T) {
writer := &bytes.Buffer{}
cmd := &Command{
Writer: writer,
Flags: []Flag{
&BoolFlag{Name: "help", Usage: "custom help behavior"},
},
Action: func(context.Context, *Command) error {
return nil
},
}

err := cmd.Run(buildTestContext(t), []string{"foo", "--help"})
require.NoError(t, err)
require.True(t, cmd.Bool("help"))

var helpFlags int
for _, fl := range cmd.Flags {
if slices.Contains(fl.Names(), "help") {
helpFlags++
}
}
require.Equal(t, 1, helpFlags)
require.Contains(t, writer.String(), "--help custom help behavior")
require.NotContains(t, writer.String(), "--help, -h show help")
}

func TestShorthandCommand(t *testing.T) {
af := func(p *int) ActionFunc {
return func(context.Context, *Command) error {
Expand Down