From 13f0c8c0f68ee8e25a9bb44dfdad819e1a7e5e2e Mon Sep 17 00:00:00 2001 From: Summer Mousa Date: Wed, 16 Apr 2014 11:18:00 -0500 Subject: [PATCH 1/6] More consistent implementation of recursive subcommands --- app.go | 56 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ command.go | 39 ++++++++++++++++++++++++++++++++++++- help.go | 46 ++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 140 insertions(+), 1 deletion(-) diff --git a/app.go b/app.go index a7c7122..f9342d7 100644 --- a/app.go +++ b/app.go @@ -129,6 +129,62 @@ func (a *App) Run(arguments []string) error { return nil } +// Invokes the subcommand given the context, parses ctx.Args() to generate command-specific flags +func (a *App) RunAsSubcommand(c *Context) error { + // append help to commands + if a.Command(helpCommand.Name) == nil { + a.Commands = append(a.Commands, helpCommand) + } + + // append help flags + if a.EnableBashCompletion { + a.appendFlag(BashCompletionFlag) + } + a.appendFlag(BoolFlag{"help, h", "show help"}) + + // parse flags + set := flagSet(a.Name, a.Flags) + set.SetOutput(ioutil.Discard) + err := set.Parse(c.Args().Tail()) + nerr := normalizeFlags(a.Flags, set) + context := NewContext(a, set, set) + + if nerr != nil { + fmt.Println(nerr) + ShowSubcommandHelp(context) + fmt.Println("") + return nerr + } + + if err != nil { + fmt.Printf("Incorrect Usage.\n\n") + ShowSubcommandHelp(context) + fmt.Println("") + return err + } + + if checkCompletions(context) { + return nil + } + + if checkSubcommandHelp(context) { + return nil + } + + args := context.Args() + if args.Present() { + name := args.First() + c := a.Command(name) + if c != nil { + return c.Run(context) + } + } + + // Run default Action + a.Action(context) + return nil +} + // Returns the named command on App. Returns nil if the command does not exist func (a *App) Command(name string) *Command { for _, c := range a.Commands { diff --git a/command.go b/command.go index 60d29aa..2d5c662 100644 --- a/command.go +++ b/command.go @@ -14,7 +14,7 @@ type Command struct { ShortName string // A short description of the usage of this command Usage string - // A longer explaination of how the command works + // A longer explanation of how the command works Description string // The function to call when checking for bash command completions BashComplete func(context *Context) @@ -22,10 +22,16 @@ type Command struct { Action func(context *Context) // List of flags to parse Flags []Flag + // List of child commands + Subcommands []Command } // Invokes the command given the context, parses ctx.Args() to generate command-specific flags func (c Command) Run(ctx *Context) error { + if len(c.Subcommands) > 0 { + return c.startApp(ctx) + } + // append help to flags c.Flags = append( c.Flags, @@ -89,3 +95,34 @@ func (c Command) Run(ctx *Context) error { func (c Command) HasName(name string) bool { return c.Name == name || c.ShortName == name } + +func (c Command) startApp(ctx *Context) error { + app := NewApp() + + // set the name and usage + app.Name = fmt.Sprintf("%s %s", ctx.App.Name, c.Name) + if c.Description != "" { + app.Usage = c.Description + } else { + app.Usage = c.Usage + } + + // set the flags and commands + app.Commands = c.Subcommands + app.Flags = c.Flags + + // bash completion + app.EnableBashCompletion = ctx.App.EnableBashCompletion + if c.BashComplete != nil { + app.BashComplete = c.BashComplete + } + + // set the action + if c.Action != nil { + app.Action = c.Action + } else { + app.Action = helpSubcommand.Action + } + + return app.RunAsSubcommand(ctx) +} diff --git a/help.go b/help.go index 64b4bab..db9ff02 100644 --- a/help.go +++ b/help.go @@ -44,6 +44,24 @@ OPTIONS: {{end}} ` +// The text template for the subcommand help topic. +// cli.go uses text/template to render templates. You can +// render custom help text by setting this variable. +var SubcommandHelpTemplate = `NAME: + {{.Name}} - {{.Usage}} + +USAGE: + {{.Name}} [global options] command [command options] [arguments...] + +COMMANDS: + {{range .Commands}}{{.Name}}{{with .ShortName}}, {{.}}{{end}}{{ "\t" }}{{.Usage}} + {{end}} + +OPTIONS: + {{range .Flags}}{{.}} + {{end}} +` + var helpCommand = Command{ Name: "help", ShortName: "h", @@ -58,6 +76,20 @@ var helpCommand = Command{ }, } +var helpSubcommand = Command{ + Name: "help", + ShortName: "h", + Usage: "Shows a list of commands or help for one command", + Action: func(c *Context) { + args := c.Args() + if args.Present() { + ShowCommandHelp(c, args.First()) + } else { + ShowSubcommandHelp(c) + } + }, +} + // Prints help for the App var HelpPrinter = printHelp @@ -87,6 +119,11 @@ func ShowCommandHelp(c *Context, command string) { fmt.Printf("No help topic for '%v'\n", command) } +// Prints help for the given subcommand +func ShowSubcommandHelp(c *Context) { + HelpPrinter(SubcommandHelpTemplate, c.App) +} + // Prints the version number of the App func ShowVersion(c *Context) { fmt.Printf("%v version %v\n", c.App.Name, c.App.Version) @@ -145,6 +182,15 @@ func checkCommandHelp(c *Context, name string) bool { return false } +func checkSubcommandHelp(c *Context) bool { + if c.GlobalBool("h") || c.GlobalBool("help") { + ShowSubcommandHelp(c) + return true + } + + return false +} + func checkCompletions(c *Context) bool { if c.GlobalBool(BashCompletionFlag.Name) && c.App.EnableBashCompletion { ShowCompletions(c) From 705994c2c612399e782968f967824f13a2701490 Mon Sep 17 00:00:00 2001 From: Summer Mousa Date: Wed, 16 Apr 2014 12:59:34 -0500 Subject: [PATCH 2/6] Added unit tests --- app_test.go | 33 ++++++++++++++++++++++++++++++++ cli_test.go | 55 +++++++++++++++++++++++++++++++++++++++++++++++++++++ help.go | 1 - 3 files changed, 88 insertions(+), 1 deletion(-) diff --git a/app_test.go b/app_test.go index 49c525c..e5f8b83 100644 --- a/app_test.go +++ b/app_test.go @@ -24,6 +24,39 @@ func ExampleApp() { // Hello Jeremy } +func ExampleAppSubcommand() { + // set args for examples sake + os.Args = []string{"say", "hi", "english", "--name", "Jeremy"} + app := cli.NewApp() + app.Name = "say" + app.Commands = []cli.Command{ + { + Name: "hello", + ShortName: "hi", + Usage: "use it to see a description", + Description: "This is how we describe hello the function", + Subcommands: []cli.Command{ + { + Name: "english", + ShortName: "en", + Usage: "sends a greeting in english", + Description: "greets someone in english", + Flags: []cli.Flag{ + cli.StringFlag{"name", "Bob", "Name of the person to greet"}, + }, + Action: func(c *cli.Context) { + fmt.Println("Hello,", c.String("name")) + }, + }, + }, + }, + } + + app.Run(os.Args) + // Output: + // Hello, Jeremy +} + func ExampleAppHelp() { // set args for examples sake os.Args = []string{"greet", "h", "describeit"} diff --git a/cli_test.go b/cli_test.go index 772e90f..30f3c13 100644 --- a/cli_test.go +++ b/cli_test.go @@ -30,3 +30,58 @@ func Example() { app.Run(os.Args) } + +func ExampleSubcommand() { + app := cli.NewApp() + app.Name = "say" + app.Commands = []cli.Command{ + { + Name: "hello", + ShortName: "hi", + Usage: "use it to see a description", + Description: "This is how we describe hello the function", + Subcommands: []cli.Command{ + { + Name: "english", + ShortName: "en", + Usage: "sends a greeting in english", + Description: "greets someone in english", + Flags: []cli.Flag{ + cli.StringFlag{"name", "Bob", "Name of the person to greet"}, + }, + Action: func(c *cli.Context) { + println("Hello, ", c.String("name")) + }, + }, { + Name: "spanish", + ShortName: "sp", + Usage: "sends a greeting in spanish", + Flags: []cli.Flag{ + cli.StringFlag{"surname", "Jones", "Surname of the person to greet"}, + }, + Action: func(c *cli.Context) { + println("Hola, ", c.String("surname")) + }, + }, { + Name: "french", + ShortName: "fr", + Usage: "sends a greeting in french", + Flags: []cli.Flag{ + cli.StringFlag{"nickname", "Stevie", "Nickname of the person to greet"}, + }, + Action: func(c *cli.Context) { + println("Bonjour, ", c.String("nickname")) + }, + }, + }, + }, { + Name: "bye", + Usage: "says goodbye", + Action: func(c *cli.Context) { + println("bye") + }, + }, + } + + app.Run(os.Args) +} diff --git a/help.go b/help.go index db9ff02..0b81894 100644 --- a/help.go +++ b/help.go @@ -56,7 +56,6 @@ USAGE: COMMANDS: {{range .Commands}}{{.Name}}{{with .ShortName}}, {{.}}{{end}}{{ "\t" }}{{.Usage}} {{end}} - OPTIONS: {{range .Flags}}{{.}} {{end}} From 4f92d19cbb872d817966f0055db3b6b6b35f58c2 Mon Sep 17 00:00:00 2001 From: Summer Mousa Date: Wed, 16 Apr 2014 13:09:14 -0500 Subject: [PATCH 3/6] Updated readme --- README.md | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/README.md b/README.md index 0c12ce2..373d66d 100644 --- a/README.md +++ b/README.md @@ -187,6 +187,27 @@ app.Commands = []cli.Command{ println("completed task: ", c.Args().First()) }, }, + { + Name: "template", + ShortName: "r", + Usage: "options for task templates", + Subcommands: []cli.Command{ + { + Name: "add", + Usage: "add a new template", + Action: func(c *cli.Context) { + println("new task template: ", c.Args().First()) + }, + }, + { + Name: "remove", + Usage: "remove an existing template", + Action: func(c *cli.Context) { + println("removed task template: ", c.Args().First()) + }, + }, + }, + }, } ... ``` From 1a63283d443a9223a52777e55606f33e35a0838e Mon Sep 17 00:00:00 2001 From: Summer Mousa Date: Wed, 16 Apr 2014 13:10:13 -0500 Subject: [PATCH 4/6] dolling it up --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 373d66d..4621310 100644 --- a/README.md +++ b/README.md @@ -190,7 +190,7 @@ app.Commands = []cli.Command{ { Name: "template", ShortName: "r", - Usage: "options for task templates", + Usage: "options for task templates", Subcommands: []cli.Command{ { Name: "add", From faf2a3d4a3371d302c16b4a751571d95ea732679 Mon Sep 17 00:00:00 2001 From: Summer Mousa Date: Wed, 16 Apr 2014 15:26:28 -0500 Subject: [PATCH 5/6] Added Before method to command. If set, or if command.Subcommands is set, then the command is treated as a recursive subcommand --- app.go | 7 +++++++ command.go | 12 ++++++++---- 2 files changed, 15 insertions(+), 4 deletions(-) diff --git a/app.go b/app.go index f9342d7..a4a76ef 100644 --- a/app.go +++ b/app.go @@ -171,6 +171,13 @@ func (a *App) RunAsSubcommand(c *Context) error { return nil } + if a.Before != nil { + err := a.Before(context) + if err != nil { + return err + } + } + args := context.Args() if args.Present() { name := args.First() diff --git a/command.go b/command.go index 2d5c662..735505b 100644 --- a/command.go +++ b/command.go @@ -18,17 +18,20 @@ type Command struct { Description 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 + // If a non-nil error is returned, no sub-subcommands are run + Before func(context *Context) error // The function to call when this command is invoked Action func(context *Context) - // List of flags to parse - Flags []Flag // List of child commands Subcommands []Command + // List of flags to parse + Flags []Flag } // Invokes the command given the context, parses ctx.Args() to generate command-specific flags func (c Command) Run(ctx *Context) error { - if len(c.Subcommands) > 0 { + if (c.Subcommands != nil && len(c.Subcommands) > 0) || c.Before != nil { return c.startApp(ctx) } @@ -117,7 +120,8 @@ func (c Command) startApp(ctx *Context) error { app.BashComplete = c.BashComplete } - // set the action + // set the actions + app.Before = c.Before if c.Action != nil { app.Action = c.Action } else { From 2535376782cb8bf244a767afd45250ce7fc8b326 Mon Sep 17 00:00:00 2001 From: Summer Mousa Date: Thu, 17 Apr 2014 11:48:00 -0500 Subject: [PATCH 6/6] If the Subcommand is instantiated, via the Before method and has no subcommands, display the CommandHelp instead of the SubcommandHelp --- app.go | 36 ++++++++++++++++++++++++++---------- command.go | 3 ++- 2 files changed, 28 insertions(+), 11 deletions(-) diff --git a/app.go b/app.go index a4a76ef..a792705 100644 --- a/app.go +++ b/app.go @@ -130,13 +130,15 @@ func (a *App) Run(arguments []string) error { } // Invokes the subcommand given the context, parses ctx.Args() to generate command-specific flags -func (a *App) RunAsSubcommand(c *Context) error { +func (a *App) RunAsSubcommand(ctx *Context) error { // append help to commands - if a.Command(helpCommand.Name) == nil { - a.Commands = append(a.Commands, helpCommand) + if len(a.Commands) > 0 { + if a.Command(helpCommand.Name) == nil { + a.Commands = append(a.Commands, helpCommand) + } } - // append help flags + // append flags if a.EnableBashCompletion { a.appendFlag(BashCompletionFlag) } @@ -145,13 +147,17 @@ func (a *App) RunAsSubcommand(c *Context) error { // parse flags set := flagSet(a.Name, a.Flags) set.SetOutput(ioutil.Discard) - err := set.Parse(c.Args().Tail()) + err := set.Parse(ctx.Args().Tail()) nerr := normalizeFlags(a.Flags, set) context := NewContext(a, set, set) if nerr != nil { fmt.Println(nerr) - ShowSubcommandHelp(context) + if len(a.Commands) > 0 { + ShowSubcommandHelp(context) + } else { + ShowCommandHelp(ctx, context.Args().First()) + } fmt.Println("") return nerr } @@ -159,7 +165,6 @@ func (a *App) RunAsSubcommand(c *Context) error { if err != nil { fmt.Printf("Incorrect Usage.\n\n") ShowSubcommandHelp(context) - fmt.Println("") return err } @@ -167,8 +172,14 @@ func (a *App) RunAsSubcommand(c *Context) error { return nil } - if checkSubcommandHelp(context) { - return nil + if len(a.Commands) > 0 { + if checkSubcommandHelp(context) { + return nil + } + } else { + if checkCommandHelp(ctx, context.Args().First()) { + return nil + } } if a.Before != nil { @@ -188,7 +199,12 @@ func (a *App) RunAsSubcommand(c *Context) error { } // Run default Action - a.Action(context) + if len(a.Commands) > 0 { + a.Action(context) + } else { + a.Action(ctx) + } + return nil } diff --git a/command.go b/command.go index 735505b..cbf6e24 100644 --- a/command.go +++ b/command.go @@ -31,7 +31,8 @@ type Command struct { // Invokes the command given the context, parses ctx.Args() to generate command-specific flags func (c Command) Run(ctx *Context) error { - if (c.Subcommands != nil && len(c.Subcommands) > 0) || c.Before != nil { + + if len(c.Subcommands) > 0 || c.Before != nil { return c.startApp(ctx) }