diff --git a/README.md b/README.md index ff1e584..0c12ce2 100644 --- a/README.md +++ b/README.md @@ -191,5 +191,46 @@ app.Commands = []cli.Command{ ... ``` +### Bash Completion + +You can enable completion commands by setting the EnableBashCompletion +flag on the App object. By default, this setting will only auto-complete to +show an app's subcommands, but you can write your own completion methods for +the App or its subcommands. +```go +... +var tasks = []string{"cook", "clean", "laundry", "eat", "sleep", "code"} +app := cli.NewApp() +app.EnableBashCompletion = true +app.Commands = []cli.Command{ + { + Name: "complete", + ShortName: "c", + Usage: "complete a task on the list", + Action: func(c *cli.Context) { + println("completed task: ", c.Args().First()) + }, + BashComplete: func(c *cli.Context) { + // This will complete if no args are passed + if len(c.Args()) > 0 { + return + } + for _, t := range tasks { + println(t) + } + }, + } +} +... +``` + +#### To Enable + +Source the autocomplete/bash_autocomplete file in your .bashrc file while +setting the PROG variable to the name of your program: + +`PROG=myprogram source /.../cli/autocomplete/bash_autocomplete` + + ## About cli.go is written by none other than the [Code Gangsta](http://codegangsta.io) diff --git a/app.go b/app.go index a614236..a7c7122 100644 --- a/app.go +++ b/app.go @@ -20,6 +20,10 @@ type App struct { Commands []Command // List of flags to parse Flags []Flag + // Boolean to enable bash completion commands + EnableBashCompletion bool + // 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 // If a non-nil error is returned, no subcommands are run Before func(context *Context) error @@ -46,13 +50,14 @@ func compileTime() time.Time { // Creates a new cli Application with some reasonable defaults for Name, Usage, Version and Action. func NewApp() *App { return &App{ - Name: os.Args[0], - Usage: "A new cli application", - Version: "0.0.0", - Action: helpCommand.Action, - Compiled: compileTime(), - Author: "Author", - Email: "unknown@email", + Name: os.Args[0], + Usage: "A new cli application", + Version: "0.0.0", + BashComplete: DefaultAppComplete, + Action: helpCommand.Action, + Compiled: compileTime(), + Author: "Author", + Email: "unknown@email", } } @@ -64,6 +69,9 @@ func (a *App) Run(arguments []string) error { } //append version/help flags + if a.EnableBashCompletion { + a.appendFlag(BashCompletionFlag) + } a.appendFlag(BoolFlag{"version, v", "print the version"}) a.appendFlag(BoolFlag{"help, h", "show help"}) @@ -88,6 +96,10 @@ func (a *App) Run(arguments []string) error { return err } + if checkCompletions(context) { + return nil + } + if checkHelp(context) { return nil } diff --git a/app_test.go b/app_test.go index 4e94f9e..49c525c 100644 --- a/app_test.go +++ b/app_test.go @@ -35,9 +35,9 @@ func ExampleAppHelp() { } app.Commands = []cli.Command{ { - Name: "describeit", - ShortName: "d", - Usage: "use it to see a description", + Name: "describeit", + ShortName: "d", + Usage: "use it to see a description", Description: "This is how we describe describeit the function", Action: func(c *cli.Context) { fmt.Printf("i like to describe things") @@ -58,6 +58,41 @@ func ExampleAppHelp() { // OPTIONS: } +func ExampleAppBashComplete() { + // set args for examples sake + os.Args = []string{"greet", "--generate-bash-completion"} + + app := cli.NewApp() + app.Name = "greet" + app.EnableBashCompletion = true + app.Commands = []cli.Command{ + { + Name: "describeit", + ShortName: "d", + Usage: "use it to see a description", + Description: "This is how we describe describeit the function", + Action: func(c *cli.Context) { + fmt.Printf("i like to describe things") + }, + }, { + Name: "next", + Usage: "next example", + Description: "more stuff to see when generating bash completion", + Action: func(c *cli.Context) { + fmt.Printf("the next example") + }, + }, + } + + app.Run(os.Args) + // Output: + // describeit + // d + // next + // help + // h +} + func TestApp_Run(t *testing.T) { s := "" diff --git a/autocomplete/bash_autocomplete b/autocomplete/bash_autocomplete new file mode 100644 index 0000000..a860e03 --- /dev/null +++ b/autocomplete/bash_autocomplete @@ -0,0 +1,13 @@ +#! /bin/bash + +_cli_bash_autocomplete() { + local cur prev opts base + COMPREPLY=() + cur="${COMP_WORDS[COMP_CWORD]}" + prev="${COMP_WORDS[COMP_CWORD-1]}" + opts=$( ${COMP_WORDS[@]:0:COMP_CWORD} --generate-bash-completion ) + COMPREPLY=( $(compgen -W "${opts}" -- ${cur}) ) + return 0 + } + + complete -F _cli_bash_autocomplete $PROG \ No newline at end of file diff --git a/command.go b/command.go index d05cdf5..60d29aa 100644 --- a/command.go +++ b/command.go @@ -16,6 +16,8 @@ type Command struct { Usage string // A longer explaination of how the command works Description string + // The function to call when checking for bash command completions + BashComplete func(context *Context) // The function to call when this command is invoked Action func(context *Context) // List of flags to parse @@ -30,6 +32,10 @@ func (c Command) Run(ctx *Context) error { BoolFlag{"help, h", "show help"}, ) + if ctx.App.EnableBashCompletion { + c.Flags = append(c.Flags, BashCompletionFlag) + } + set := flagSet(c.Name, c.Flags) set.SetOutput(ioutil.Discard) @@ -67,6 +73,11 @@ func (c Command) Run(ctx *Context) error { return nerr } context := NewContext(ctx.App, set, ctx.globalSet) + + if checkCommandCompletions(context, c.Name) { + return nil + } + if checkCommandHelp(context, c.Name) { return nil } diff --git a/flag.go b/flag.go index 59dbdf7..cec34b0 100644 --- a/flag.go +++ b/flag.go @@ -7,6 +7,9 @@ import ( "strings" ) +// This flag enables bash-completion for all commands and subcommands +var BashCompletionFlag = BoolFlag{"generate-bash-completion", ""} + // Flag is a common interface related to parsing flags in cli. // For more advanced flag parsing techniques, it is recomended that // this interface be implemented. diff --git a/help.go b/help.go index 094d01d..64b4bab 100644 --- a/help.go +++ b/help.go @@ -60,10 +60,21 @@ var helpCommand = Command{ // Prints help for the App var HelpPrinter = printHelp + func ShowAppHelp(c *Context) { HelpPrinter(AppHelpTemplate, c.App) } +// Prints the list of subcommands as the default app completion method +func DefaultAppComplete(c *Context) { + for _, command := range c.App.Commands { + fmt.Println(command.Name) + if command.ShortName != "" { + fmt.Println(command.ShortName) + } + } +} + // Prints help for the given command func ShowCommandHelp(c *Context, command string) { for _, c := range c.App.Commands { @@ -81,6 +92,22 @@ func ShowVersion(c *Context) { fmt.Printf("%v version %v\n", c.App.Name, c.App.Version) } +// Prints the lists of commands within a given context +func ShowCompletions(c *Context) { + a := c.App + if a != nil && a.BashComplete != nil { + a.BashComplete(c) + } +} + +// Prints the custom completions for a given command +func ShowCommandCompletions(ctx *Context, command string) { + c := ctx.App.Command(command) + if c != nil && c.BashComplete != nil { + c.BashComplete(ctx) + } +} + func printHelp(templ string, data interface{}) { w := tabwriter.NewWriter(os.Stdout, 0, 8, 1, '\t', 0) t := template.Must(template.New("help").Parse(templ)) @@ -117,3 +144,21 @@ func checkCommandHelp(c *Context, name string) bool { return false } + +func checkCompletions(c *Context) bool { + if c.GlobalBool(BashCompletionFlag.Name) && c.App.EnableBashCompletion { + ShowCompletions(c) + return true + } + + return false +} + +func checkCommandCompletions(c *Context, name string) bool { + if c.Bool(BashCompletionFlag.Name) && c.App.EnableBashCompletion { + ShowCommandCompletions(c, name) + return true + } + + return false +}