-
Notifications
You must be signed in to change notification settings - Fork 2.9k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat: add flag help groups #2117
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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: | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This can cause problems if localization support is added. |
||
{{$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 { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I know that There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. There is a previous discussion on why to use cobra's There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I was only referring to naming here, i.e. |
||
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 { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Instead of ID, should you use There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't understand. Do you mean renaming I prefer There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Yes.
Let's stick with |
||
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 { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It could be useful to have some option to add multiple flags in one call (like in AddFlagHelpGroup) There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I second this point, having a variadic function would be welcomed! func (c *Command) AddFlagsToHelpGroupID(groupID string, flags ...string) error {
for _, flag := range flags {
err := AddFlagToHelpGroupID(flag, groupID)
if err != nil {
return err
}
}
return nil
} |
||
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" | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Seems that the code return help for single flag group, not help split by groups. Function comment needs update? Also maybe rename the function to UsageForHelpGroup()? |
||
func (c *Command) UsageByFlagHelpGroupID(groupID string) string { | ||
if groupID == "global" { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. A constant for "global" would be nice. |
||
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) | ||
} | ||
Comment on lines
+1412
to
+1414
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I am not sure to understand why you are adding flags without group to this flag set, can you please shed some light? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is the solution I've found for printing the usage of flags that have not been added to any If you see the template on The same goes for There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. So, this code basically avoids having orphan flags without group. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Maybe add a constant for the special empty group id?
|
||
return | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Isn't return There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. you're right, my mistake! Resolving it |
||
} | ||
|
||
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 { | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. child and rootCmd are not consistent. I think changing rootCmd to root will help. |
||
|
||
rootCmd.AddCommand(child) | ||
|
||
b := "b" | ||
s := "s" | ||
i := "i" | ||
g := "g" | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. These variable do not help much. Maybe using more meaningful names could be better. |
||
|
||
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) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This add the flag to the help group - not the to help group id. Rename to AddFlagToHelpGroup? |
||
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"} | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
"Flags" and "Title Flags" are not consistent. It would be nice if all flags are grouped, and so every group has a meaning full name.
If only some flags are grouped, maybe the rest (the un-grouped flags) can appear last instead of first?
We can have 2 reasons to group flags:
Example:
We probably could use example from real commands to evaluate this.