From e9498ae28a51b66b113c01c75c12223704d757ea Mon Sep 17 00:00:00 2001
From: pedromotita
Date: Thu, 7 Mar 2024 15:21:31 -0300
Subject: [PATCH] feat: add flag help groups
Issue #1327
---
command.go | 91 +++++++++++++++++++++++++++++++++++++++++++++++--
command_test.go | 76 +++++++++++++++++++++++++++++++++++++++++
2 files changed, 165 insertions(+), 2 deletions(-)
diff --git a/command.go b/command.go
index b6f8f4b14..2526dfd95 100644
--- a/command.go
+++ b/command.go
@@ -32,6 +32,7 @@ import (
const (
FlagSetByCobraAnnotation = "cobra_annotation_flag_set_by_cobra"
+ FlagHelpGroupAnnotation = "cobra_annotation_flag_help_group"
CommandDisplayNameAnnotation = "cobra_annotation_command_display_name"
)
@@ -145,6 +146,9 @@ type Command struct {
// groups for subcommands
commandgroups []*Group
+ // groups for flags in usage text.
+ flagHelpGroups []*Group
+
// args is actual args parsed from flags.
args []string
// flagErrorBuf contains all error messages from pflag.
@@ -568,13 +572,22 @@ Available Commands:{{range $cmds}}{{if (or .IsAvailableCommand (eq .Name "help")
{{rpad .Name .NamePadding }} {{.Short}}{{end}}{{end}}{{end}}{{if not .AllChildCommandsHaveGroup}}
Additional Commands:{{range $cmds}}{{if (and (eq .GroupID "") (or .IsAvailableCommand (eq .Name "help")))}}
- {{rpad .Name .NamePadding }} {{.Short}}{{end}}{{end}}{{end}}{{end}}{{end}}{{if .HasAvailableLocalFlags}}
+ {{rpad .Name .NamePadding }} {{.Short}}{{end}}{{end}}{{end}}{{end}}{{end}}{{$cmd := .}}{{if eq (len .FlagHelpGroups) 0}}{{if .HasAvailableLocalFlags}}
Flags:
{{.LocalFlags.FlagUsages | trimTrailingWhitespaces}}{{end}}{{if .HasAvailableInheritedFlags}}
Global Flags:
-{{.InheritedFlags.FlagUsages | trimTrailingWhitespaces}}{{end}}{{if .HasHelpSubCommands}}
+{{.InheritedFlags.FlagUsages | trimTrailingWhitespaces}}{{end}}{{else}}{{$flags := .LocalFlags}}{{range $helpGroup := .FlagHelpGroups}}{{if not (eq (len ($cmd.UsageByFlagHelpGroupID "")) 0)}}
+
+Flags:
+{{$cmd.UsageByFlagHelpGroupID "" | trimTrailingWhitespaces}}{{end}}
+
+{{.Title}} Flags:
+{{$cmd.UsageByFlagHelpGroupID $helpGroup.ID | trimTrailingWhitespaces}}{{if not (eq (len ($cmd.UsageByFlagHelpGroupID "global")) 0)}}
+
+Global Flags:
+{{$cmd.UsageByFlagHelpGroupID "global" | trimTrailingWhitespaces}}{{end}}{{end}}{{end}}{{if .HasHelpSubCommands}}
Additional help topics:{{range .Commands}}{{if .IsAdditionalHelpTopicCommand}}
{{rpad .CommandPath .CommandPathPadding}} {{.Short}}{{end}}{{end}}{{end}}{{if .HasAvailableSubCommands}}
@@ -1336,6 +1349,80 @@ func (c *Command) Groups() []*Group {
return c.commandgroups
}
+// FlagHelpGroups returns a slice of the command's flag help groups
+func (c *Command) FlagHelpGroups() []*Group {
+ return c.flagHelpGroups
+}
+
+// AddFlagHelpGroup adds one more flag help group do the command. Returns an error if the Group.ID is empty,
+// or if the "global" reserved ID is used
+func (c *Command) AddFlagHelpGroup(groups ...*Group) error {
+ for _, group := range groups {
+ if len(group.ID) == 0 {
+ return fmt.Errorf("flag help group ID must have at least one character")
+ }
+
+ if group.ID == "global" {
+ return fmt.Errorf(`"global" is a reserved flag help group ID`)
+ }
+
+ c.flagHelpGroups = append(c.flagHelpGroups, group)
+ }
+
+ return nil
+}
+
+func (c *Command) hasFlagHelpGroup(groupID string) bool {
+ for _, g := range c.flagHelpGroups {
+ if g.ID == groupID {
+ return true
+ }
+ }
+
+ return false
+}
+
+// AddFlagToHelpGroupID adds associates a flag to a groupID. Returns an error if the flag or group is non-existent
+func (c *Command) AddFlagToHelpGroupID(flag, groupID string) error {
+ lf := c.Flags()
+
+ if !c.hasFlagHelpGroup(groupID) {
+ return fmt.Errorf("no such flag help group: %v", groupID)
+ }
+
+ err := lf.SetAnnotation(flag, FlagHelpGroupAnnotation, []string{groupID})
+ if err != nil {
+ return err
+ }
+
+ return nil
+}
+
+// UsageByFlagHelpGroupID returns the command flag's usage split by flag help groups. Flags without groups associated
+// will appear under "Flags", and inherited flags will appear under "Global Flags"
+func (c *Command) UsageByFlagHelpGroupID(groupID string) string {
+ if groupID == "global" {
+ return c.InheritedFlags().FlagUsages()
+ }
+
+ fs := &flag.FlagSet{}
+
+ c.LocalFlags().VisitAll(func(f *flag.Flag) {
+ if _, ok := f.Annotations[FlagHelpGroupAnnotation]; !ok {
+ if groupID == "" {
+ fs.AddFlag(f)
+ }
+ return
+ }
+
+ if id := f.Annotations[FlagHelpGroupAnnotation][0]; id == groupID {
+ fs.AddFlag(f)
+ }
+ })
+
+ return fs.FlagUsages()
+}
+
// AllChildCommandsHaveGroup returns if all subcommands are assigned to a group
func (c *Command) AllChildCommandsHaveGroup() bool {
for _, sub := range c.commands {
diff --git a/command_test.go b/command_test.go
index b7d88e4d5..b165f2777 100644
--- a/command_test.go
+++ b/command_test.go
@@ -920,6 +920,82 @@ func TestPersistentRequiredFlagsWithDisableFlagParsing(t *testing.T) {
}
}
+func TestFlagHelpGroups(t *testing.T) {
+
+ t.Run("add flag to non-existing flag help group", func(t *testing.T) {
+ rootCmd := &Command{Use: "root", Run: emptyRun}
+ b := "b"
+
+ rootCmd.Flags().Bool(b, false, "bool flag")
+
+ err := rootCmd.AddFlagToHelpGroupID(b, "id")
+ if err == nil {
+ t.Error("Expected error when adding a flag to non-existent flag help group")
+ }
+ })
+
+ t.Run("add non-existing flag to flag help group", func(t *testing.T) {
+ rootCmd := &Command{Use: "root", Run: emptyRun}
+
+ group := Group{ID: "id", Title: "GroupTitle"}
+ rootCmd.AddFlagHelpGroup(&group)
+
+ err := rootCmd.AddFlagToHelpGroupID("", "id")
+ if err == nil {
+ t.Error("Expected error when adding a non-existent flag to flag help group")
+ }
+
+ })
+
+ t.Run("add flag to flag help group", func(t *testing.T) {
+ child := &Command{Use: "child", Run: emptyRun}
+ rootCmd := &Command{Use: "root", Run: emptyRun}
+
+ rootCmd.AddCommand(child)
+
+ b := "b"
+ s := "s"
+ i := "i"
+ g := "g"
+
+ child.Flags().Bool(b, false, "bool flag")
+ child.Flags().String(s, "", "string flag")
+ child.Flags().Int(i, 0, "int flag")
+ rootCmd.PersistentFlags().String(g, "", "global flag")
+
+ group := Group{ID: "groupId", Title: "GroupTitle"}
+
+ child.AddFlagHelpGroup(&group)
+
+ _ = child.AddFlagToHelpGroupID(b, group.ID)
+ _ = child.AddFlagToHelpGroupID(s, group.ID)
+ x := `Usage:
+ root child [flags]
+
+Flags:
+ -h, --help help for child
+ --i int int flag
+
+GroupTitle Flags:
+ --b bool flag
+ --s string string flag
+
+Global Flags:
+ --g string global flag
+`
+
+ got, err := executeCommand(rootCmd, "help", "child")
+ if err != nil {
+ t.Errorf("Unexpected error: %v", err)
+ }
+
+ if got != x {
+ t.Errorf("Help text mismatch.\nExpected:\n%s\n\nGot:\n%s\n", x, got)
+ }
+ })
+
+}
+
func TestInitHelpFlagMergesFlags(t *testing.T) {
usage := "custom flag"
rootCmd := &Command{Use: "root"}