From 27c1d51d38d745f8cb2c0bca2f9ca39523e74a07 Mon Sep 17 00:00:00 2001 From: Alexander Weiss Date: Tue, 8 Feb 2022 22:32:03 +0000 Subject: [PATCH] Add groups for commands in help Adds the possibility to group the commands in the help message. Merge https://github.com/spf13/cobra/pull/1003 closes #836 closes #1271 --- command.go | 58 ++++++++++++++++++++++++++++++++++++++++++++++--- command_test.go | 47 +++++++++++++++++++++++++++++++++++++++ user_guide.md | 5 +++++ 3 files changed, 107 insertions(+), 3 deletions(-) diff --git a/command.go b/command.go index 7c0de0b..264dfe0 100644 --- a/command.go +++ b/command.go @@ -31,6 +31,12 @@ import ( // FParseErrWhitelist configures Flag parse errors to be ignored type FParseErrWhitelist flag.ParseErrorsWhitelist +// Structure to manage groups for commands +type Group struct { + Group string + Title string +} + // 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 @@ -57,6 +63,9 @@ type Command struct { // Short is the short description shown in the 'help' output. Short string + // The group under which the command is grouped in the 'help' output. + Group string + // Long is the long message shown in the 'help ' output. Long string @@ -124,6 +133,9 @@ type Command struct { // PersistentPostRunE: PersistentPostRun but returns an error. PersistentPostRunE func(cmd *Command, args []string) error + // groups for commands + commandgroups []*Group + // args is actual args parsed from flags. args []string // flagErrorBuf contains all error messages from pflag. @@ -156,6 +168,9 @@ type Command struct { // helpCommand is command with usage 'help'. If it's not defined by user, // cobra uses default help command. helpCommand *Command + // helpCommandGroup is the default group the helpCommand is in + helpCommandGroup string + // versionTemplate is the version template defined by user. versionTemplate string @@ -297,6 +312,15 @@ func (c *Command) SetHelpCommand(cmd *Command) { c.helpCommand = cmd } +// SetHelpCommandGroup sets the group of the help command. +func (c *Command) SetHelpCommandGroup(group string) { + if c.helpCommand != nil { + c.helpCommand.Group = group + } + // helpCommandGroup is used if no helpCommand is defined by the user + c.helpCommandGroup = group +} + // SetHelpTemplate sets help template to be used. Application can use it to set custom template. func (c *Command) SetHelpTemplate(s string) { c.helpTemplate = s @@ -510,10 +534,13 @@ Aliases: {{.NameAndAliases}}{{end}}{{if .HasExample}} Examples: -{{.Example}}{{end}}{{if .HasAvailableSubCommands}} +{{.Example}}{{end}}{{if .HasAvailableSubCommands}}{{$cmds := .Commands}} -Available Commands:{{range .Commands}}{{if (or .IsAvailableCommand (eq .Name "help"))}} - {{rpad .Name .NamePadding }} {{.Short}}{{end}}{{end}}{{end}}{{if .HasAvailableLocalFlags}} +Available Commands:{{range $cmds}}{{if (and (eq .Group "") (or .IsAvailableCommand (eq .Name "help")))}} + {{rpad .Name .NamePadding }} {{.Short}}{{end}}{{end}}{{range $group := .Groups}} + +{{.Title}}{{range $cmds}}{{if (and (eq .Group $group.Group) (or .IsAvailableCommand (eq .Name "help")))}} + {{rpad .Name .NamePadding }} {{.Short}}{{end}}{{end}}{{end}}{{end}}{{if .HasAvailableLocalFlags}} Flags: {{.LocalFlags.FlagUsages | trimTrailingWhitespaces}}{{end}}{{if .HasAvailableInheritedFlags}} @@ -1137,6 +1164,7 @@ Simply type ` + c.Name() + ` help [path to command] for full details.`, CheckErr(cmd.Help()) } }, + Group: c.helpCommandGroup, } } c.RemoveCommand(c.helpCommand) @@ -1175,6 +1203,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 generate a new one with same title + if x.Group != "" && !c.ContainsGroup(x.Group) { + c.AddGroup(&Group{Group: x.Group, Title: x.Group}) + } // update max lengths usageLen := len(x.Use) if usageLen > c.commandsMaxUseLen { @@ -1197,6 +1229,26 @@ func (c *Command) AddCommand(cmds ...*Command) { } } +// Groups returns a slice of child command groups. +func (c *Command) Groups() []*Group { + return c.commandgroups +} + +// ContainGroups return if group is in command groups. +func (c *Command) ContainsGroup(group string) bool { + for _, x := range c.commandgroups { + if x.Group == group { + 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{} diff --git a/command_test.go b/command_test.go index 52b5724..9dbc39f 100644 --- a/command_test.go +++ b/command_test.go @@ -1639,6 +1639,53 @@ func TestEnableCommandSortingIsDisabled(t *testing.T) { EnableCommandSorting = true } +func TestUsageWithGroup(t *testing.T) { + var rootCmd = &Command{Use: "root", Short: "test", Run: emptyRun} + + rootCmd.AddCommand(&Command{Use: "cmd1", Group: "group1", Run: emptyRun}) + rootCmd.AddCommand(&Command{Use: "cmd2", Group: "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, "\nAvailable 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.AddCommand(&Command{Use: "xxx", Group: "group", Run: emptyRun}) + rootCmd.SetHelpCommandGroup("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, "\nAvailable Commands:\n help") + checkStringContains(t, output, "\nAvailable Commands:\n\ngroup\n help") +} + +func TestAddGroup(t *testing.T) { + var rootCmd = &Command{Use: "root", Short: "test", Run: emptyRun} + + rootCmd.AddGroup(&Group{Group: "group", Title: "Test group"}) + rootCmd.AddCommand(&Command{Use: "cmd", Group: "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) diff --git a/user_guide.md b/user_guide.md index 9d28f2c..d76c294 100644 --- a/user_guide.md +++ b/user_guide.md @@ -455,6 +455,11 @@ 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 can either be explicitly defined by `AddGroup` and set by +the `Group` element of a subcommand. If Groups are not explicitly defined they are implicitly defined. + ### Defining your own help You can provide your own Help command or your own template for the default command to use