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"}