Skip to content
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

Add groups for commands in help #1003

Merged
merged 3 commits into from
Oct 10, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ Cobra provides:
* Global, local and cascading flags
* Intelligent suggestions (`app srver`... did you mean `app server`?)
* Automatic help generation for commands and flags
* Grouping help for subcommands
* Automatic help flag recognition of `-h`, `--help`, etc.
* Automatically generated shell autocomplete for your application (bash, zsh, fish, powershell)
* Automatically generated man pages for your application
Expand Down
80 changes: 77 additions & 3 deletions command.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,12 @@ const FlagSetByCobraAnnotation = "cobra_annotation_flag_set_by_cobra"
// FParseErrWhitelist configures Flag parse errors to be ignored
type FParseErrWhitelist flag.ParseErrorsWhitelist

// Structure to manage groups for commands
type Group struct {
ID string
Title string
}
aawsome marked this conversation as resolved.
Show resolved Hide resolved

// Command is just that, a command for your application.
// E.g. 'go run ...' - 'run' is the command. Cobra requires
// you to define the usage and description as part of your command
Expand All @@ -61,6 +67,9 @@ type Command struct {
// Short is the short description shown in the 'help' output.
Short string

// The group id under which this subcommand is grouped in the 'help' output of its parent.
GroupID string

// Long is the long message shown in the 'help <this-command>' output.
Long string

Expand Down Expand Up @@ -128,6 +137,9 @@ type Command struct {
// PersistentPostRunE: PersistentPostRun but returns an error.
PersistentPostRunE func(cmd *Command, args []string) error

// groups for subcommands
commandgroups []*Group

// args is actual args parsed from flags.
args []string
// flagErrorBuf contains all error messages from pflag.
Expand Down Expand Up @@ -160,6 +172,12 @@ type Command struct {
// helpCommand is command with usage 'help'. If it's not defined by user,
// cobra uses default help command.
helpCommand *Command
// helpCommandGroupID is the group id for the helpCommand
helpCommandGroupID string

// completionCommandGroupID is the group id for the completion command
completionCommandGroupID string

// versionTemplate is the version template defined by user.
versionTemplate string

Expand Down Expand Up @@ -303,6 +321,21 @@ func (c *Command) SetHelpCommand(cmd *Command) {
c.helpCommand = cmd
}

// SetHelpCommandGroup sets the group id of the help command.
func (c *Command) SetHelpCommandGroupID(groupID string) {
if c.helpCommand != nil {
c.helpCommand.GroupID = groupID
}
// helpCommandGroupID is used if no helpCommand is defined by the user
c.helpCommandGroupID = groupID
}

// SetCompletionCommandGroup sets the group id of the completion command.
func (c *Command) SetCompletionCommandGroupID(groupID string) {
// completionCommandGroupID is used if no completion command is defined by the user
c.Root().completionCommandGroupID = groupID
}

// SetHelpTemplate sets help template to be used. Application can use it to set custom template.
func (c *Command) SetHelpTemplate(s string) {
c.helpTemplate = s
Expand Down Expand Up @@ -511,10 +544,16 @@ Aliases:
{{.NameAndAliases}}{{end}}{{if .HasExample}}

Examples:
{{.Example}}{{end}}{{if .HasAvailableSubCommands}}
{{.Example}}{{end}}{{if .HasAvailableSubCommands}}{{$cmds := .Commands}}{{if eq (len .Groups) 0}}

Available Commands:{{range $cmds}}{{if (or .IsAvailableCommand (eq .Name "help"))}}
{{rpad .Name .NamePadding }} {{.Short}}{{end}}{{end}}{{else}}{{range $group := .Groups}}

Available Commands:{{range .Commands}}{{if (or .IsAvailableCommand (eq .Name "help"))}}
{{rpad .Name .NamePadding }} {{.Short}}{{end}}{{end}}{{end}}{{if .HasAvailableLocalFlags}}
{{.Title}}{{range $cmds}}{{if (and (eq .GroupID $group.ID) (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}}

Flags:
{{.LocalFlags.FlagUsages | trimTrailingWhitespaces}}{{end}}{{if .HasAvailableInheritedFlags}}
Expand Down Expand Up @@ -1140,6 +1179,7 @@ Simply type ` + c.Name() + ` help [path to command] for full details.`,
CheckErr(cmd.Help())
}
},
GroupID: c.helpCommandGroupID,
}
}
c.RemoveCommand(c.helpCommand)
Expand Down Expand Up @@ -1178,6 +1218,10 @@ func (c *Command) AddCommand(cmds ...*Command) {
panic("Command can't be a child of itself")
}
cmds[i].parent = c
// if Group is not defined let the developer know right away
if x.GroupID != "" && !c.ContainsGroup(x.GroupID) {
panic(fmt.Sprintf("Group id '%s' is not defined for subcommand '%s'", x.GroupID, cmds[i].CommandPath()))
}
aawsome marked this conversation as resolved.
Show resolved Hide resolved
// update max lengths
usageLen := len(x.Use)
if usageLen > c.commandsMaxUseLen {
Expand All @@ -1200,6 +1244,36 @@ func (c *Command) AddCommand(cmds ...*Command) {
}
}

// Groups returns a slice of child command groups.
func (c *Command) Groups() []*Group {
return c.commandgroups
}

// AllChildCommandsHaveGroup returns if all subcommands are assigned to a group
func (c *Command) AllChildCommandsHaveGroup() bool {
for _, sub := range c.commands {
if (sub.IsAvailableCommand() || sub == c.helpCommand) && sub.GroupID == "" {
return false
}
}
return true
}

// ContainGroups return if groupID exists in the list of command groups.
func (c *Command) ContainsGroup(groupID string) bool {
for _, x := range c.commandgroups {
if x.ID == groupID {
return true
}
}
return false
}

// AddGroup adds one or more command groups to this parent command.
func (c *Command) AddGroup(groups ...*Group) {
c.commandgroups = append(c.commandgroups, groups...)
}

// RemoveCommand removes one or more commands from a parent command.
func (c *Command) RemoveCommand(cmds ...*Command) {
commands := []*Command{}
Expand Down
95 changes: 95 additions & 0 deletions command_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1767,6 +1767,101 @@ func TestEnableCommandSortingIsDisabled(t *testing.T) {
EnableCommandSorting = defaultCommandSorting
}

func TestUsageWithGroup(t *testing.T) {
var rootCmd = &Command{Use: "root", Short: "test", Run: emptyRun}
rootCmd.CompletionOptions.DisableDefaultCmd = true

rootCmd.AddGroup(&Group{ID: "group1", Title: "group1"})
rootCmd.AddGroup(&Group{ID: "group2", Title: "group2"})

rootCmd.AddCommand(&Command{Use: "cmd1", GroupID: "group1", Run: emptyRun})
rootCmd.AddCommand(&Command{Use: "cmd2", GroupID: "group2", Run: emptyRun})

output, err := executeCommand(rootCmd, "--help")
if err != nil {
t.Errorf("Unexpected error: %v", err)
}

// help should be ungrouped here
checkStringContains(t, output, "\nAdditional Commands:\n help")
checkStringContains(t, output, "\ngroup1\n cmd1")
checkStringContains(t, output, "\ngroup2\n cmd2")
}

func TestUsageHelpGroup(t *testing.T) {
var rootCmd = &Command{Use: "root", Short: "test", Run: emptyRun}
rootCmd.CompletionOptions.DisableDefaultCmd = true

rootCmd.AddGroup(&Group{ID: "group", Title: "group"})
rootCmd.AddCommand(&Command{Use: "xxx", GroupID: "group", Run: emptyRun})
rootCmd.SetHelpCommandGroupID("group")

output, err := executeCommand(rootCmd, "--help")
if err != nil {
t.Errorf("Unexpected error: %v", err)
}

// now help should be grouped under "group"
checkStringOmits(t, output, "\nAdditional Commands:\n help")
checkStringContains(t, output, "\ngroup\n help")
}

func TestUsageCompletionGroup(t *testing.T) {
var rootCmd = &Command{Use: "root", Short: "test", Run: emptyRun}

rootCmd.AddGroup(&Group{ID: "group", Title: "group"})
rootCmd.AddGroup(&Group{ID: "help", Title: "help"})

rootCmd.AddCommand(&Command{Use: "xxx", GroupID: "group", Run: emptyRun})
rootCmd.SetHelpCommandGroupID("help")
rootCmd.SetCompletionCommandGroupID("group")

output, err := executeCommand(rootCmd, "--help")
if err != nil {
t.Errorf("Unexpected error: %v", err)
}

// now completion should be grouped under "group"
checkStringOmits(t, output, "\nAdditional Commands:\n completion")
checkStringContains(t, output, "\ngroup\n completion")
}

func TestUngroupedCommand(t *testing.T) {
var rootCmd = &Command{Use: "root", Short: "test", Run: emptyRun}

rootCmd.AddGroup(&Group{ID: "group", Title: "group"})
rootCmd.AddGroup(&Group{ID: "help", Title: "help"})

rootCmd.AddCommand(&Command{Use: "xxx", GroupID: "group", Run: emptyRun})
rootCmd.SetHelpCommandGroupID("help")
rootCmd.SetCompletionCommandGroupID("group")

// Add a command without a group
rootCmd.AddCommand(&Command{Use: "yyy", Run: emptyRun})

output, err := executeCommand(rootCmd, "--help")
if err != nil {
t.Errorf("Unexpected error: %v", err)
}

// The yyy command should be in the additional command "group"
checkStringContains(t, output, "\nAdditional Commands:\n yyy")
}

func TestAddGroup(t *testing.T) {
var rootCmd = &Command{Use: "root", Short: "test", Run: emptyRun}

rootCmd.AddGroup(&Group{ID: "group", Title: "Test group"})
rootCmd.AddCommand(&Command{Use: "cmd", GroupID: "group", Run: emptyRun})

output, err := executeCommand(rootCmd, "--help")
if err != nil {
t.Errorf("Unexpected error: %v", err)
}

checkStringContains(t, output, "\nTest group\n cmd")
}

func TestSetOutput(t *testing.T) {
c := &Command{}
c.SetOutput(nil)
Expand Down
1 change: 1 addition & 0 deletions completions.go
Original file line number Diff line number Diff line change
Expand Up @@ -673,6 +673,7 @@ See each sub-command's help for details on how to use the generated script.
Args: NoArgs,
ValidArgsFunction: NoFileCompletions,
Hidden: c.CompletionOptions.HiddenDefaultCmd,
GroupID: c.completionCommandGroupID,
}
c.AddCommand(completionCmd)

Expand Down
7 changes: 7 additions & 0 deletions user_guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -490,6 +490,13 @@ command and flag definitions are needed.
Help is just a command like any other. There is no special logic or behavior
around it. In fact, you can provide your own if you want.

### Grouping commands in help

Cobra supports grouping of available commands. Groups must be explicitly defined by `AddGroup` and set by
the `GroupId` element of a subcommand. The groups will appear in the same order as they are defined.
If you use the generated `help` or `completion` commands, you can set the group ids by `SetHelpCommandGroupId`
and `SetCompletionCommandGroupId`, respectively.

### Defining your own help

You can provide your own Help command or your own template for the default command to use
Expand Down