Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for flags bash completion #808

Merged
merged 13 commits into from Aug 6, 2019
83 changes: 83 additions & 0 deletions app_test.go
Expand Up @@ -221,6 +221,89 @@ func ExampleApp_Run_subcommandNoAction() {

}

func ExampleApp_Run_bashComplete_withShortFlag() {
os.Args = []string{"greet", "-", "--generate-bash-completion"}

app := NewApp()
app.Name = "greet"
app.EnableBashCompletion = true
app.Flags = []Flag{
IntFlag{
Name: "other,o",
},
StringFlag{
Name: "xyz,x",
},
}

app.Run(os.Args)
// Output:
// --other
// -o
// --xyz
// -x
// --help
// -h
// --version
// -v
}
AudriusButkevicius marked this conversation as resolved.
Show resolved Hide resolved

func ExampleApp_Run_bashComplete_withLongFlag() {
os.Args = []string{"greet", "--s", "--generate-bash-completion"}

app := NewApp()
app.Name = "greet"
app.EnableBashCompletion = true
app.Flags = []Flag{
IntFlag{
Name: "other,o",
},
StringFlag{
Name: "xyz,x",
},
StringFlag{
Name: "some-flag,s",
},
StringFlag{
Name: "similar-flag",
},
}

app.Run(os.Args)
// Output:
// --some-flag
// --similar-flag
}
yogeshlonkar marked this conversation as resolved.
Show resolved Hide resolved
func ExampleApp_Run_bashComplete_withMultipleLongFlag() {
os.Args = []string{"greet", "--st", "--generate-bash-completion"}

app := NewApp()
app.Name = "greet"
app.EnableBashCompletion = true
app.Flags = []Flag{
IntFlag{
Name: "int-flag,i",
},
StringFlag{
Name: "string,s",
},
StringFlag{
Name: "string-flag-2",
},
StringFlag{
Name: "similar-flag",
},
StringFlag{
Name: "some-flag",
},
}

app.Run(os.Args)
// Output:
// --string
// --string-flag-2
}

func ExampleApp_Run_bashComplete() {
// set args for examples sake
os.Args = []string{"greet", "--generate-bash-completion"}
Expand Down
11 changes: 8 additions & 3 deletions autocomplete/bash_autocomplete
Expand Up @@ -3,14 +3,19 @@
: ${PROG:=$(basename ${BASH_SOURCE})}

_cli_bash_autocomplete() {
if [[ "${COMP_WORDS[0]}" != "source" ]]; then
local cur opts base
COMPREPLY=()
cur="${COMP_WORDS[COMP_CWORD]}"
opts=$( ${COMP_WORDS[@]:0:$COMP_CWORD} --generate-bash-completion )
if [[ "$cur" == "-"* ]]; then
opts=$( ${COMP_WORDS[@]:0:$COMP_CWORD} ${cur} --generate-bash-completion )
yogeshlonkar marked this conversation as resolved.
Show resolved Hide resolved
else
opts=$( ${COMP_WORDS[@]:0:$COMP_CWORD} --generate-bash-completion )
yogeshlonkar marked this conversation as resolved.
Show resolved Hide resolved
fi
COMPREPLY=( $(compgen -W "${opts}" -- ${cur}) )
return 0
fi
}

complete -F _cli_bash_autocomplete $PROG

complete -o bashdefault -o default -o nospace -F _cli_bash_autocomplete $PROG
yogeshlonkar marked this conversation as resolved.
Show resolved Hide resolved
unset PROG
85 changes: 80 additions & 5 deletions help.go
Expand Up @@ -7,6 +7,7 @@ import (
"strings"
"text/tabwriter"
"text/template"
"unicode/utf8"
)

// AppHelpTemplate is the text template for the Default help topic.
Expand Down Expand Up @@ -157,19 +158,88 @@ func ShowAppHelp(c *Context) (err error) {

// DefaultAppComplete prints the list of subcommands as the default app completion method
func DefaultAppComplete(c *Context) {
for _, command := range c.App.Commands {
DefaultCompleteWithFlags(nil)(c)
yogeshlonkar marked this conversation as resolved.
Show resolved Hide resolved
}

func printCommandSuggestions(commands []Command, writer io.Writer) {
for _, command := range commands {
if command.Hidden {
continue
}
if os.Getenv("_CLI_ZSH_AUTOCOMPLETE_HACK") == "1" {
for _, name := range command.Names() {
fmt.Fprintf(c.App.Writer, "%s:%s\n", name, command.Usage)
fmt.Fprintf(writer, "%s:%s\n", name, command.Usage)
}
} else {
for _, name := range command.Names() {
fmt.Fprintf(c.App.Writer, "%s\n", name)
fmt.Fprintf(writer, "%s\n", name)
}
}
}
}

func cliArgContains(flagName string) bool {
for _, name := range strings.Split(flagName, ",") {
name = strings.TrimSpace(name)
count := utf8.RuneCountInString(name)
if count > 2 {
count = 2
}
flag := fmt.Sprintf("%s%s", strings.Repeat("-", count), name)
for _, a := range os.Args {
if a == flag {
return true
}
}
}
return false
}

func printFlagSuggestions(lastArg string, flags []Flag, writer io.Writer) {
cur := strings.TrimPrefix(lastArg, "-")
cur = strings.TrimPrefix(cur, "-")
for _, flag := range flags {
if bflag, ok := flag.(BoolFlag); ok && bflag.Hidden {
continue
}
for _, name := range strings.Split(flag.GetName(), ",") {
name = strings.TrimSpace(name)
// this will get total count utf8 letters in flag name
count := utf8.RuneCountInString(name)
if count > 2 {
count = 2 // resuse this count to generate single - or -- in flag completion
}
AudriusButkevicius marked this conversation as resolved.
Show resolved Hide resolved
// if flag name has more than one utf8 letter and last argument in cli has -- prefix then
// skip flag completion for short flags example -v or -x
if strings.HasPrefix(lastArg, "--") && count == 1 {
continue
}
yogeshlonkar marked this conversation as resolved.
Show resolved Hide resolved
// match if last argument matches this flag and it is not repeated
if strings.HasPrefix(name, cur) && cur != name && !cliArgContains(flag.GetName()) {
flagCompletion := fmt.Sprintf("%s%s", strings.Repeat("-", count), name)
fmt.Fprintln(writer, flagCompletion)
}
yogeshlonkar marked this conversation as resolved.
Show resolved Hide resolved
}
}
AudriusButkevicius marked this conversation as resolved.
Show resolved Hide resolved
}

func DefaultCompleteWithFlags(cmd *Command) func(c *Context) {
yogeshlonkar marked this conversation as resolved.
Show resolved Hide resolved
return func(c *Context) {
if len(os.Args) > 2 {
lastArg := os.Args[len(os.Args)-2]
if strings.HasPrefix(lastArg, "-") {
printFlagSuggestions(lastArg, c.App.Flags, c.App.Writer)
if cmd != nil {
printFlagSuggestions(lastArg, cmd.Flags, c.App.Writer)
}
return
}
}
if cmd != nil {
printCommandSuggestions(cmd.Subcommands, c.App.Writer)
} else {
printCommandSuggestions(c.App.Commands, c.App.Writer)
}
}
}

Expand Down Expand Up @@ -231,9 +301,14 @@ func ShowCompletions(c *Context) {
// ShowCommandCompletions 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)
if c != nil {
if c.BashComplete != nil {
c.BashComplete(ctx)
} else {
DefaultCompleteWithFlags(c)(ctx)
}
}

}

func printHelpCustom(out io.Writer, templ string, data interface{}, customFunc map[string]interface{}) {
Expand Down