From a0801792cc2dc77e913bd8405d5ef0e2f5ba32e8 Mon Sep 17 00:00:00 2001 From: Soulou Date: Mon, 15 Dec 2014 23:35:49 +0100 Subject: [PATCH 1/5] Allow to sort commands by category --- app.go | 11 +++++++++++ command.go | 6 +++++- help.go | 12 +++++++++--- 3 files changed, 25 insertions(+), 4 deletions(-) diff --git a/app.go b/app.go index 1ea3fd0..fdb2ba5 100644 --- a/app.go +++ b/app.go @@ -34,6 +34,10 @@ type App struct { HideHelp bool // Boolean to hide built-in version flag HideVersion bool + // Display commands by category + CategorizedHelp bool + // Populate when displaying AppHelp + Categories map[string]Commands // 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 @@ -95,6 +99,13 @@ func (a *App) Run(arguments []string) (err error) { a.Authors = append(a.Authors, Author{Name: a.Author, Email: a.Email}) } + if a.CategorizedHelp { + a.Categories = make(map[string]Commands) + for _, command := range a.Commands { + a.Categories[command.Category] = append(a.Categories[command.Category], command) + } + } + newCmds := []Command{} for _, c := range a.Commands { if c.HelpName == "" { diff --git a/command.go b/command.go index 0153713..024ddbc 100644 --- a/command.go +++ b/command.go @@ -22,6 +22,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 +39,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 +61,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 { diff --git a/help.go b/help.go index 15916f8..8285ff0 100644 --- a/help.go +++ b/help.go @@ -23,9 +23,12 @@ VERSION: AUTHOR(S): {{range .Authors}}{{ . }}{{end}} {{end}}{{if .Commands}} -COMMANDS: - {{range .Commands}}{{join .Names ", "}}{{ "\t" }}{{.Usage}} - {{end}}{{end}}{{if .Flags}} +COMMANDS:{{if .CategorizedHelp}}{{range $category, $commands := .Categories}}{{if $category}} + {{$category}}{{ ":" }}{{end}}{{range $commands}} + {{.Name}}{{with .ShortName}}, {{.}}{{end}}{{ "\t" }}{{.Usage}}{{end}} +{{end}}{{else}}{{range .Commands}} + {{.Name}}{{with .ShortName}}, {{.}}{{end}}{{ "\t" }}{{.Usage}}{{end}} +{{end}}{{end}}{{if .Flags}} GLOBAL OPTIONS: {{range .Flags}}{{.}} {{end}}{{end}}{{if .Copyright }} @@ -43,6 +46,9 @@ var CommandHelpTemplate = `NAME: USAGE: {{.HelpName}}{{if .Flags}} [command options]{{end}} {{if .ArgsUsage}}{{.ArgsUsage}}{{else}}[arguments...]{{end}}{{if .Description}} +{{if .Category}}CATEGORY: + {{.Category}} +{{end}} DESCRIPTION: {{.Description}}{{end}}{{if .Flags}} From 994a7028e275fefa0b3bbd88c4c48c0942c9be3a Mon Sep 17 00:00:00 2001 From: Soulou Date: Fri, 21 Aug 2015 10:58:14 +0200 Subject: [PATCH 2/5] Categories as slice, not a map anymore, order is always preserved --- app.go | 8 +++++--- category.go | 30 ++++++++++++++++++++++++++++++ help.go | 4 ++-- 3 files changed, 37 insertions(+), 5 deletions(-) create mode 100644 category.go diff --git a/app.go b/app.go index fdb2ba5..5ebf201 100644 --- a/app.go +++ b/app.go @@ -6,6 +6,7 @@ import ( "io/ioutil" "os" "path" + "sort" "time" ) @@ -37,7 +38,7 @@ type App struct { // Display commands by category CategorizedHelp bool // Populate when displaying AppHelp - Categories map[string]Commands + 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 @@ -100,11 +101,12 @@ func (a *App) Run(arguments []string) (err error) { } if a.CategorizedHelp { - a.Categories = make(map[string]Commands) + a.Categories = CommandCategories{} for _, command := range a.Commands { - a.Categories[command.Category] = append(a.Categories[command.Category], command) + a.Categories = a.Categories.AddCommand(command.Category, command) } } + sort.Sort(a.Categories) newCmds := []Command{} for _, c := range a.Commands { 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/help.go b/help.go index 8285ff0..7838c89 100644 --- a/help.go +++ b/help.go @@ -23,8 +23,8 @@ VERSION: AUTHOR(S): {{range .Authors}}{{ . }}{{end}} {{end}}{{if .Commands}} -COMMANDS:{{if .CategorizedHelp}}{{range $category, $commands := .Categories}}{{if $category}} - {{$category}}{{ ":" }}{{end}}{{range $commands}} +COMMANDS:{{if .CategorizedHelp}}{{range .Categories}}{{if .Name}} + {{.Name}}{{ ":" }}{{end}}{{range .Commands}} {{.Name}}{{with .ShortName}}, {{.}}{{end}}{{ "\t" }}{{.Usage}}{{end}} {{end}}{{else}}{{range .Commands}} {{.Name}}{{with .ShortName}}, {{.}}{{end}}{{ "\t" }}{{.Usage}}{{end}} From d0997e8f99b5a3006872e0294ef7434a0e01c93a Mon Sep 17 00:00:00 2001 From: Soulou Date: Fri, 21 Aug 2015 13:25:37 +0200 Subject: [PATCH 3/5] Set Categories as a read-only method and fix tests --- app.go | 15 ++++++++++----- app_test.go | 44 ++++++++++++++++++++++++++++++++++++++++++++ help.go | 3 +-- 3 files changed, 55 insertions(+), 7 deletions(-) diff --git a/app.go b/app.go index 5ebf201..4bc333b 100644 --- a/app.go +++ b/app.go @@ -37,8 +37,8 @@ type App struct { HideVersion bool // Display commands by category CategorizedHelp bool - // Populate when displaying AppHelp - Categories CommandCategories + // 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 @@ -101,12 +101,12 @@ func (a *App) Run(arguments []string) (err error) { } if a.CategorizedHelp { - a.Categories = CommandCategories{} + a.categories = CommandCategories{} for _, command := range a.Commands { - a.Categories = a.Categories.AddCommand(command.Category, command) + a.categories = a.categories.AddCommand(command.Category, command) } } - sort.Sort(a.Categories) + sort.Sort(a.categories) newCmds := []Command{} for _, c := range a.Commands { @@ -329,6 +329,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..95bdd41 100644 --- a/app_test.go +++ b/app_test.go @@ -8,6 +8,7 @@ import ( "io" "io/ioutil" "os" + "reflect" "strings" "testing" ) @@ -939,6 +940,49 @@ func TestApp_Run_Version(t *testing.T) { } } +func TestApp_Run_Categories(t *testing.T) { + app := NewApp() + app.Name = "categories" + app.CategorizedHelp = true + 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) + } +} + func TestApp_Run_DoesNotOverwriteErrorFromBefore(t *testing.T) { app := NewApp() app.Action = func(c *Context) {} diff --git a/help.go b/help.go index 7838c89..6708192 100644 --- a/help.go +++ b/help.go @@ -48,8 +48,7 @@ USAGE: {{if .Category}}CATEGORY: {{.Category}} -{{end}} -DESCRIPTION: +{{end}}DESCRIPTION: {{.Description}}{{end}}{{if .Flags}} OPTIONS: From 042842b81998322ad2b94fe6a15105825036898e Mon Sep 17 00:00:00 2001 From: Jesse Szwedko Date: Sun, 20 Mar 2016 12:17:13 -0700 Subject: [PATCH 4/5] Remove CategorizedHelp from App and allow subcommands to have categories Just place all subcommands in categories, the default category will be "" which will properly format the output (and group commands that have no category). Also allow subcommands to have categories. Lastly, augment the test to check the output. --- app.go | 16 ++++++---------- app_test.go | 8 +++++++- command.go | 8 ++++++++ help.go | 20 ++++++++++---------- 4 files changed, 31 insertions(+), 21 deletions(-) diff --git a/app.go b/app.go index 4bc333b..dcab379 100644 --- a/app.go +++ b/app.go @@ -35,8 +35,6 @@ type App struct { HideHelp bool // Boolean to hide built-in version flag HideVersion bool - // Display commands by category - CategorizedHelp bool // Populate on app startup, only gettable throught method Categories() categories CommandCategories // An action to execute when the bash-completion flag is set @@ -100,14 +98,6 @@ func (a *App) Run(arguments []string) (err error) { a.Authors = append(a.Authors, Author{Name: a.Author, Email: a.Email}) } - if a.CategorizedHelp { - a.categories = CommandCategories{} - for _, command := range a.Commands { - a.categories = a.categories.AddCommand(command.Category, command) - } - } - sort.Sort(a.categories) - newCmds := []Command{} for _, c := range a.Commands { if c.HelpName == "" { @@ -117,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) diff --git a/app_test.go b/app_test.go index 95bdd41..ebf26c7 100644 --- a/app_test.go +++ b/app_test.go @@ -943,7 +943,6 @@ func TestApp_Run_Version(t *testing.T) { func TestApp_Run_Categories(t *testing.T) { app := NewApp() app.Name = "categories" - app.CategorizedHelp = true app.Commands = []Command{ Command{ Name: "command1", @@ -981,6 +980,13 @@ func TestApp_Run_Categories(t *testing.T) { 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) { diff --git a/command.go b/command.go index 024ddbc..1a05b54 100644 --- a/command.go +++ b/command.go @@ -3,6 +3,7 @@ package cli import ( "fmt" "io/ioutil" + "sort" "strings" ) @@ -231,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 6708192..6efb011 100644 --- a/help.go +++ b/help.go @@ -23,11 +23,9 @@ VERSION: AUTHOR(S): {{range .Authors}}{{ . }}{{end}} {{end}}{{if .Commands}} -COMMANDS:{{if .CategorizedHelp}}{{range .Categories}}{{if .Name}} +COMMANDS:{{range .Categories}}{{if .Name}} {{.Name}}{{ ":" }}{{end}}{{range .Commands}} {{.Name}}{{with .ShortName}}, {{.}}{{end}}{{ "\t" }}{{.Usage}}{{end}} -{{end}}{{else}}{{range .Commands}} - {{.Name}}{{with .ShortName}}, {{.}}{{end}}{{ "\t" }}{{.Usage}}{{end}} {{end}}{{end}}{{if .Flags}} GLOBAL OPTIONS: {{range .Flags}}{{.}} @@ -44,11 +42,12 @@ 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}} -{{if .Category}}CATEGORY: - {{.Category}} -{{end}}DESCRIPTION: +CATEGORY: + {{.Category}}{{end}}{{if .Description}} + +DESCRIPTION: {{.Description}}{{end}}{{if .Flags}} OPTIONS: @@ -65,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}} From a7be4a3f196ec5ea6f6b5cf0e09bb1ee71776cee Mon Sep 17 00:00:00 2001 From: Jesse Szwedko Date: Sun, 20 Mar 2016 12:18:28 -0700 Subject: [PATCH 5/5] Describe category behavior in README --- README.md | 39 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/README.md b/README.md index 364c964..89de990 100644 --- a/README.md +++ b/README.md @@ -328,6 +328,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`