Skip to content

Commit 2169adb

Browse files
aawsomemarckhouzam
andauthored
Add groups for commands in help (#1003)
* Add tests for grouping commands * Adds Additional Command section in help Signed-off-by: Marc Khouzam <marc.khouzam@gmail.com> Co-authored-by: Marc Khouzam <marc.khouzam@gmail.com>
1 parent 212ea40 commit 2169adb

File tree

5 files changed

+181
-3
lines changed

5 files changed

+181
-3
lines changed

README.md

+1
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ Cobra provides:
2323
* Global, local and cascading flags
2424
* Intelligent suggestions (`app srver`... did you mean `app server`?)
2525
* Automatic help generation for commands and flags
26+
* Grouping help for subcommands
2627
* Automatic help flag recognition of `-h`, `--help`, etc.
2728
* Automatically generated shell autocomplete for your application (bash, zsh, fish, powershell)
2829
* Automatically generated man pages for your application

command.go

+77-3
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,12 @@ const FlagSetByCobraAnnotation = "cobra_annotation_flag_set_by_cobra"
3535
// FParseErrWhitelist configures Flag parse errors to be ignored
3636
type FParseErrWhitelist flag.ParseErrorsWhitelist
3737

38+
// Structure to manage groups for commands
39+
type Group struct {
40+
ID string
41+
Title string
42+
}
43+
3844
// Command is just that, a command for your application.
3945
// E.g. 'go run ...' - 'run' is the command. Cobra requires
4046
// you to define the usage and description as part of your command
@@ -61,6 +67,9 @@ type Command struct {
6167
// Short is the short description shown in the 'help' output.
6268
Short string
6369

70+
// The group id under which this subcommand is grouped in the 'help' output of its parent.
71+
GroupID string
72+
6473
// Long is the long message shown in the 'help <this-command>' output.
6574
Long string
6675

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

140+
// groups for subcommands
141+
commandgroups []*Group
142+
131143
// args is actual args parsed from flags.
132144
args []string
133145
// flagErrorBuf contains all error messages from pflag.
@@ -160,6 +172,12 @@ type Command struct {
160172
// helpCommand is command with usage 'help'. If it's not defined by user,
161173
// cobra uses default help command.
162174
helpCommand *Command
175+
// helpCommandGroupID is the group id for the helpCommand
176+
helpCommandGroupID string
177+
178+
// completionCommandGroupID is the group id for the completion command
179+
completionCommandGroupID string
180+
163181
// versionTemplate is the version template defined by user.
164182
versionTemplate string
165183

@@ -303,6 +321,21 @@ func (c *Command) SetHelpCommand(cmd *Command) {
303321
c.helpCommand = cmd
304322
}
305323

324+
// SetHelpCommandGroup sets the group id of the help command.
325+
func (c *Command) SetHelpCommandGroupID(groupID string) {
326+
if c.helpCommand != nil {
327+
c.helpCommand.GroupID = groupID
328+
}
329+
// helpCommandGroupID is used if no helpCommand is defined by the user
330+
c.helpCommandGroupID = groupID
331+
}
332+
333+
// SetCompletionCommandGroup sets the group id of the completion command.
334+
func (c *Command) SetCompletionCommandGroupID(groupID string) {
335+
// completionCommandGroupID is used if no completion command is defined by the user
336+
c.Root().completionCommandGroupID = groupID
337+
}
338+
306339
// SetHelpTemplate sets help template to be used. Application can use it to set custom template.
307340
func (c *Command) SetHelpTemplate(s string) {
308341
c.helpTemplate = s
@@ -511,10 +544,16 @@ Aliases:
511544
{{.NameAndAliases}}{{end}}{{if .HasExample}}
512545
513546
Examples:
514-
{{.Example}}{{end}}{{if .HasAvailableSubCommands}}
547+
{{.Example}}{{end}}{{if .HasAvailableSubCommands}}{{$cmds := .Commands}}{{if eq (len .Groups) 0}}
548+
549+
Available Commands:{{range $cmds}}{{if (or .IsAvailableCommand (eq .Name "help"))}}
550+
{{rpad .Name .NamePadding }} {{.Short}}{{end}}{{end}}{{else}}{{range $group := .Groups}}
515551
516-
Available Commands:{{range .Commands}}{{if (or .IsAvailableCommand (eq .Name "help"))}}
517-
{{rpad .Name .NamePadding }} {{.Short}}{{end}}{{end}}{{end}}{{if .HasAvailableLocalFlags}}
552+
{{.Title}}{{range $cmds}}{{if (and (eq .GroupID $group.ID) (or .IsAvailableCommand (eq .Name "help")))}}
553+
{{rpad .Name .NamePadding }} {{.Short}}{{end}}{{end}}{{end}}{{if not .AllChildCommandsHaveGroup}}
554+
555+
Additional Commands:{{range $cmds}}{{if (and (eq .GroupID "") (or .IsAvailableCommand (eq .Name "help")))}}
556+
{{rpad .Name .NamePadding }} {{.Short}}{{end}}{{end}}{{end}}{{end}}{{end}}{{if .HasAvailableLocalFlags}}
518557
519558
Flags:
520559
{{.LocalFlags.FlagUsages | trimTrailingWhitespaces}}{{end}}{{if .HasAvailableInheritedFlags}}
@@ -1140,6 +1179,7 @@ Simply type ` + c.Name() + ` help [path to command] for full details.`,
11401179
CheckErr(cmd.Help())
11411180
}
11421181
},
1182+
GroupID: c.helpCommandGroupID,
11431183
}
11441184
}
11451185
c.RemoveCommand(c.helpCommand)
@@ -1178,6 +1218,10 @@ func (c *Command) AddCommand(cmds ...*Command) {
11781218
panic("Command can't be a child of itself")
11791219
}
11801220
cmds[i].parent = c
1221+
// if Group is not defined let the developer know right away
1222+
if x.GroupID != "" && !c.ContainsGroup(x.GroupID) {
1223+
panic(fmt.Sprintf("Group id '%s' is not defined for subcommand '%s'", x.GroupID, cmds[i].CommandPath()))
1224+
}
11811225
// update max lengths
11821226
usageLen := len(x.Use)
11831227
if usageLen > c.commandsMaxUseLen {
@@ -1200,6 +1244,36 @@ func (c *Command) AddCommand(cmds ...*Command) {
12001244
}
12011245
}
12021246

1247+
// Groups returns a slice of child command groups.
1248+
func (c *Command) Groups() []*Group {
1249+
return c.commandgroups
1250+
}
1251+
1252+
// AllChildCommandsHaveGroup returns if all subcommands are assigned to a group
1253+
func (c *Command) AllChildCommandsHaveGroup() bool {
1254+
for _, sub := range c.commands {
1255+
if (sub.IsAvailableCommand() || sub == c.helpCommand) && sub.GroupID == "" {
1256+
return false
1257+
}
1258+
}
1259+
return true
1260+
}
1261+
1262+
// ContainGroups return if groupID exists in the list of command groups.
1263+
func (c *Command) ContainsGroup(groupID string) bool {
1264+
for _, x := range c.commandgroups {
1265+
if x.ID == groupID {
1266+
return true
1267+
}
1268+
}
1269+
return false
1270+
}
1271+
1272+
// AddGroup adds one or more command groups to this parent command.
1273+
func (c *Command) AddGroup(groups ...*Group) {
1274+
c.commandgroups = append(c.commandgroups, groups...)
1275+
}
1276+
12031277
// RemoveCommand removes one or more commands from a parent command.
12041278
func (c *Command) RemoveCommand(cmds ...*Command) {
12051279
commands := []*Command{}

command_test.go

+95
Original file line numberDiff line numberDiff line change
@@ -1767,6 +1767,101 @@ func TestEnableCommandSortingIsDisabled(t *testing.T) {
17671767
EnableCommandSorting = defaultCommandSorting
17681768
}
17691769

1770+
func TestUsageWithGroup(t *testing.T) {
1771+
var rootCmd = &Command{Use: "root", Short: "test", Run: emptyRun}
1772+
rootCmd.CompletionOptions.DisableDefaultCmd = true
1773+
1774+
rootCmd.AddGroup(&Group{ID: "group1", Title: "group1"})
1775+
rootCmd.AddGroup(&Group{ID: "group2", Title: "group2"})
1776+
1777+
rootCmd.AddCommand(&Command{Use: "cmd1", GroupID: "group1", Run: emptyRun})
1778+
rootCmd.AddCommand(&Command{Use: "cmd2", GroupID: "group2", Run: emptyRun})
1779+
1780+
output, err := executeCommand(rootCmd, "--help")
1781+
if err != nil {
1782+
t.Errorf("Unexpected error: %v", err)
1783+
}
1784+
1785+
// help should be ungrouped here
1786+
checkStringContains(t, output, "\nAdditional Commands:\n help")
1787+
checkStringContains(t, output, "\ngroup1\n cmd1")
1788+
checkStringContains(t, output, "\ngroup2\n cmd2")
1789+
}
1790+
1791+
func TestUsageHelpGroup(t *testing.T) {
1792+
var rootCmd = &Command{Use: "root", Short: "test", Run: emptyRun}
1793+
rootCmd.CompletionOptions.DisableDefaultCmd = true
1794+
1795+
rootCmd.AddGroup(&Group{ID: "group", Title: "group"})
1796+
rootCmd.AddCommand(&Command{Use: "xxx", GroupID: "group", Run: emptyRun})
1797+
rootCmd.SetHelpCommandGroupID("group")
1798+
1799+
output, err := executeCommand(rootCmd, "--help")
1800+
if err != nil {
1801+
t.Errorf("Unexpected error: %v", err)
1802+
}
1803+
1804+
// now help should be grouped under "group"
1805+
checkStringOmits(t, output, "\nAdditional Commands:\n help")
1806+
checkStringContains(t, output, "\ngroup\n help")
1807+
}
1808+
1809+
func TestUsageCompletionGroup(t *testing.T) {
1810+
var rootCmd = &Command{Use: "root", Short: "test", Run: emptyRun}
1811+
1812+
rootCmd.AddGroup(&Group{ID: "group", Title: "group"})
1813+
rootCmd.AddGroup(&Group{ID: "help", Title: "help"})
1814+
1815+
rootCmd.AddCommand(&Command{Use: "xxx", GroupID: "group", Run: emptyRun})
1816+
rootCmd.SetHelpCommandGroupID("help")
1817+
rootCmd.SetCompletionCommandGroupID("group")
1818+
1819+
output, err := executeCommand(rootCmd, "--help")
1820+
if err != nil {
1821+
t.Errorf("Unexpected error: %v", err)
1822+
}
1823+
1824+
// now completion should be grouped under "group"
1825+
checkStringOmits(t, output, "\nAdditional Commands:\n completion")
1826+
checkStringContains(t, output, "\ngroup\n completion")
1827+
}
1828+
1829+
func TestUngroupedCommand(t *testing.T) {
1830+
var rootCmd = &Command{Use: "root", Short: "test", Run: emptyRun}
1831+
1832+
rootCmd.AddGroup(&Group{ID: "group", Title: "group"})
1833+
rootCmd.AddGroup(&Group{ID: "help", Title: "help"})
1834+
1835+
rootCmd.AddCommand(&Command{Use: "xxx", GroupID: "group", Run: emptyRun})
1836+
rootCmd.SetHelpCommandGroupID("help")
1837+
rootCmd.SetCompletionCommandGroupID("group")
1838+
1839+
// Add a command without a group
1840+
rootCmd.AddCommand(&Command{Use: "yyy", Run: emptyRun})
1841+
1842+
output, err := executeCommand(rootCmd, "--help")
1843+
if err != nil {
1844+
t.Errorf("Unexpected error: %v", err)
1845+
}
1846+
1847+
// The yyy command should be in the additional command "group"
1848+
checkStringContains(t, output, "\nAdditional Commands:\n yyy")
1849+
}
1850+
1851+
func TestAddGroup(t *testing.T) {
1852+
var rootCmd = &Command{Use: "root", Short: "test", Run: emptyRun}
1853+
1854+
rootCmd.AddGroup(&Group{ID: "group", Title: "Test group"})
1855+
rootCmd.AddCommand(&Command{Use: "cmd", GroupID: "group", Run: emptyRun})
1856+
1857+
output, err := executeCommand(rootCmd, "--help")
1858+
if err != nil {
1859+
t.Errorf("Unexpected error: %v", err)
1860+
}
1861+
1862+
checkStringContains(t, output, "\nTest group\n cmd")
1863+
}
1864+
17701865
func TestSetOutput(t *testing.T) {
17711866
c := &Command{}
17721867
c.SetOutput(nil)

completions.go

+1
Original file line numberDiff line numberDiff line change
@@ -673,6 +673,7 @@ See each sub-command's help for details on how to use the generated script.
673673
Args: NoArgs,
674674
ValidArgsFunction: NoFileCompletions,
675675
Hidden: c.CompletionOptions.HiddenDefaultCmd,
676+
GroupID: c.completionCommandGroupID,
676677
}
677678
c.AddCommand(completionCmd)
678679

user_guide.md

+7
Original file line numberDiff line numberDiff line change
@@ -490,6 +490,13 @@ command and flag definitions are needed.
490490
Help is just a command like any other. There is no special logic or behavior
491491
around it. In fact, you can provide your own if you want.
492492

493+
### Grouping commands in help
494+
495+
Cobra supports grouping of available commands. Groups must be explicitly defined by `AddGroup` and set by
496+
the `GroupId` element of a subcommand. The groups will appear in the same order as they are defined.
497+
If you use the generated `help` or `completion` commands, you can set the group ids by `SetHelpCommandGroupId`
498+
and `SetCompletionCommandGroupId`, respectively.
499+
493500
### Defining your own help
494501

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

0 commit comments

Comments
 (0)