From 9180b31f6f5ef050cd6cfafbbc7abb5a1099ce42 Mon Sep 17 00:00:00 2001 From: Charlie Tonneslan Date: Sat, 16 May 2026 18:51:09 -0400 Subject: [PATCH] v3: yield the version flag's -v alias to a user-defined flag The default version flag carries -v as an alias. A user-defined flag that also wants -v (canonical case: --verbose / -v) wasn't actually broken at parse time: -v ended up setting the user flag. But checkVersion went through cmd.Bool, which resolves aliases, so it asked for the value of "v" and got back the user flag's value. The end result: -v was silently treated as "print version and exit". Two changes, both narrow: - During root setup, drop any aliases from the local copy of the version flag that are already claimed by a user-defined flag's name or alias. Keeps the user flag the sole owner of the short form. - checkVersion now looks for the actual version flag attached to the command (matching against the canonical primary name from the global VersionFlag) and checks its IsSet directly, so it can't be fooled by a same-named alias on a different flag. Fixes #2229. Signed-off-by: Charlie Tonneslan --- command_setup.go | 36 ++++++++++++++++++++++++++++++++++++ command_test.go | 22 ++++++++++++++++++++++ help.go | 17 ++++++++++++----- 3 files changed, 70 insertions(+), 5 deletions(-) diff --git a/command_setup.go b/command_setup.go index cac4a30314..c187f1d1cd 100644 --- a/command_setup.go +++ b/command_setup.go @@ -84,6 +84,14 @@ func (cmd *Command) setupDefaults(osArgs []string) { var localVersionFlag Flag if globalVersionFlag, ok := VersionFlag.(*BoolFlag); ok { flag := *globalVersionFlag + // A user-defined flag may already use the short + // name the default version flag wants. `-v` for + // `--verbose` is the canonical collision: the + // version flag's `v` alias used to silently + // override the user's `--verbose`. Drop any alias + // that's already taken so the user flag wins. See + // #2229. + flag.Aliases = dropClashingAliases(flag.Aliases, cmd.Flags, flag.Name) localVersionFlag = &flag } else { localVersionFlag = VersionFlag @@ -221,3 +229,31 @@ func (cmd *Command) ensureHelp() { } } } + +// dropClashingAliases removes aliases from `aliases` that are already +// claimed by a flag in `userFlags` (either as a primary name or as one +// of its own aliases). Aliases equal to `selfName` are kept so the +// flag's primary name doesn't accidentally remove itself. +func dropClashingAliases(aliases []string, userFlags []Flag, selfName string) []string { + if len(aliases) == 0 || len(userFlags) == 0 { + return aliases + } + taken := map[string]struct{}{} + for _, f := range userFlags { + for _, n := range f.Names() { + taken[n] = struct{}{} + } + } + kept := aliases[:0:0] + for _, a := range aliases { + if a == selfName { + kept = append(kept, a) + continue + } + if _, ok := taken[a]; ok { + continue + } + kept = append(kept, a) + } + return kept +} diff --git a/command_test.go b/command_test.go index 9d77c12f23..e518e5d040 100644 --- a/command_test.go +++ b/command_test.go @@ -2674,6 +2674,28 @@ func TestCustomFlagsUsed(t *testing.T) { assert.NoError(t, err, "Run returned unexpected error") } +// Regression for #2229. A user-defined --verbose/-v flag should take +// precedence over the version flag's default -v alias instead of being +// silently shadowed by it. +func TestVersionFlagYieldsAliasToUserFlag(t *testing.T) { + var seen bool + cmd := &Command{ + Name: "demo", + Version: "1.0.0", + Flags: []Flag{ + &BoolFlag{Name: "verbose", Aliases: []string{"v"}}, + }, + Writer: io.Discard, + Action: func(_ context.Context, c *Command) error { + seen = c.Bool("verbose") + return nil + }, + } + err := cmd.Run(buildTestContext(t), []string{"demo", "-v"}) + assert.NoError(t, err) + assert.True(t, seen, "expected --verbose to be set when -v is passed") +} + func TestCustomHelpVersionFlags(t *testing.T) { cmd := &Command{ Writer: io.Discard, diff --git a/help.go b/help.go index 1fba6edf0c..b7e11a5c7b 100644 --- a/help.go +++ b/help.go @@ -459,13 +459,20 @@ func DefaultPrintHelp(out io.Writer, templ string, data any) { } func checkVersion(cmd *Command) bool { - found := false - for _, name := range VersionFlag.Names() { - if cmd.Bool(name) { - found = true + // Look up the actual version flag attached to this command (which + // may have had clashing aliases stripped during setup) rather than + // going through cmd.Bool by name. cmd.Bool resolves through + // aliases, so when a user-defined flag claims one of the default + // version flag's aliases (commonly -v for --verbose), Bool would + // return the user flag's value and trigger the version path. See + // #2229. + primary := VersionFlag.Names()[0] + for _, f := range cmd.appliedFlags { + if names := f.Names(); len(names) > 0 && names[0] == primary { + return f.IsSet() } } - return found + return false } func checkShellCompleteFlag(c *Command, arguments []string) (bool, []string) {