mirror of
https://github.com/spf13/cobra
synced 2024-11-24 14:47:12 +00:00
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>
This commit is contained in:
parent
212ea40783
commit
2169adb574
5 changed files with 181 additions and 3 deletions
|
@ -23,6 +23,7 @@ Cobra provides:
|
||||||
* Global, local and cascading flags
|
* Global, local and cascading flags
|
||||||
* Intelligent suggestions (`app srver`... did you mean `app server`?)
|
* Intelligent suggestions (`app srver`... did you mean `app server`?)
|
||||||
* Automatic help generation for commands and flags
|
* Automatic help generation for commands and flags
|
||||||
|
* Grouping help for subcommands
|
||||||
* Automatic help flag recognition of `-h`, `--help`, etc.
|
* Automatic help flag recognition of `-h`, `--help`, etc.
|
||||||
* Automatically generated shell autocomplete for your application (bash, zsh, fish, powershell)
|
* Automatically generated shell autocomplete for your application (bash, zsh, fish, powershell)
|
||||||
* Automatically generated man pages for your application
|
* Automatically generated man pages for your application
|
||||||
|
|
80
command.go
80
command.go
|
@ -35,6 +35,12 @@ const FlagSetByCobraAnnotation = "cobra_annotation_flag_set_by_cobra"
|
||||||
// FParseErrWhitelist configures Flag parse errors to be ignored
|
// FParseErrWhitelist configures Flag parse errors to be ignored
|
||||||
type FParseErrWhitelist flag.ParseErrorsWhitelist
|
type FParseErrWhitelist flag.ParseErrorsWhitelist
|
||||||
|
|
||||||
|
// Structure to manage groups for commands
|
||||||
|
type Group struct {
|
||||||
|
ID string
|
||||||
|
Title string
|
||||||
|
}
|
||||||
|
|
||||||
// Command is just that, a command for your application.
|
// Command is just that, a command for your application.
|
||||||
// E.g. 'go run ...' - 'run' is the command. Cobra requires
|
// E.g. 'go run ...' - 'run' is the command. Cobra requires
|
||||||
// you to define the usage and description as part of your command
|
// you to define the usage and description as part of your command
|
||||||
|
@ -61,6 +67,9 @@ type Command struct {
|
||||||
// Short is the short description shown in the 'help' output.
|
// Short is the short description shown in the 'help' output.
|
||||||
Short string
|
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 is the long message shown in the 'help <this-command>' output.
|
||||||
Long string
|
Long string
|
||||||
|
|
||||||
|
@ -128,6 +137,9 @@ type Command struct {
|
||||||
// PersistentPostRunE: PersistentPostRun but returns an error.
|
// PersistentPostRunE: PersistentPostRun but returns an error.
|
||||||
PersistentPostRunE func(cmd *Command, args []string) error
|
PersistentPostRunE func(cmd *Command, args []string) error
|
||||||
|
|
||||||
|
// groups for subcommands
|
||||||
|
commandgroups []*Group
|
||||||
|
|
||||||
// args is actual args parsed from flags.
|
// args is actual args parsed from flags.
|
||||||
args []string
|
args []string
|
||||||
// flagErrorBuf contains all error messages from pflag.
|
// flagErrorBuf contains all error messages from pflag.
|
||||||
|
@ -160,6 +172,12 @@ type Command struct {
|
||||||
// helpCommand is command with usage 'help'. If it's not defined by user,
|
// helpCommand is command with usage 'help'. If it's not defined by user,
|
||||||
// cobra uses default help command.
|
// cobra uses default help command.
|
||||||
helpCommand *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 is the version template defined by user.
|
||||||
versionTemplate string
|
versionTemplate string
|
||||||
|
|
||||||
|
@ -303,6 +321,21 @@ func (c *Command) SetHelpCommand(cmd *Command) {
|
||||||
c.helpCommand = cmd
|
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.
|
// SetHelpTemplate sets help template to be used. Application can use it to set custom template.
|
||||||
func (c *Command) SetHelpTemplate(s string) {
|
func (c *Command) SetHelpTemplate(s string) {
|
||||||
c.helpTemplate = s
|
c.helpTemplate = s
|
||||||
|
@ -511,10 +544,16 @@ Aliases:
|
||||||
{{.NameAndAliases}}{{end}}{{if .HasExample}}
|
{{.NameAndAliases}}{{end}}{{if .HasExample}}
|
||||||
|
|
||||||
Examples:
|
Examples:
|
||||||
{{.Example}}{{end}}{{if .HasAvailableSubCommands}}
|
{{.Example}}{{end}}{{if .HasAvailableSubCommands}}{{$cmds := .Commands}}{{if eq (len .Groups) 0}}
|
||||||
|
|
||||||
Available Commands:{{range .Commands}}{{if (or .IsAvailableCommand (eq .Name "help"))}}
|
Available Commands:{{range $cmds}}{{if (or .IsAvailableCommand (eq .Name "help"))}}
|
||||||
{{rpad .Name .NamePadding }} {{.Short}}{{end}}{{end}}{{end}}{{if .HasAvailableLocalFlags}}
|
{{rpad .Name .NamePadding }} {{.Short}}{{end}}{{end}}{{else}}{{range $group := .Groups}}
|
||||||
|
|
||||||
|
{{.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:
|
Flags:
|
||||||
{{.LocalFlags.FlagUsages | trimTrailingWhitespaces}}{{end}}{{if .HasAvailableInheritedFlags}}
|
{{.LocalFlags.FlagUsages | trimTrailingWhitespaces}}{{end}}{{if .HasAvailableInheritedFlags}}
|
||||||
|
@ -1140,6 +1179,7 @@ Simply type ` + c.Name() + ` help [path to command] for full details.`,
|
||||||
CheckErr(cmd.Help())
|
CheckErr(cmd.Help())
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
GroupID: c.helpCommandGroupID,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
c.RemoveCommand(c.helpCommand)
|
c.RemoveCommand(c.helpCommand)
|
||||||
|
@ -1178,6 +1218,10 @@ func (c *Command) AddCommand(cmds ...*Command) {
|
||||||
panic("Command can't be a child of itself")
|
panic("Command can't be a child of itself")
|
||||||
}
|
}
|
||||||
cmds[i].parent = c
|
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()))
|
||||||
|
}
|
||||||
// update max lengths
|
// update max lengths
|
||||||
usageLen := len(x.Use)
|
usageLen := len(x.Use)
|
||||||
if usageLen > c.commandsMaxUseLen {
|
if usageLen > c.commandsMaxUseLen {
|
||||||
|
@ -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.
|
// RemoveCommand removes one or more commands from a parent command.
|
||||||
func (c *Command) RemoveCommand(cmds ...*Command) {
|
func (c *Command) RemoveCommand(cmds ...*Command) {
|
||||||
commands := []*Command{}
|
commands := []*Command{}
|
||||||
|
|
|
@ -1767,6 +1767,101 @@ func TestEnableCommandSortingIsDisabled(t *testing.T) {
|
||||||
EnableCommandSorting = defaultCommandSorting
|
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) {
|
func TestSetOutput(t *testing.T) {
|
||||||
c := &Command{}
|
c := &Command{}
|
||||||
c.SetOutput(nil)
|
c.SetOutput(nil)
|
||||||
|
|
|
@ -673,6 +673,7 @@ See each sub-command's help for details on how to use the generated script.
|
||||||
Args: NoArgs,
|
Args: NoArgs,
|
||||||
ValidArgsFunction: NoFileCompletions,
|
ValidArgsFunction: NoFileCompletions,
|
||||||
Hidden: c.CompletionOptions.HiddenDefaultCmd,
|
Hidden: c.CompletionOptions.HiddenDefaultCmd,
|
||||||
|
GroupID: c.completionCommandGroupID,
|
||||||
}
|
}
|
||||||
c.AddCommand(completionCmd)
|
c.AddCommand(completionCmd)
|
||||||
|
|
||||||
|
|
|
@ -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
|
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.
|
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
|
### Defining your own help
|
||||||
|
|
||||||
You can provide your own Help command or your own template for the default command to use
|
You can provide your own Help command or your own template for the default command to use
|
||||||
|
|
Loading…
Reference in a new issue