From 58a072d5733d4bb2dc61ffbc3557ec9592e34adc Mon Sep 17 00:00:00 2001 From: Yogesh Lonkar Date: Wed, 20 Mar 2019 20:28:51 +0530 Subject: [PATCH 1/9] Add bash completion support for flags --- autocomplete/bash_autocomplete | 9 +++- help.go | 79 ++++++++++++++++++++++++++++++---- 2 files changed, 79 insertions(+), 9 deletions(-) diff --git a/autocomplete/bash_autocomplete b/autocomplete/bash_autocomplete index 37d9c14..d9305db 100755 --- a/autocomplete/bash_autocomplete +++ b/autocomplete/bash_autocomplete @@ -6,7 +6,14 @@ _cli_bash_autocomplete() { 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 ) + else + opts=$( ${COMP_WORDS[@]:0:$COMP_CWORD} --generate-bash-completion ) + fi + if [[ "$opts1" == "$cur1" ]]; then + return 0 + fi COMPREPLY=( $(compgen -W "${opts}" -- ${cur}) ) return 0 } diff --git a/help.go b/help.go index 65874fa..be79e88 100644 --- a/help.go +++ b/help.go @@ -4,9 +4,11 @@ import ( "fmt" "io" "os" + "regexp" "strings" "text/tabwriter" "text/template" + "unicode/utf8" ) // AppHelpTemplate is the text template for the Default help topic. @@ -152,19 +154,80 @@ func ShowAppHelp(c *Context) (err error) { return nil } +var shortFlagRegex = regexp.MustCompile(`^-`) + // DefaultAppComplete prints the list of subcommands as the default app completion method func DefaultAppComplete(c *Context) { - for _, command := range c.App.Commands { - if command.Hidden { - continue + DefaultAppCompleteWithFlags(nil)(c) +} + +func DefaultAppCompleteWithFlags(cmd *Command) func(c *Context) { + return func(c *Context) { + if len(os.Args) > 2 { + lastArg := os.Args[len(os.Args)-2] + if strings.HasPrefix(lastArg, "-") { + lastArg = shortFlagRegex.ReplaceAllString(lastArg, "") + lastArg = shortFlagRegex.ReplaceAllString(lastArg, "") + for _, flag := range c.App.Flags { + for _, name := range strings.Split(flag.GetName(), ",") { + name = strings.Trim(name, " ") + if strings.HasPrefix(name, lastArg) && lastArg != name { + count := utf8.RuneCountInString(name) + if count > 2 { + count = 2 + } + fmt.Fprintf(c.App.Writer, "%s%s\n", strings.Repeat("-", count), name) + } + } + } + if cmd != nil { + for _, flag := range cmd.Flags { + for _, name := range strings.Split(flag.GetName(), ",") { + name = strings.Trim(name, " ") + if strings.HasPrefix(name, lastArg) && lastArg != name { + count := utf8.RuneCountInString(name) + if count > 2 { + count = 2 + } + fmt.Fprintf(c.App.Writer, "%s%s\n", strings.Repeat("-", count), name) + } + } + } + } + return + } } - if os.Getenv("_CLI_ZSH_AUTOCOMPLETE_HACK") == "1" { - for _, name := range command.Names() { - fmt.Fprintf(c.App.Writer, "%s:%s\n", name, command.Usage) + if cmd != nil { + for _, command := range cmd.Subcommands { + 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) + } + } else { + for _, name := range command.Names() { + if name != "h" { + fmt.Fprintf(c.App.Writer, "%s\n", name) + } + } + } } } else { - for _, name := range command.Names() { - fmt.Fprintf(c.App.Writer, "%s\n", name) + for _, command := range c.App.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) + } + } else { + for _, name := range command.Names() { + fmt.Fprintf(c.App.Writer, "%s\n", name) + } + } } } } From fb1421d9031313c5e0f3c4a92625ed9cf5739b0d Mon Sep 17 00:00:00 2001 From: Yogesh Lonkar Date: Wed, 20 Mar 2019 21:34:56 +0530 Subject: [PATCH 2/9] Fix duplicate completion of existing flag --- autocomplete/bash_autocomplete | 3 --- help.go | 42 ++++++++++++++++++++++++---------- 2 files changed, 30 insertions(+), 15 deletions(-) diff --git a/autocomplete/bash_autocomplete b/autocomplete/bash_autocomplete index d9305db..303d126 100755 --- a/autocomplete/bash_autocomplete +++ b/autocomplete/bash_autocomplete @@ -11,9 +11,6 @@ _cli_bash_autocomplete() { else opts=$( ${COMP_WORDS[@]:0:$COMP_CWORD} --generate-bash-completion ) fi - if [[ "$opts1" == "$cur1" ]]; then - return 0 - fi COMPREPLY=( $(compgen -W "${opts}" -- ${cur}) ) return 0 } diff --git a/help.go b/help.go index be79e88..bd68295 100644 --- a/help.go +++ b/help.go @@ -162,6 +162,22 @@ func DefaultAppComplete(c *Context) { } func DefaultAppCompleteWithFlags(cmd *Command) func(c *Context) { + cliArgContains := func(flagName string) bool { + for _, name := range strings.Split(flagName, ",") { + name = strings.Trim(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 + } return func(c *Context) { if len(os.Args) > 2 { lastArg := os.Args[len(os.Args)-2] @@ -171,12 +187,13 @@ func DefaultAppCompleteWithFlags(cmd *Command) func(c *Context) { for _, flag := range c.App.Flags { for _, name := range strings.Split(flag.GetName(), ",") { name = strings.Trim(name, " ") - if strings.HasPrefix(name, lastArg) && lastArg != name { - count := utf8.RuneCountInString(name) - if count > 2 { - count = 2 - } - fmt.Fprintf(c.App.Writer, "%s%s\n", strings.Repeat("-", count), name) + count := utf8.RuneCountInString(name) + if count > 2 { + count = 2 + } + flagCompletion := fmt.Sprintf("%s%s", strings.Repeat("-", count), name) + if strings.HasPrefix(name, lastArg) && lastArg != name && !cliArgContains(flag.GetName()) { + fmt.Fprintln(c.App.Writer, flagCompletion) } } } @@ -184,12 +201,13 @@ func DefaultAppCompleteWithFlags(cmd *Command) func(c *Context) { for _, flag := range cmd.Flags { for _, name := range strings.Split(flag.GetName(), ",") { name = strings.Trim(name, " ") - if strings.HasPrefix(name, lastArg) && lastArg != name { - count := utf8.RuneCountInString(name) - if count > 2 { - count = 2 - } - fmt.Fprintf(c.App.Writer, "%s%s\n", strings.Repeat("-", count), name) + count := utf8.RuneCountInString(name) + if count > 2 { + count = 2 + } + flagCompletion := fmt.Sprintf("%s%s", strings.Repeat("-", count), name) + if strings.HasPrefix(name, lastArg) && lastArg != name && !cliArgContains(flag.GetName()) { + fmt.Fprintln(c.App.Writer, flagCompletion) } } } From 1d7a2b08d6f8e9764e2f2b911b1bb9fa49596f92 Mon Sep 17 00:00:00 2001 From: Yogesh Lonkar Date: Thu, 21 Mar 2019 13:01:48 +0530 Subject: [PATCH 3/9] Add default completion on commands, test cases, refactor code --- app_test.go | 54 ++++++++++++++++++++++ help.go | 130 ++++++++++++++++++++++++---------------------------- 2 files changed, 114 insertions(+), 70 deletions(-) diff --git a/app_test.go b/app_test.go index 629681e..e7841e6 100644 --- a/app_test.go +++ b/app_test.go @@ -221,6 +221,60 @@ 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 +} + +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 +} + func ExampleApp_Run_bashComplete() { // set args for examples sake os.Args = []string{"greet", "--generate-bash-completion"} diff --git a/help.go b/help.go index bd68295..c20f5a4 100644 --- a/help.go +++ b/help.go @@ -158,95 +158,80 @@ var shortFlagRegex = regexp.MustCompile(`^-`) // DefaultAppComplete prints the list of subcommands as the default app completion method func DefaultAppComplete(c *Context) { - DefaultAppCompleteWithFlags(nil)(c) + DefaultCompleteWithFlags(nil)(c) } -func DefaultAppCompleteWithFlags(cmd *Command) func(c *Context) { - cliArgContains := func(flagName string) bool { - for _, name := range strings.Split(flagName, ",") { +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(writer, "%s:%s\n", name, command.Usage) + } + } else { + for _, name := range command.Names() { + fmt.Fprintf(writer, "%s\n", name) + } + } + } +} + +func cliArgContains(flagName string) bool { + for _, name := range strings.Split(flagName, ",") { + name = strings.Trim(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 := shortFlagRegex.ReplaceAllString(lastArg, "") + cur = shortFlagRegex.ReplaceAllString(cur, "") + for _, flag := range flags { + for _, name := range strings.Split(flag.GetName(), ",") { name = strings.Trim(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 - } + if strings.HasPrefix(lastArg, "--") && count == 1 { + continue + } + flagCompletion := fmt.Sprintf("%s%s", strings.Repeat("-", count), name) + if strings.HasPrefix(name, cur) && cur != name && !cliArgContains(flag.GetName()) { + fmt.Fprintln(writer, flagCompletion) } } - return false } +} + +func DefaultCompleteWithFlags(cmd *Command) func(c *Context) { return func(c *Context) { if len(os.Args) > 2 { lastArg := os.Args[len(os.Args)-2] if strings.HasPrefix(lastArg, "-") { - lastArg = shortFlagRegex.ReplaceAllString(lastArg, "") - lastArg = shortFlagRegex.ReplaceAllString(lastArg, "") - for _, flag := range c.App.Flags { - for _, name := range strings.Split(flag.GetName(), ",") { - name = strings.Trim(name, " ") - count := utf8.RuneCountInString(name) - if count > 2 { - count = 2 - } - flagCompletion := fmt.Sprintf("%s%s", strings.Repeat("-", count), name) - if strings.HasPrefix(name, lastArg) && lastArg != name && !cliArgContains(flag.GetName()) { - fmt.Fprintln(c.App.Writer, flagCompletion) - } - } - } + printFlagSuggestions(lastArg, c.App.Flags, c.App.Writer) if cmd != nil { - for _, flag := range cmd.Flags { - for _, name := range strings.Split(flag.GetName(), ",") { - name = strings.Trim(name, " ") - count := utf8.RuneCountInString(name) - if count > 2 { - count = 2 - } - flagCompletion := fmt.Sprintf("%s%s", strings.Repeat("-", count), name) - if strings.HasPrefix(name, lastArg) && lastArg != name && !cliArgContains(flag.GetName()) { - fmt.Fprintln(c.App.Writer, flagCompletion) - } - } - } + printFlagSuggestions(lastArg, cmd.Flags, c.App.Writer) } return } } if cmd != nil { - for _, command := range cmd.Subcommands { - 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) - } - } else { - for _, name := range command.Names() { - if name != "h" { - fmt.Fprintf(c.App.Writer, "%s\n", name) - } - } - } - } + printCommandSuggestions(cmd.Subcommands, c.App.Writer) } else { - for _, command := range c.App.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) - } - } else { - for _, name := range command.Names() { - fmt.Fprintf(c.App.Writer, "%s\n", name) - } - } - } + printCommandSuggestions(c.App.Commands, c.App.Writer) } } } @@ -309,9 +294,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{}) { From 62f02f21ef0b5c3c0aa67d3240aee15bc8a53457 Mon Sep 17 00:00:00 2001 From: Yogesh Lonkar Date: Thu, 11 Apr 2019 10:57:58 +0530 Subject: [PATCH 4/9] Don't complete hidden flags --- help.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/help.go b/help.go index c20f5a4..72c59b1 100644 --- a/help.go +++ b/help.go @@ -199,6 +199,9 @@ func printFlagSuggestions(lastArg string, flags []Flag, writer io.Writer) { cur := shortFlagRegex.ReplaceAllString(lastArg, "") cur = shortFlagRegex.ReplaceAllString(cur, "") for _, flag := range flags { + if bflag, ok := flag.(BoolFlag); ok && bflag.Hidden { + continue + } for _, name := range strings.Split(flag.GetName(), ",") { name = strings.Trim(name, " ") count := utf8.RuneCountInString(name) From d79d2a04242b21441061e00475287f4b826614f8 Mon Sep 17 00:00:00 2001 From: Yogesh Lonkar Date: Wed, 24 Jul 2019 16:08:47 +0200 Subject: [PATCH 5/9] Fix issue with source command completion Avoid competion for bash builtin `source` and fallback to default implementation as it throws below error ``` -bash: source: --: invalid option source: usage: source filename [arguments] ``` --- autocomplete/bash_autocomplete | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/autocomplete/bash_autocomplete b/autocomplete/bash_autocomplete index 303d126..f5a8d1f 100755 --- a/autocomplete/bash_autocomplete +++ b/autocomplete/bash_autocomplete @@ -3,6 +3,7 @@ : ${PROG:=$(basename ${BASH_SOURCE})} _cli_bash_autocomplete() { + if [[ "${COMP_WORDS[@]:0:$COMP_CWORD}" != "source" ]]; then local cur opts base COMPREPLY=() cur="${COMP_WORDS[COMP_CWORD]}" @@ -13,8 +14,8 @@ _cli_bash_autocomplete() { 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 unset PROG From c5612e8cd21e0cd99f73d23103df99a9af70f853 Mon Sep 17 00:00:00 2001 From: Yogesh Lonkar Date: Mon, 5 Aug 2019 16:58:04 +0200 Subject: [PATCH 6/9] Fix review comments --- autocomplete/bash_autocomplete | 2 +- help.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/autocomplete/bash_autocomplete b/autocomplete/bash_autocomplete index f5a8d1f..a118bda 100755 --- a/autocomplete/bash_autocomplete +++ b/autocomplete/bash_autocomplete @@ -7,7 +7,7 @@ _cli_bash_autocomplete() { local cur opts base COMPREPLY=() cur="${COMP_WORDS[COMP_CWORD]}" - if [[ $cur == -* ]]; then + if [[ "$cur" == "-"* ]]; then opts=$( ${COMP_WORDS[@]:0:$COMP_CWORD} ${cur} --generate-bash-completion ) else opts=$( ${COMP_WORDS[@]:0:$COMP_CWORD} --generate-bash-completion ) diff --git a/help.go b/help.go index 8e74ec4..efc3889 100644 --- a/help.go +++ b/help.go @@ -183,7 +183,7 @@ func printCommandSuggestions(commands []Command, writer io.Writer) { func cliArgContains(flagName string) bool { for _, name := range strings.Split(flagName, ",") { - name = strings.Trim(name, " ") + name = strings.TrimSpace(name) count := utf8.RuneCountInString(name) if count > 2 { count = 2 From c3f51bed6fffdf84227c5b59bd3f2e90683314df Mon Sep 17 00:00:00 2001 From: Yogesh Lonkar Date: Mon, 5 Aug 2019 17:07:46 +0200 Subject: [PATCH 7/9] Fix SC2199: Arrays implicitly concatenate in --- autocomplete/bash_autocomplete | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/autocomplete/bash_autocomplete b/autocomplete/bash_autocomplete index a118bda..f0f6241 100755 --- a/autocomplete/bash_autocomplete +++ b/autocomplete/bash_autocomplete @@ -3,7 +3,7 @@ : ${PROG:=$(basename ${BASH_SOURCE})} _cli_bash_autocomplete() { - if [[ "${COMP_WORDS[@]:0:$COMP_CWORD}" != "source" ]]; then + if [[ "${COMP_WORDS[0]}" != "source" ]]; then local cur opts base COMPREPLY=() cur="${COMP_WORDS[COMP_CWORD]}" From 2be2bc755e4634d34136769a426a7ca52e698cc0 Mon Sep 17 00:00:00 2001 From: Yogesh Lonkar Date: Mon, 5 Aug 2019 20:18:08 +0200 Subject: [PATCH 8/9] Add additional test for log flag completion and comments --- app_test.go | 29 +++++++++++++++++++++++++++++ help.go | 14 +++++++++----- 2 files changed, 38 insertions(+), 5 deletions(-) diff --git a/app_test.go b/app_test.go index 6d5ccd0..3fc27b5 100644 --- a/app_test.go +++ b/app_test.go @@ -274,6 +274,35 @@ func ExampleApp_Run_bashComplete_withLongFlag() { // --some-flag // --similar-flag } +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 diff --git a/help.go b/help.go index efc3889..d057a03 100644 --- a/help.go +++ b/help.go @@ -199,23 +199,27 @@ func cliArgContains(flagName string) bool { } func printFlagSuggestions(lastArg string, flags []Flag, writer io.Writer) { - cur := shortFlagRegex.ReplaceAllString(lastArg, "") - cur = shortFlagRegex.ReplaceAllString(cur, "") + 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.Trim(name, " ") + name = strings.TrimSpace(name) + // this will get total count utf8 letters in flag name count := utf8.RuneCountInString(name) if count > 2 { - count = 2 + count = 2 // resuse this count to generate single - or -- in flag completion } + // 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 } - flagCompletion := fmt.Sprintf("%s%s", strings.Repeat("-", count), name) + // 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) } } From 1db049685ac49e11b2e27285e1287793cfe0ea84 Mon Sep 17 00:00:00 2001 From: Yogesh Lonkar Date: Mon, 5 Aug 2019 20:22:52 +0200 Subject: [PATCH 9/9] Fix unused regex --- help.go | 3 --- 1 file changed, 3 deletions(-) diff --git a/help.go b/help.go index d057a03..e504fc2 100644 --- a/help.go +++ b/help.go @@ -4,7 +4,6 @@ import ( "fmt" "io" "os" - "regexp" "strings" "text/tabwriter" "text/template" @@ -157,8 +156,6 @@ func ShowAppHelp(c *Context) (err error) { return nil } -var shortFlagRegex = regexp.MustCompile(`^-`) - // DefaultAppComplete prints the list of subcommands as the default app completion method func DefaultAppComplete(c *Context) { DefaultCompleteWithFlags(nil)(c)