From 44743e22c9f247b6dfcab12b329f18e7c629f188 Mon Sep 17 00:00:00 2001 From: deepakganesh78 Date: Sun, 2 Aug 2026 21:01:22 +0530 Subject: [PATCH] fix: reject duplicate flag names Detect duplicate local flag names and aliases after built-in defaults are added so ambiguous command configurations fail instead of rendering repeated flags or shadowing built-in help. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- command.go | 13 ++++++++++ command_run.go | 3 +++ command_setup.go | 8 +++--- command_test.go | 65 ++++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 86 insertions(+), 3 deletions(-) diff --git a/command.go b/command.go index 4cd907a558..465d39217a 100644 --- a/command.go +++ b/command.go @@ -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...) diff --git a/command_run.go b/command_run.go index 8d5907151e..15ae2fecbe 100644 --- a/command_run.go +++ b/command_run.go @@ -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 { diff --git a/command_setup.go b/command_setup.go index 646e270ce7..20984ec4c7 100644 --- a/command_setup.go +++ b/command_setup.go @@ -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) } diff --git a/command_test.go b/command_test.go index cad70b7801..e2c0f2dc83 100644 --- a/command_test.go +++ b/command_test.go @@ -10,6 +10,7 @@ import ( "io" "net/mail" "os" + "slices" "sort" "strconv" "strings" @@ -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 {