diff --git a/README.md b/README.md index 0c12ce2..4621310 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()) + }, + }, + }, + }, } ... ``` diff --git a/app.go b/app.go index a7c7122..a792705 100644 --- a/app.go +++ b/app.go @@ -129,6 +129,85 @@ 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(ctx *Context) error { + // append help to commands + if len(a.Commands) > 0 { + if a.Command(helpCommand.Name) == nil { + a.Commands = append(a.Commands, helpCommand) + } + } + + // append 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(ctx.Args().Tail()) + nerr := normalizeFlags(a.Flags, set) + context := NewContext(a, set, set) + + if nerr != nil { + fmt.Println(nerr) + if len(a.Commands) > 0 { + ShowSubcommandHelp(context) + } else { + ShowCommandHelp(ctx, context.Args().First()) + } + fmt.Println("") + return nerr + } + + if err != nil { + fmt.Printf("Incorrect Usage.\n\n") + ShowSubcommandHelp(context) + return err + } + + if checkCompletions(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 { + err := a.Before(context) + if err != nil { + return err + } + } + + args := context.Args() + if args.Present() { + name := args.First() + c := a.Command(name) + if c != nil { + return c.Run(context) + } + } + + // Run default Action + if len(a.Commands) > 0 { + a.Action(context) + } else { + a.Action(ctx) + } + + 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/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/command.go b/command.go index 60d29aa..cbf6e24 100644 --- a/command.go +++ b/command.go @@ -14,18 +14,28 @@ 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) + // 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 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 || c.Before != nil { + return c.startApp(ctx) + } + // append help to flags c.Flags = append( c.Flags, @@ -89,3 +99,35 @@ 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 actions + app.Before = c.Before + 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..0b81894 100644 --- a/help.go +++ b/help.go @@ -44,6 +44,23 @@ 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 +75,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 +118,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 +181,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)