From c40db5c1afc864831ec5c4d6bcdf787daff642ea Mon Sep 17 00:00:00 2001 From: Josh French Date: Sat, 3 Feb 2024 10:27:15 -0500 Subject: [PATCH 1/6] MutuallyExclusiveFlags are included in default help text --- command.go | 2 +- help_test.go | 22 ++++++++++++++++++++++ 2 files changed, 23 insertions(+), 1 deletion(-) diff --git a/command.go b/command.go index fde2e67e65..8f3afe027e 100644 --- a/command.go +++ b/command.go @@ -855,7 +855,7 @@ func (cmd *Command) VisibleFlagCategories() []VisibleFlagCategory { // VisibleFlags returns a slice of the Flags with Hidden=false func (cmd *Command) VisibleFlags() []Flag { - return visibleFlags(cmd.Flags) + return visibleFlags(cmd.allFlags()) } func (cmd *Command) appendFlag(fl Flag) { diff --git a/help_test.go b/help_test.go index 1026e5afa5..d92b324277 100644 --- a/help_test.go +++ b/help_test.go @@ -1176,6 +1176,28 @@ func TestDefaultCompleteWithFlags(t *testing.T) { } } +func TestMutuallyExclusiveFlags(t *testing.T) { + writer := &bytes.Buffer{} + cmd := &Command{ + Name: "cmd", + Writer: writer, + MutuallyExclusiveFlags: []MutuallyExclusiveFlags{ + { + Flags: [][]Flag{ + { + &StringFlag{ + Name: "s1", + }, + }, + }}, + }, + } + + ShowAppHelp(cmd) + + assert.Contains(t, writer.String(), "--s1", "written help does not include mutex flag") +} + func TestWrap(t *testing.T) { emptywrap := wrap("", 4, 16) assert.Empty(t, emptywrap, "Wrapping empty line should return empty line") From d3ec9e84a4dfc8b883bb6184d84e22a2c3720373 Mon Sep 17 00:00:00 2001 From: Josh French Date: Sat, 3 Feb 2024 11:06:16 -0500 Subject: [PATCH 2/6] VisibleFlagCategories include MutuallyExclusiveFlags --- command.go | 6 +++--- command_test.go | 30 +++++++++++++++++++++++++----- help_test.go | 31 +++++++++++++++++++++++++++++-- 3 files changed, 57 insertions(+), 10 deletions(-) diff --git a/command.go b/command.go index 8f3afe027e..79a32137d9 100644 --- a/command.go +++ b/command.go @@ -283,7 +283,7 @@ func (cmd *Command) setupDefaults(osArgs []string) { sort.Sort(cmd.categories.(*commandCategories)) tracef("setting flag categories (cmd=%[1]q)", cmd.Name) - cmd.flagCategories = newFlagCategoriesFromFlags(cmd.Flags) + cmd.flagCategories = newFlagCategoriesFromFlags(cmd.allFlags()) if cmd.Metadata == nil { tracef("setting default Metadata (cmd=%[1]q)", cmd.Name) @@ -325,7 +325,7 @@ func (cmd *Command) setupSubcommand() { sort.Sort(cmd.categories.(*commandCategories)) tracef("setting flag categories (cmd=%[1]q)", cmd.Name) - cmd.flagCategories = newFlagCategoriesFromFlags(cmd.Flags) + cmd.flagCategories = newFlagCategoriesFromFlags(cmd.allFlags()) } func (cmd *Command) ensureHelp() { @@ -848,7 +848,7 @@ func (cmd *Command) VisibleCommands() []*Command { // VisibleFlagCategories returns a slice containing all the visible flag categories with the flags they contain func (cmd *Command) VisibleFlagCategories() []VisibleFlagCategory { if cmd.flagCategories == nil { - cmd.flagCategories = newFlagCategoriesFromFlags(cmd.Flags) + cmd.flagCategories = newFlagCategoriesFromFlags(cmd.allFlags()) } return cmd.flagCategories.VisibleCategories() } diff --git a/command_test.go b/command_test.go index bcff5f681a..937caa326c 100644 --- a/command_test.go +++ b/command_test.go @@ -524,19 +524,39 @@ func TestCommand_VisibleFlagCategories(t *testing.T) { Category: "cat1", }, }, + MutuallyExclusiveFlags: []MutuallyExclusiveFlags{{ + Flags: [][]Flag{ + { + &BoolFlag{ + Name: "mutexStrd", + }, + &BoolFlag{ + Name: "mutexCat1", + Category: "cat1", + }, + &BoolFlag{ + Name: "mutexCat2", + Category: "cat2", + }, + }, + }, + }}, } vfc := cmd.VisibleFlagCategories() - require.Len(t, vfc, 2) + require.Len(t, vfc, 3) assert.Equal(t, vfc[0].Name(), "", "expected category name to be empty") + assert.Equal(t, vfc[0].Flags()[0].Names(), []string{"mutexStrd"}) assert.Equal(t, vfc[1].Name(), "cat1", "expected category name cat1") + require.Len(t, vfc[1].Flags(), 2, "expected flag category to have two flags") + assert.Equal(t, vfc[1].Flags()[0].Names(), []string{"intd", "altd1", "altd2"}) + assert.Equal(t, vfc[1].Flags()[1].Names(), []string{"mutexCat1"}) - require.Len(t, vfc[1].Flags(), 1, "expected flag category to have just one flag") - - fl := vfc[1].Flags()[0] - assert.Equal(t, fl.Names(), []string{"intd", "altd1", "altd2"}) + assert.Equal(t, vfc[2].Name(), "cat2", "expected category name cat2") + require.Len(t, vfc[2].Flags(), 1, "expected flag category to have one flag") + assert.Equal(t, vfc[2].Flags()[0].Names(), []string{"mutexCat2"}) } func TestCommand_RunSubcommandWithDefault(t *testing.T) { diff --git a/help_test.go b/help_test.go index d92b324277..e663a30e81 100644 --- a/help_test.go +++ b/help_test.go @@ -1526,6 +1526,27 @@ func TestCategorizedHelp(t *testing.T) { Category: "cat1", }, }, + MutuallyExclusiveFlags: []MutuallyExclusiveFlags{ + { + Flags: [][]Flag{ + { + &StringFlag{ + Name: "mutexstd", + }, + &IntFlag{ + Name: "mutexcat1", + Category: "cat1", + }, + }, + { + &IntFlag{ + Name: "mutexcat2", + Category: "cat2", + }, + }, + }, + }, + }, } HelpPrinter = func(w io.Writer, templ string, data interface{}) { @@ -1554,12 +1575,18 @@ COMMANDS: for one command GLOBAL OPTIONS: - --help, -h show help (default: false) - --strd value + --help, -h show help (default: false) + --mutexstd value + --strd value cat1 --intd value, --altd1 value, --altd2 value (default: 0) + --mutexcat1 value (default: 0) + + cat2 + + --mutexcat2 value (default: 0) `, output.String()) } From 873755e3c475bbe7175c65b5fcf4c3e19ac3f04f Mon Sep 17 00:00:00 2001 From: Josh French Date: Sun, 4 Feb 2024 11:35:20 -0500 Subject: [PATCH 3/6] appease the linter --- help_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/help_test.go b/help_test.go index e663a30e81..be05725ea9 100644 --- a/help_test.go +++ b/help_test.go @@ -1193,7 +1193,7 @@ func TestMutuallyExclusiveFlags(t *testing.T) { }, } - ShowAppHelp(cmd) + _ = ShowAppHelp(cmd) assert.Contains(t, writer.String(), "--s1", "written help does not include mutex flag") } From d754265087e936351c40a97f098c670171bdefc8 Mon Sep 17 00:00:00 2001 From: Josh French Date: Sun, 4 Feb 2024 20:49:14 -0500 Subject: [PATCH 4/6] MutuallyExclusiveFlags group category is propagated to all flags --- command.go | 10 ++++++++++ command_test.go | 22 ++++++++-------------- flag.go | 3 +++ flag_impl.go | 4 ++++ flag_mutex.go | 13 +++++++++++++ help_test.go | 30 ++++++++++++++---------------- 6 files changed, 52 insertions(+), 30 deletions(-) diff --git a/command.go b/command.go index 79a32137d9..d871365ff9 100644 --- a/command.go +++ b/command.go @@ -282,6 +282,11 @@ func (cmd *Command) setupDefaults(osArgs []string) { tracef("sorting command categories (cmd=%[1]q)", cmd.Name) sort.Sort(cmd.categories.(*commandCategories)) + tracef("setting category on mutually exclusive flags (cmd=%[1]q)", cmd.Name) + for _, grp := range cmd.MutuallyExclusiveFlags { + grp.propagateCategory() + } + tracef("setting flag categories (cmd=%[1]q)", cmd.Name) cmd.flagCategories = newFlagCategoriesFromFlags(cmd.allFlags()) @@ -324,6 +329,11 @@ func (cmd *Command) setupSubcommand() { tracef("sorting command categories (cmd=%[1]q)", cmd.Name) sort.Sort(cmd.categories.(*commandCategories)) + tracef("setting category on mutually exclusive flags (cmd=%[1]q)", cmd.Name) + for _, grp := range cmd.MutuallyExclusiveFlags { + grp.propagateCategory() + } + tracef("setting flag categories (cmd=%[1]q)", cmd.Name) cmd.flagCategories = newFlagCategoriesFromFlags(cmd.allFlags()) } diff --git a/command_test.go b/command_test.go index 937caa326c..9a69e2f02b 100644 --- a/command_test.go +++ b/command_test.go @@ -525,38 +525,32 @@ func TestCommand_VisibleFlagCategories(t *testing.T) { }, }, MutuallyExclusiveFlags: []MutuallyExclusiveFlags{{ + Category: "cat2", Flags: [][]Flag{ { - &BoolFlag{ - Name: "mutexStrd", - }, - &BoolFlag{ - Name: "mutexCat1", - Category: "cat1", - }, - &BoolFlag{ - Name: "mutexCat2", - Category: "cat2", + &StringFlag{ + Name: "mutex", }, }, }, }}, } + cmd.MutuallyExclusiveFlags[0].propagateCategory() + vfc := cmd.VisibleFlagCategories() require.Len(t, vfc, 3) assert.Equal(t, vfc[0].Name(), "", "expected category name to be empty") - assert.Equal(t, vfc[0].Flags()[0].Names(), []string{"mutexStrd"}) + assert.Equal(t, vfc[0].Flags()[0].Names(), []string{"strd"}) assert.Equal(t, vfc[1].Name(), "cat1", "expected category name cat1") - require.Len(t, vfc[1].Flags(), 2, "expected flag category to have two flags") + require.Len(t, vfc[1].Flags(), 1, "expected flag category to have one flag") assert.Equal(t, vfc[1].Flags()[0].Names(), []string{"intd", "altd1", "altd2"}) - assert.Equal(t, vfc[1].Flags()[1].Names(), []string{"mutexCat1"}) assert.Equal(t, vfc[2].Name(), "cat2", "expected category name cat2") require.Len(t, vfc[2].Flags(), 1, "expected flag category to have one flag") - assert.Equal(t, vfc[2].Flags()[0].Names(), []string{"mutexCat2"}) + assert.Equal(t, vfc[2].Flags()[0].Names(), []string{"mutex"}) } func TestCommand_RunSubcommandWithDefault(t *testing.T) { diff --git a/flag.go b/flag.go index 5332b22201..d7f771b063 100644 --- a/flag.go +++ b/flag.go @@ -162,6 +162,9 @@ type VisibleFlag interface { type CategorizableFlag interface { // Returns the category of the flag GetCategory() string + + // Sets the category of the flag + SetCategory(string) } // PersistentFlag is an interface to enable detection of flags which are persistent diff --git a/flag_impl.go b/flag_impl.go index dd21f3beb9..3e9fdbc901 100644 --- a/flag_impl.go +++ b/flag_impl.go @@ -221,6 +221,10 @@ func (f *FlagBase[T, C, V]) GetCategory() string { return f.Category } +func (f *FlagBase[T, C, V]) SetCategory(c string) { + f.Category = c +} + // GetUsage returns the usage string for the flag func (f *FlagBase[T, C, V]) GetUsage() string { return f.Usage diff --git a/flag_mutex.go b/flag_mutex.go index 2ca38e71ce..e03de8da9b 100644 --- a/flag_mutex.go +++ b/flag_mutex.go @@ -11,6 +11,9 @@ type MutuallyExclusiveFlags struct { // whether this group is required Required bool + + // Category to apply to all flags within group + Category string } func (grp MutuallyExclusiveFlags) check(cmd *Command) error { @@ -41,3 +44,13 @@ func (grp MutuallyExclusiveFlags) check(cmd *Command) error { } return nil } + +func (grp MutuallyExclusiveFlags) propagateCategory() { + for _, grpf := range grp.Flags { + for _, f := range grpf { + if cf, ok := f.(CategorizableFlag); ok { + cf.SetCategory(grp.Category) + } + } + } +} diff --git a/help_test.go b/help_test.go index be05725ea9..44d715a55a 100644 --- a/help_test.go +++ b/help_test.go @@ -1528,20 +1528,22 @@ func TestCategorizedHelp(t *testing.T) { }, MutuallyExclusiveFlags: []MutuallyExclusiveFlags{ { + Category: "cat1", Flags: [][]Flag{ { &StringFlag{ - Name: "mutexstd", - }, - &IntFlag{ - Name: "mutexcat1", - Category: "cat1", + Name: "m1", + Category: "overridden", }, }, + }, + }, + { + Flags: [][]Flag{ { - &IntFlag{ - Name: "mutexcat2", - Category: "cat2", + &StringFlag{ + Name: "m2", + Category: "ignored", }, }, }, @@ -1575,18 +1577,14 @@ COMMANDS: for one command GLOBAL OPTIONS: - --help, -h show help (default: false) - --mutexstd value - --strd value + --help, -h show help (default: false) + --m2 value + --strd value cat1 --intd value, --altd1 value, --altd2 value (default: 0) - --mutexcat1 value (default: 0) - - cat2 - - --mutexcat2 value (default: 0) + --m1 value `, output.String()) } From fcfd5b3a90c1131622aa1083a268eee2a4418634 Mon Sep 17 00:00:00 2001 From: Josh French Date: Sun, 4 Feb 2024 21:11:07 -0500 Subject: [PATCH 5/6] update docs --- godoc-current.txt | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/godoc-current.txt b/godoc-current.txt index 92162465a4..e30dbe2bb2 100644 --- a/godoc-current.txt +++ b/godoc-current.txt @@ -289,6 +289,9 @@ func (s *BoolWithInverseFlag) Value() bool type CategorizableFlag interface { // Returns the category of the flag GetCategory() string + + // Sets the category of the flag + SetCategory(string) } CategorizableFlag is an interface that allows us to potentially use a flag in a categorized representation. @@ -702,6 +705,8 @@ func (f *FlagBase[T, C, V]) Names() []string func (f *FlagBase[T, C, V]) RunAction(ctx context.Context, cmd *Command) error RunAction executes flag action if set +func (f *FlagBase[T, C, V]) SetCategory(c string) + func (f *FlagBase[T, C, V]) String() string String returns a readable representation of this value (for usage defaults) @@ -821,6 +826,9 @@ type MutuallyExclusiveFlags struct { // whether this group is required Required bool + + // Category to apply to all flags within group + Category string } MutuallyExclusiveFlags defines a mutually exclusive flag group Multiple option paths can be provided out of which only one can be defined on cmdline From a366d0b6670cc820b0cb949fd8ec21ca1fb04ef2 Mon Sep 17 00:00:00 2001 From: Josh French Date: Sun, 11 Feb 2024 19:28:00 -0500 Subject: [PATCH 6/6] approve v3 docs changes --- testdata/godoc-v3.x.txt | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/testdata/godoc-v3.x.txt b/testdata/godoc-v3.x.txt index 92162465a4..e30dbe2bb2 100644 --- a/testdata/godoc-v3.x.txt +++ b/testdata/godoc-v3.x.txt @@ -289,6 +289,9 @@ func (s *BoolWithInverseFlag) Value() bool type CategorizableFlag interface { // Returns the category of the flag GetCategory() string + + // Sets the category of the flag + SetCategory(string) } CategorizableFlag is an interface that allows us to potentially use a flag in a categorized representation. @@ -702,6 +705,8 @@ func (f *FlagBase[T, C, V]) Names() []string func (f *FlagBase[T, C, V]) RunAction(ctx context.Context, cmd *Command) error RunAction executes flag action if set +func (f *FlagBase[T, C, V]) SetCategory(c string) + func (f *FlagBase[T, C, V]) String() string String returns a readable representation of this value (for usage defaults) @@ -821,6 +826,9 @@ type MutuallyExclusiveFlags struct { // whether this group is required Required bool + + // Category to apply to all flags within group + Category string } MutuallyExclusiveFlags defines a mutually exclusive flag group Multiple option paths can be provided out of which only one can be defined on cmdline