diff --git a/README.md b/README.md index bb769fe..d9371cf 100644 --- a/README.md +++ b/README.md @@ -329,6 +329,45 @@ app.Commands = []cli.Command{ ... ``` +### Subcommands categories + +For additional organization in apps that have many subcommands, you can +associate a category for each command to group them together in the help +output. + +E.g. + +```go +... + app.Commands = []cli.Command{ + { + Name: "noop", + }, + { + Name: "add", + Category: "template", + }, + { + Name: "remove", + Category: "template", + }, + } +... +``` + +Will include: + +``` +... +COMMANDS: + noop + + Template actions: + add + remove +... +``` + ### Bash Completion You can enable completion commands by setting the `EnableBashCompletion` diff --git a/app.go b/app.go index 6632ec0..b4dd201 100644 --- a/app.go +++ b/app.go @@ -6,6 +6,7 @@ import ( "io/ioutil" "os" "path" + "sort" "time" ) @@ -34,6 +35,8 @@ type App struct { HideHelp bool // Boolean to hide built-in version flag and the VERSION section of help HideVersion bool + // Populate on app startup, only gettable throught method Categories() + categories CommandCategories // An action to execute when the bash-completion flag is set BashComplete func(context *Context) // An action to execute before any subcommands are run, but after the context is ready @@ -104,6 +107,12 @@ func (a *App) Run(arguments []string) (err error) { } a.Commands = newCmds + a.categories = CommandCategories{} + for _, command := range a.Commands { + a.categories = a.categories.AddCommand(command.Category, command) + } + sort.Sort(a.categories) + // append help to commands if a.Command(helpCommand.Name) == nil && !a.HideHelp { a.Commands = append(a.Commands, helpCommand) @@ -316,6 +325,11 @@ func (a *App) Command(name string) *Command { return nil } +// Returnes the array containing all the categories with the commands they contain +func (a *App) Categories() CommandCategories { + return a.categories +} + func (a *App) hasFlag(flag Flag) bool { for _, f := range a.Flags { if flag == f { diff --git a/app_test.go b/app_test.go index 7feaf1f..ebf26c7 100644 --- a/app_test.go +++ b/app_test.go @@ -8,6 +8,7 @@ import ( "io" "io/ioutil" "os" + "reflect" "strings" "testing" ) @@ -939,6 +940,55 @@ func TestApp_Run_Version(t *testing.T) { } } +func TestApp_Run_Categories(t *testing.T) { + app := NewApp() + app.Name = "categories" + app.Commands = []Command{ + Command{ + Name: "command1", + Category: "1", + }, + Command{ + Name: "command2", + Category: "1", + }, + Command{ + Name: "command3", + Category: "2", + }, + } + buf := new(bytes.Buffer) + app.Writer = buf + + app.Run([]string{"categories"}) + + expect := CommandCategories{ + &CommandCategory{ + Name: "1", + Commands: []Command{ + app.Commands[0], + app.Commands[1], + }, + }, + &CommandCategory{ + Name: "2", + Commands: []Command{ + app.Commands[2], + }, + }, + } + if !reflect.DeepEqual(app.Categories(), expect) { + t.Fatalf("expected categories %#v, to equal %#v", app.Categories(), expect) + } + + output := buf.String() + t.Logf("output: %q\n", buf.Bytes()) + + if !strings.Contains(output, "1:\n command1") { + t.Errorf("want buffer to include category %q, did not: \n%q", "1:\n command1", output) + } +} + func TestApp_Run_DoesNotOverwriteErrorFromBefore(t *testing.T) { app := NewApp() app.Action = func(c *Context) {} diff --git a/category.go b/category.go new file mode 100644 index 0000000..7dbf218 --- /dev/null +++ b/category.go @@ -0,0 +1,30 @@ +package cli + +type CommandCategories []*CommandCategory + +type CommandCategory struct { + Name string + Commands Commands +} + +func (c CommandCategories) Less(i, j int) bool { + return c[i].Name < c[j].Name +} + +func (c CommandCategories) Len() int { + return len(c) +} + +func (c CommandCategories) Swap(i, j int) { + c[i], c[j] = c[j], c[i] +} + +func (c CommandCategories) AddCommand(category string, command Command) CommandCategories { + for _, commandCategory := range c { + if commandCategory.Name == category { + commandCategory.Commands = append(commandCategory.Commands, command) + return c + } + } + return append(c, &CommandCategory{Name: category, Commands: []Command{command}}) +} diff --git a/command.go b/command.go index 0153713..1a05b54 100644 --- a/command.go +++ b/command.go @@ -3,6 +3,7 @@ package cli import ( "fmt" "io/ioutil" + "sort" "strings" ) @@ -22,6 +23,8 @@ type Command struct { Description string // A short description of the arguments of this command ArgsUsage string + // The category the command is part of + Category string // The function to call when checking for bash command completions BashComplete func(context *Context) // An action to execute before any sub-subcommands are run, but after the context is ready @@ -37,7 +40,7 @@ type Command struct { // If this function is not set, the "Incorrect usage" is displayed and the execution is interrupted. OnUsageError func(context *Context, err error) error // List of child commands - Subcommands []Command + Subcommands Commands // List of flags to parse Flags []Flag // Treat all flags as normal arguments if true @@ -59,6 +62,8 @@ func (c Command) FullName() string { return strings.Join(c.commandNamePath, " ") } +type Commands []Command + // Invokes the command given the context, parses ctx.Args() to generate command-specific flags func (c Command) Run(ctx *Context) (err error) { if len(c.Subcommands) > 0 { @@ -227,6 +232,13 @@ func (c Command) startApp(ctx *Context) error { app.Email = ctx.App.Email app.Writer = ctx.App.Writer + app.categories = CommandCategories{} + for _, command := range c.Subcommands { + app.categories = app.categories.AddCommand(command.Category, command) + } + + sort.Sort(app.categories) + // bash completion app.EnableBashCompletion = ctx.App.EnableBashCompletion if c.BashComplete != nil { diff --git a/help.go b/help.go index d3a12a2..adf157d 100644 --- a/help.go +++ b/help.go @@ -23,9 +23,10 @@ VERSION: AUTHOR(S): {{range .Authors}}{{ . }}{{end}} {{end}}{{if .Commands}} -COMMANDS: - {{range .Commands}}{{join .Names ", "}}{{ "\t" }}{{.Usage}} - {{end}}{{end}}{{if .Flags}} +COMMANDS:{{range .Categories}}{{if .Name}} + {{.Name}}{{ ":" }}{{end}}{{range .Commands}} + {{.Name}}{{with .ShortName}}, {{.}}{{end}}{{ "\t" }}{{.Usage}}{{end}} +{{end}}{{end}}{{if .Flags}} GLOBAL OPTIONS: {{range .Flags}}{{.}} {{end}}{{end}}{{if .Copyright }} @@ -41,7 +42,10 @@ var CommandHelpTemplate = `NAME: {{.HelpName}} - {{.Usage}} USAGE: - {{.HelpName}}{{if .Flags}} [command options]{{end}} {{if .ArgsUsage}}{{.ArgsUsage}}{{else}}[arguments...]{{end}}{{if .Description}} + {{.HelpName}}{{if .Flags}} [command options]{{end}} {{if .ArgsUsage}}{{.ArgsUsage}}{{else}}[arguments...]{{end}}{{if .Category}} + +CATEGORY: + {{.Category}}{{end}}{{if .Description}} DESCRIPTION: {{.Description}}{{end}}{{if .Flags}} @@ -60,9 +64,10 @@ var SubcommandHelpTemplate = `NAME: USAGE: {{.HelpName}} command{{if .Flags}} [command options]{{end}} {{if .ArgsUsage}}{{.ArgsUsage}}{{else}}[arguments...]{{end}} -COMMANDS: - {{range .Commands}}{{join .Names ", "}}{{ "\t" }}{{.Usage}} - {{end}}{{if .Flags}} +COMMANDS:{{range .Categories}}{{if .Name}} + {{.Name}}{{ ":" }}{{end}}{{range .Commands}} + {{.Name}}{{with .ShortName}}, {{.}}{{end}}{{ "\t" }}{{.Usage}}{{end}} +{{end}}{{if .Flags}} OPTIONS: {{range .Flags}}{{.}} {{end}}{{end}}