From 7eeafc5381e984e39d8cfbaa866091c35bacf85e Mon Sep 17 00:00:00 2001 From: Shivansh Garg Date: Tue, 18 Aug 2026 21:52:25 +0530 Subject: [PATCH 1/3] Support custom FlagStringer for MutuallyExclusiveFlags Adds a Stringer field to MutuallyExclusiveFlags that lets callers override how flags within the group are rendered in help output. Flags opt in via the new FlagStringerOverrider interface. Fixes #2220 --- command_setup.go | 2 ++ flag.go | 12 ++++++++++++ flag_bool_with_inverse.go | 15 ++++++++++++++- flag_impl.go | 22 ++++++++++++++++----- flag_mutex.go | 20 ++++++++++++++++++++ flag_mutex_test.go | 40 +++++++++++++++++++++++++++++++++++++++ godoc-current.txt | 26 +++++++++++++++++++++++++ 7 files changed, 131 insertions(+), 6 deletions(-) diff --git a/command_setup.go b/command_setup.go index 13ef706315..3ea2fa851e 100644 --- a/command_setup.go +++ b/command_setup.go @@ -153,6 +153,7 @@ func (cmd *Command) setupDefaults(osArgs []string) { tracef("setting category on mutually exclusive flags (cmd=%[1]q)", cmd.Name) for _, grp := range cmd.MutuallyExclusiveFlags { grp.propagateCategory() + grp.propagateStringer() } tracef("setting flag categories (cmd=%[1]q)", cmd.Name) @@ -196,6 +197,7 @@ func (cmd *Command) setupSubcommand() { tracef("setting category on mutually exclusive flags (cmd=%[1]q)", cmd.Name) for _, grp := range cmd.MutuallyExclusiveFlags { grp.propagateCategory() + grp.propagateStringer() } tracef("setting flag categories (cmd=%[1]q)", cmd.Name) diff --git a/flag.go b/flag.go index 3cb9ab9608..564c5159f3 100644 --- a/flag.go +++ b/flag.go @@ -190,6 +190,18 @@ type CategorizableFlag interface { SetCategory(string) } +// FlagStringerOverrider is an optional interface that allows an individual +// flag to be given a per-flag override of [FlagStringer]. FlagBase and +// BoolWithInverseFlag implement this. It's used by +// [MutuallyExclusiveFlags.Stringer] to customize how flags within a +// mutually exclusive group are displayed in help output. +type FlagStringerOverrider interface { + // SetStringer overrides the [FlagStringFunc] used by this flag's + // String method. Passing nil restores the default behavior of using + // the package-level [FlagStringer]. + SetStringer(FlagStringFunc) +} + // LocalFlag is an interface to enable detection of flags which are local // to current command type LocalFlag interface { diff --git a/flag_bool_with_inverse.go b/flag_bool_with_inverse.go index 33c0927257..583cef8c92 100644 --- a/flag_bool_with_inverse.go +++ b/flag_bool_with_inverse.go @@ -37,6 +37,15 @@ type BoolWithInverseFlag struct { value Value // value representing this flag's value pset bool nset bool + stringer FlagStringFunc // optional per-flag override of FlagStringer +} + +// SetStringer overrides the [FlagStringFunc] used by this flag's String +// method. Passing nil restores the default behavior of using the +// package-level [FlagStringer]. This is used e.g. by +// [MutuallyExclusiveFlags.Stringer]. +func (bif *BoolWithInverseFlag) SetStringer(s FlagStringFunc) { + bif.stringer = s } func (bif *BoolWithInverseFlag) IsSet() bool { @@ -171,7 +180,11 @@ func (bif *BoolWithInverseFlag) IsVisible() bool { // Example for BoolFlag{Name: "env", Aliases: []string{"e"}} // --[no-]env, -e (default: false) func (bif *BoolWithInverseFlag) String() string { - out := FlagStringer(bif) + fs := FlagStringer + if bif.stringer != nil { + fs = bif.stringer + } + out := fs(bif) i := strings.Index(out, "\t") diff --git a/flag_impl.go b/flag_impl.go index be702d33f4..e829d6814e 100644 --- a/flag_impl.go +++ b/flag_impl.go @@ -75,11 +75,12 @@ type FlagBase[T any, C any, VC ValueCreator[T, C]] struct { ValidateDefaults bool `json:"validateDefaults"` // whether to validate defaults or not // unexported fields for internal use - count int // number of times the flag has been set - hasBeenSet bool // whether the flag has been set from env or file - applied bool // whether the flag has been applied to a flag set already - creator VC // value creator for this flag type - value Value // value representing this flag's value + count int // number of times the flag has been set + hasBeenSet bool // whether the flag has been set from env or file + applied bool // whether the flag has been applied to a flag set already + creator VC // value creator for this flag type + value Value // value representing this flag's value + stringer FlagStringFunc // optional per-flag override of FlagStringer } // GetValue returns the flags value as string representation and an empty @@ -232,9 +233,20 @@ func (f *FlagBase[T, C, V]) IsDefaultVisible() bool { // String returns a readable representation of this value (for usage defaults) func (f *FlagBase[T, C, V]) String() string { + if f.stringer != nil { + return f.stringer(f) + } return FlagStringer(f) } +// SetStringer overrides the [FlagStringFunc] used by this flag's String +// method. Passing nil restores the default behavior of using the +// package-level [FlagStringer]. This is used e.g. by +// [MutuallyExclusiveFlags.Stringer]. +func (f *FlagBase[T, C, V]) SetStringer(s FlagStringFunc) { + f.stringer = s +} + // IsSet returns whether or not the flag has been set through env or file func (f *FlagBase[T, C, V]) IsSet() bool { return f.hasBeenSet diff --git a/flag_mutex.go b/flag_mutex.go index 5a1e87cfb5..33361d850a 100644 --- a/flag_mutex.go +++ b/flag_mutex.go @@ -14,6 +14,10 @@ type MutuallyExclusiveFlags struct { // Category to apply to all flags within group Category string + + // Stringer overrides how each flag within this group is displayed in + // help output. If nil, flags use [FlagStringer] as usual. + Stringer FlagStringFunc } func (grp MutuallyExclusiveFlags) check(_ *Command) error { @@ -69,3 +73,19 @@ func (grp MutuallyExclusiveFlags) propagateCategory() { } } } + +// propagateStringer applies [MutuallyExclusiveFlags.Stringer], if set, to +// every flag within the group that supports a [FlagStringerOverrider]. +func (grp MutuallyExclusiveFlags) propagateStringer() { + if grp.Stringer == nil { + return + } + + for _, grpf := range grp.Flags { + for _, f := range grpf { + if sf, ok := f.(FlagStringerOverrider); ok { + sf.SetStringer(grp.Stringer) + } + } + } +} diff --git a/flag_mutex_test.go b/flag_mutex_test.go index 114ab57c44..c82c6b3653 100644 --- a/flag_mutex_test.go +++ b/flag_mutex_test.go @@ -130,3 +130,43 @@ func TestFlagMutuallyExclusiveFlags(t *testing.T) { }) } } + +func TestMutuallyExclusiveFlags_PropagateStringer(t *testing.T) { + customStringer := func(f Flag) string { + return "custom:" + f.Names()[0] + } + + grp := MutuallyExclusiveFlags{ + Stringer: customStringer, + Flags: [][]Flag{ + { + &StringFlag{Name: "foo"}, + &BoolWithInverseFlag{Name: "bar"}, + }, + { + &Int64Flag{Name: "baz"}, + }, + }, + } + + grp.propagateStringer() + + assert.Equal(t, "custom:foo", grp.Flags[0][0].String()) + assert.Contains(t, grp.Flags[0][1].String(), "custom:bar") + assert.Equal(t, "custom:baz", grp.Flags[1][0].String()) +} + +func TestMutuallyExclusiveFlags_PropagateStringerNil(t *testing.T) { + grp := MutuallyExclusiveFlags{ + Flags: [][]Flag{ + { + &StringFlag{Name: "foo"}, + }, + }, + } + + // should not panic and should leave flags using the default FlagStringer + grp.propagateStringer() + + assert.NotEqual(t, "", grp.Flags[0][0].String()) +} diff --git a/godoc-current.txt b/godoc-current.txt index a5382b2d87..5832486382 100644 --- a/godoc-current.txt +++ b/godoc-current.txt @@ -423,6 +423,11 @@ func (bif *BoolWithInverseFlag) Set(name, val string) error func (bif *BoolWithInverseFlag) SetCategory(c string) +func (bif *BoolWithInverseFlag) SetStringer(s FlagStringFunc) + SetStringer overrides the FlagStringFunc used by this flag's String method. + Passing nil restores the default behavior of using the package-level + FlagStringer. This is used e.g. by MutuallyExclusiveFlags.Stringer. + func (bif *BoolWithInverseFlag) String() string String implements the standard Stringer interface. @@ -1078,6 +1083,11 @@ func (f *FlagBase[T, C, V]) Set(_ string, val string) error func (f *FlagBase[T, C, V]) SetCategory(c string) +func (f *FlagBase[T, C, V]) SetStringer(s FlagStringFunc) + SetStringer overrides the FlagStringFunc used by this flag's String method. + Passing nil restores the default behavior of using the package-level + FlagStringer. This is used e.g. by MutuallyExclusiveFlags.Stringer. + func (f *FlagBase[T, C, V]) String() string String returns a readable representation of this value (for usage defaults) @@ -1127,6 +1137,18 @@ var FlagStringer FlagStringFunc = stringifyFlag FlagStringer converts a flag definition to a string. This is used by help to display a flag. +type FlagStringerOverrider interface { + // SetStringer overrides the [FlagStringFunc] used by this flag's + // String method. Passing nil restores the default behavior of using + // the package-level [FlagStringer]. + SetStringer(FlagStringFunc) +} + FlagStringerOverrider is an optional interface that allows an + individual flag to be given a per-flag override of FlagStringer. + FlagBase and BoolWithInverseFlag implement this. It's used by + MutuallyExclusiveFlags.Stringer to customize how flags within a mutually + exclusive group are displayed in help output. + type FlagsByName []Flag FlagsByName is a slice of Flag. @@ -1315,6 +1337,10 @@ type MutuallyExclusiveFlags struct { // Category to apply to all flags within group Category string + + // Stringer overrides how each flag within this group is displayed in + // help output. If nil, flags use [FlagStringer] as usual. + Stringer FlagStringFunc } MutuallyExclusiveFlags defines a mutually exclusive flag group Multiple option paths can be provided out of which only one can be defined on cmdline From b600c6ea4a848bac961bc2f7695dbc97cb735f62 Mon Sep 17 00:00:00 2001 From: Shivansh Garg Date: Tue, 18 Aug 2026 22:07:25 +0530 Subject: [PATCH 2/3] Exclude Stringer field from JSON marshaling of MutuallyExclusiveFlags FlagStringFunc is a func type and cannot be marshaled to JSON, which broke the staticcheck SA1026 lint check via json.Marshal(cmd) in existing tests. Tag the field with json:"-" to exclude it. --- flag_mutex.go | 2 +- godoc-current.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/flag_mutex.go b/flag_mutex.go index 33361d850a..a6a0c72d1e 100644 --- a/flag_mutex.go +++ b/flag_mutex.go @@ -17,7 +17,7 @@ type MutuallyExclusiveFlags struct { // Stringer overrides how each flag within this group is displayed in // help output. If nil, flags use [FlagStringer] as usual. - Stringer FlagStringFunc + Stringer FlagStringFunc `json:"-"` } func (grp MutuallyExclusiveFlags) check(_ *Command) error { diff --git a/godoc-current.txt b/godoc-current.txt index 5832486382..9dbe1047d9 100644 --- a/godoc-current.txt +++ b/godoc-current.txt @@ -1340,7 +1340,7 @@ type MutuallyExclusiveFlags struct { // Stringer overrides how each flag within this group is displayed in // help output. If nil, flags use [FlagStringer] as usual. - Stringer FlagStringFunc + Stringer FlagStringFunc `json:"-"` } MutuallyExclusiveFlags defines a mutually exclusive flag group Multiple option paths can be provided out of which only one can be defined on cmdline From 82b23b604635e17689cc3dfb69fb91bc10fc51aa Mon Sep 17 00:00:00 2001 From: Shivansh Garg Date: Tue, 18 Aug 2026 22:13:45 +0530 Subject: [PATCH 3/3] Approve v3 godoc baseline for new Stringer API --- testdata/godoc-v3.x.txt | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/testdata/godoc-v3.x.txt b/testdata/godoc-v3.x.txt index a5382b2d87..9dbe1047d9 100644 --- a/testdata/godoc-v3.x.txt +++ b/testdata/godoc-v3.x.txt @@ -423,6 +423,11 @@ func (bif *BoolWithInverseFlag) Set(name, val string) error func (bif *BoolWithInverseFlag) SetCategory(c string) +func (bif *BoolWithInverseFlag) SetStringer(s FlagStringFunc) + SetStringer overrides the FlagStringFunc used by this flag's String method. + Passing nil restores the default behavior of using the package-level + FlagStringer. This is used e.g. by MutuallyExclusiveFlags.Stringer. + func (bif *BoolWithInverseFlag) String() string String implements the standard Stringer interface. @@ -1078,6 +1083,11 @@ func (f *FlagBase[T, C, V]) Set(_ string, val string) error func (f *FlagBase[T, C, V]) SetCategory(c string) +func (f *FlagBase[T, C, V]) SetStringer(s FlagStringFunc) + SetStringer overrides the FlagStringFunc used by this flag's String method. + Passing nil restores the default behavior of using the package-level + FlagStringer. This is used e.g. by MutuallyExclusiveFlags.Stringer. + func (f *FlagBase[T, C, V]) String() string String returns a readable representation of this value (for usage defaults) @@ -1127,6 +1137,18 @@ var FlagStringer FlagStringFunc = stringifyFlag FlagStringer converts a flag definition to a string. This is used by help to display a flag. +type FlagStringerOverrider interface { + // SetStringer overrides the [FlagStringFunc] used by this flag's + // String method. Passing nil restores the default behavior of using + // the package-level [FlagStringer]. + SetStringer(FlagStringFunc) +} + FlagStringerOverrider is an optional interface that allows an + individual flag to be given a per-flag override of FlagStringer. + FlagBase and BoolWithInverseFlag implement this. It's used by + MutuallyExclusiveFlags.Stringer to customize how flags within a mutually + exclusive group are displayed in help output. + type FlagsByName []Flag FlagsByName is a slice of Flag. @@ -1315,6 +1337,10 @@ type MutuallyExclusiveFlags struct { // Category to apply to all flags within group Category string + + // Stringer overrides how each flag within this group is displayed in + // help output. If nil, flags use [FlagStringer] as usual. + Stringer FlagStringFunc `json:"-"` } MutuallyExclusiveFlags defines a mutually exclusive flag group Multiple option paths can be provided out of which only one can be defined on cmdline