-
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?
Conversation
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.
if groupID == "" { | ||
fs.AddFlag(f) | ||
} | ||
return |
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.
Isn't return ""
better than returning nil
in this case?
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.
This return
statement is from VisitAll()
closure. It's not actually returning any values, but working as a control flow statement, so we don't access an out of bound index at f.Annotations[FlagHelpGroupsAnnotation]
bellow
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.
you're right, my mistake! Resolving it
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.
Hi!
Thank you for this PR, this is really appreciated!
I tested it and it works fine:
$ cat go.mod
module github.com/eiffel-fl/group-flags-example
go 1.21.7
require github.com/spf13/cobra v1.8.0
require (
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/spf13/pflag v1.0.5 // indirect
)
replace github.com/spf13/cobra => github.com/pedromotita/cobra v0.0.0-20240307182157-e9498ae28a51
$ cat main.go
package main
import (
"github.com/spf13/cobra"
)
func main() {
child := &cobra.Command{Use: "child", Run: func (*cobra.Command, []string) {}}
rootCmd := &cobra.Command{Use: "root", Run: func (*cobra.Command, []string) {}}
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 := cobra.Group{ID: "groupId", Title: "GroupTitle"}
child.AddFlagHelpGroup(&group)
_ = child.AddFlagToHelpGroupID(b, group.ID)
_ = child.AddFlagToHelpGroupID(s, group.ID)
rootCmd.Execute()
}%
$ go run . child --help
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
I have some small comments.
Best regards.
if groupID == "" { | ||
fs.AddFlag(f) | ||
} |
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.
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 comment
The 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 flagHelpGroup
.
If you see the template on Command.UsageTemplate
, I use UsageByFlagHelpGroupID
to print flag usages when len(c.FlagHelpGroups) != 0
. But I still have to cover the case where there are flags not attached to any group, which is the case of the i
flag in your example. I guarantee that there wont be any empty groupID
by returning an error at AddFlagHelpGroup
The same goes for InheritedFlags
and the global
reserved group ID.
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.
So, this code basically avoids having orphan flags without group.
This makes sense, thank you!
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.
Maybe add a constant for the special empty group id?
const NoGroup = ""
return nil | ||
} | ||
|
||
func (c *Command) hasFlagHelpGroup(groupID string) bool { |
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.
Instead of ID, should you use category
?
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.
I don't understand. Do you mean renaming groupID
to category
?
I prefer groupID
to make it clear we are dealing with Group
here
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.
I don't understand. Do you mean renaming groupID to category?
Yes.
I prefer groupID to make it clear we are dealing with Group here
Let's stick with groupID
.
|
||
// 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 comment
The reason will be displayed to describe this comment to others. Learn more.
I know that Group
is already used in cobra
, but I am wondering if we should not use Group
instead of HelpGroup
?
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.
There is a previous discussion on why to use cobra's Group
in this thread. Take a look 😉
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.
I was only referring to naming here, i.e. AddFlagGroup()
instead of AddFlagHelperGroup()
.
} | ||
|
||
// 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 comment
The 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 comment
The reason will be displayed to describe this comment to others. Learn more.
I second this point, having a variadic function would be welcomed!
The implementation could be something like this:
func (c *Command) AddFlagsToHelpGroupID(groupID string, flags ...string) error {
for _, flag := range flags {
err := AddFlagToHelpGroupID(flag, groupID)
if err != nil {
return err
}
}
return nil
}
Flags: | ||
{{$cmd.UsageByFlagHelpGroupID "" | trimTrailingWhitespaces}}{{end}} | ||
|
||
{{.Title}} Flags: |
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.
This can cause problems if localization support is added.
{{.Title}}:
is probably better.
I noticed an edge case for a library I wrote to implement this in the past, which is that you probably want to hide a group's usage if all of the flags are hidden. I'm not sure if you've accounted for this scenario. |
{{.InheritedFlags.FlagUsages | trimTrailingWhitespaces}}{{end}}{{if .HasHelpSubCommands}} | ||
{{.InheritedFlags.FlagUsages | trimTrailingWhitespaces}}{{end}}{{else}}{{$flags := .LocalFlags}}{{range $helpGroup := .FlagHelpGroups}}{{if not (eq (len ($cmd.UsageByFlagHelpGroupID "")) 0)}} | ||
|
||
Flags: |
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:
- Move down related flags that are not common to the end
- Move up important flags that are very common to make them easier to find
Example:
Common flags:
--foo int ...
--bar string ...
Security flags:
--insecure disable TLS certificate verification
--ca-file path to ca certificate
Global flags:
...
We probably could use example from real commands to evaluate this.
b := "b" | ||
s := "s" | ||
i := "i" | ||
g := "g" |
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.
These variable do not help much. Maybe using more meaningful names could be better.
|
||
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 comment
The 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.
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 comment
The 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?
if groupID == "" { | ||
fs.AddFlag(f) | ||
} |
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.
Maybe add a constant for the special empty group id?
const NoGroup = ""
// 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" { |
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.
A constant for "global" would be nice.
} | ||
|
||
// 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 comment
The 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()?
Description
Flags can now be split into groups in usage text:
Related Issues
Implementation Details
The API for this feature was based on this comment. Check it out :)