From 93f36455edb49c0a04d65b174fb0a52ee65e9e75 Mon Sep 17 00:00:00 2001 From: Marc Khouzam Date: Sat, 16 May 2020 20:06:45 -0400 Subject: [PATCH] Bash comp v2 backwards-compatibility Signed-off-by: Marc Khouzam --- bash_completionsV2.go | 39 +++++- custom_completions.go | 70 ++++++++++ custom_completions_test.go | 256 +++++++++++++++++++++++++++++++++++++ fish_completions.go | 14 +- zsh_completions.go | 13 +- 5 files changed, 389 insertions(+), 3 deletions(-) diff --git a/bash_completionsV2.go b/bash_completionsV2.go index b21c35d1a5..2f1392e445 100644 --- a/bash_completionsV2.go +++ b/bash_completionsV2.go @@ -9,7 +9,11 @@ import ( func (c *Command) genBashCompletion(w io.Writer, includeDesc bool) error { buf := new(bytes.Buffer) + if len(c.BashCompletionFunction) > 0 { + buf.WriteString(c.BashCompletionFunction + "\n") + } genBashComp(buf, c.Name(), includeDesc) + _, err := buf.WriteTo(w) return err } @@ -46,6 +50,8 @@ __%[1]s_perform_completion() local shellCompDirectiveNoFileComp=%[5]d local shellCompDirectiveFilterFileExt=%[6]d local shellCompDirectiveFilterDirs=%[7]d + local shellCompDirectiveLegacyCustomComp=%[8]d + local shellCompDirectiveLegacyCustomArgsComp=%[9]d local out requestComp lastParam lastChar comp directive args flagPrefix @@ -133,6 +139,36 @@ __%[1]s_perform_completion() __%[1]s_debug "Listing directories in ." _filedir -d fi + elif [ $((directive & shellCompDirectiveLegacyCustomComp)) -ne 0 ]; then + local cmd + __%[1]s_debug "Legacy custom completion. Directive: $directive, cmds: ${out[*]}" + + # The following variables should get their value through the commands + # we have received as completions and are parsing below. + local last_command + local nouns + + # Execute every command received + while IFS='' read -r cmd; do + __%[1]s_debug "About to execute: $cmd" + eval "$cmd" + done < <(printf "%%s\n" "${out[@]}") + + __%[1]s_debug "last_command: $last_command" + __%[1]s_debug "nouns[0]: ${nouns[0]}, nouns[1]: ${nouns[1]}" + + if [ $((directive & shellCompDirectiveLegacyCustomArgsComp)) -ne 0 ]; then + # We should call the global legacy custom completion function, if it is defined + if declare -F __%[1]s_custom_func >/dev/null; then + # Use command name qualified legacy custom func + __%[1]s_debug "About to call: __%[1]s_custom_func" + __%[1]s_custom_func + elif declare -F __custom_func >/dev/null; then + # Otherwise fall back to unqualified legacy custom func for compatibility + __%[1]s_debug "About to call: __custom_func" + __custom_func + fi + fi else local tab tab=$(printf '\t') @@ -259,7 +295,8 @@ fi # ex: ts=4 sw=4 et filetype=sh `, name, compCmd, ShellCompDirectiveError, ShellCompDirectiveNoSpace, ShellCompDirectiveNoFileComp, - ShellCompDirectiveFilterFileExt, ShellCompDirectiveFilterDirs)) + ShellCompDirectiveFilterFileExt, ShellCompDirectiveFilterDirs, + shellCompDirectiveLegacyCustomComp, shellCompDirectiveLegacyCustomArgsComp)) } // GenBashCompletionFileV2 generates Bash completion version 2. diff --git a/custom_completions.go b/custom_completions.go index c25c03e407..b23fd87263 100644 --- a/custom_completions.go +++ b/custom_completions.go @@ -51,6 +51,11 @@ const ( // obtain the same behavior but only for flags. ShellCompDirectiveFilterDirs + // For internal use only. + // Used to maintain backwards-compatibility with the legacy bash custom completions. + shellCompDirectiveLegacyCustomComp + shellCompDirectiveLegacyCustomArgsComp + // =========================================================================== // All directives using iota should be above this one. @@ -94,6 +99,12 @@ func (d ShellCompDirective) string() string { if d&ShellCompDirectiveFilterDirs != 0 { directives = append(directives, "ShellCompDirectiveFilterDirs") } + if d&shellCompDirectiveLegacyCustomComp != 0 { + directives = append(directives, "shellCompDirectiveLegacyCustomComp") + } + if d&shellCompDirectiveLegacyCustomArgsComp != 0 { + directives = append(directives, "shellCompDirectiveLegacyCustomArgsComp") + } if len(directives) == 0 { directives = append(directives, "ShellCompDirectiveDefault") } @@ -344,6 +355,10 @@ func (c *Command) getCompletions(args []string) (*Command, []string, ShellCompDi var comps []string comps, directive = completionFn(finalCmd, finalArgs, toComplete) completions = append(completions, comps...) + } else { + // If there is no Go custom completion defined, check for legacy bash + // custom completion to preserve backwards-compatibility + completions, directive = checkLegacyCustomCompletion(finalCmd, finalArgs, flag, completions, directive) } return finalCmd, completions, directive, nil @@ -495,6 +510,61 @@ func findFlag(cmd *Command, name string) *pflag.Flag { return cmd.Flag(name) } +// This function checks if legacy bash custom completion should be performed and if so, +// it provides the shell script with the necessary information. +func checkLegacyCustomCompletion(cmd *Command, args []string, flag *pflag.Flag, completions []string, directive ShellCompDirective) ([]string, ShellCompDirective) { + // Check if any legacy custom completion is defined for the program + if len(cmd.Root().BashCompletionFunction) > 0 { + // Legacy custom completion is only triggered if no other completions were found. + if len(completions) == 0 { + if flag != nil { + // For legacy custom flag completion, we must let the script know the bash + // functions it should call based on the content of the annotation BashCompCustom. + if values, present := flag.Annotations[BashCompCustom]; present { + if len(values) > 0 { + handlers := strings.Join(values, "; ") + // We send the commands to set the shell variables that are needed + // for legacy custom completions followed by the functions to call + // to perform the actual flag completion + completions = append(prepareLegacyCustomCompletionVars(cmd, args), handlers) + directive = directive | shellCompDirectiveLegacyCustomComp + } + } + } else { + // Check if the legacy custom_func is defined. + // This check will work for both "__custom_func" and "___custom_func". + // This could happen if the program defined some functions for legacy flag completion + // but not the legacy custom_func. + if strings.Contains(cmd.Root().BashCompletionFunction, "_custom_func") { + // For legacy args completion, the script already knows what to call + // so we only need to tell it the commands to set the shell variables needed + completions = prepareLegacyCustomCompletionVars(cmd, args) + directive = directive | shellCompDirectiveLegacyCustomComp | shellCompDirectiveLegacyCustomArgsComp + } + } + } + } + return completions, directive +} + +// The original bash completion script had some shell variables that are used by legacy bash +// custom completions. Let's set those variables to allow those legacy custom completions +// to continue working. +func prepareLegacyCustomCompletionVars(cmd *Command, args []string) []string { + var compVarCmds []string + + // "last_command" variable + commandName := cmd.CommandPath() + commandName = strings.Replace(commandName, " ", "_", -1) + commandName = strings.Replace(commandName, ":", "__", -1) + compVarCmds = append(compVarCmds, fmt.Sprintf("last_command=%s", commandName)) + + // "nouns" array variable + compVarCmds = append(compVarCmds, fmt.Sprintf("nouns=(%s)", strings.Join(args, " "))) + + return compVarCmds +} + // CompDebug prints the specified string to the same file as where the // completion script prints its logs. // Note that completion printouts should never be on stdout as they would diff --git a/custom_completions_test.go b/custom_completions_test.go index 276b8a77ba..b73f0b9082 100644 --- a/custom_completions_test.go +++ b/custom_completions_test.go @@ -34,6 +34,28 @@ func validArgsFunc2(cmd *Command, args []string, toComplete string) ([]string, S return completions, ShellCompDirectiveDefault } +// Check that directives are printed properly +func TestPrintDirectives(t *testing.T) { + directive := ShellCompDirectiveFilterDirs | ShellCompDirectiveNoFileComp | ShellCompDirectiveNoSpace + dirStr := directive.string() + if !strings.Contains(dirStr, "ShellCompDirectiveFilterDirs") || + !strings.Contains(dirStr, "ShellCompDirectiveNoFileComp") || + !strings.Contains(dirStr, "ShellCompDirectiveNoSpace") { + t.Errorf("ShellCompdirective.string() printed %s which is wrong for directive %d.", dirStr, directive) + } +} + +// Make sure that if a new ShellCompDirective is added, we don't forget to +// add it to ShellCompdirective.Print() +func TestPrintDirectivesMissingDir(t *testing.T) { + for dirInt := 1; dirInt < int(shellCompDirectiveMaxValue); dirInt *= 2 { + directive := ShellCompDirective(dirInt) + if strings.Contains(directive.string(), "ShellCompDirectiveDefault") { + t.Errorf("Function ShellCompdirective.sstring() is not handling directive %d", directive) + } + } +} + func TestCmdNameCompletionInGo(t *testing.T) { rootCmd := &Command{ Use: "root", @@ -1898,3 +1920,237 @@ func TestCompleteHelp(t *testing.T) { t.Errorf("expected: %q, got: %q", expected, output) } } +func TestLegacyCustomCompArgs(t *testing.T) { + rootCmd := &Command{ + Use: "root", + Args: NoArgs, + Run: emptyRun, + BashCompletionFunction: "__root_custom_func() {}", + } + child1Cmd := &Command{ + Use: "child1", + Run: emptyRun, + } + child2Cmd := &Command{ + Use: "child2", + Run: emptyRun, + } + rootCmd.AddCommand(child1Cmd) + child1Cmd.AddCommand(child2Cmd) + + // Test that completion handles legacy custom args completion + output, err := executeCommand(rootCmd, ShellCompNoDescRequestCmd, "child1", "child2", "") + if err != nil { + t.Errorf("Unexpected error: %v", err) + } + + expected := strings.Join([]string{ + "last_command=root_child1_child2", + "nouns=()", + ":96", + "Completion ended with directive: shellCompDirectiveLegacyCustomComp, shellCompDirectiveLegacyCustomArgsComp", ""}, "\n") + + if output != expected { + t.Errorf("expected: %q, got: %q", expected, output) + } + + // Test that completion handles legacy custom args completion with arguments + output, err = executeCommand(rootCmd, ShellCompNoDescRequestCmd, "child1", "child2", "arg1", "arg2", "") + if err != nil { + t.Errorf("Unexpected error: %v", err) + } + + expected = strings.Join([]string{ + "last_command=root_child1_child2", + "nouns=(arg1 arg2)", + ":96", + "Completion ended with directive: shellCompDirectiveLegacyCustomComp, shellCompDirectiveLegacyCustomArgsComp", ""}, "\n") + + if output != expected { + t.Errorf("expected: %q, got: %q", expected, output) + } + + // Test that completion handles legacy custom args completion with legacy custom function + rootCmd.BashCompletionFunction = "__custom_func() {}" + + output, err = executeCommand(rootCmd, ShellCompNoDescRequestCmd, "child1", "child2", "") + if err != nil { + t.Errorf("Unexpected error: %v", err) + } + + expected = strings.Join([]string{ + "last_command=root_child1_child2", + "nouns=()", + ":96", + "Completion ended with directive: shellCompDirectiveLegacyCustomComp, shellCompDirectiveLegacyCustomArgsComp", ""}, "\n") + + if output != expected { + t.Errorf("expected: %q, got: %q", expected, output) + } +} + +func TestLegacyCustomCompFlag(t *testing.T) { + rootCmd := &Command{ + Use: "root", + Args: NoArgs, + Run: emptyRun, + BashCompletionFunction: "test_legacy_completion_flag_func() {}", + } + child1Cmd := &Command{ + Use: "child1", + Run: emptyRun, + } + child2Cmd := &Command{ + Use: "child2", + Run: emptyRun, + } + rootCmd.AddCommand(child1Cmd) + child1Cmd.AddCommand(child2Cmd) + + child2Cmd.Flags().String("flag", "", "Enter a value") + child2Cmd.MarkFlagCustom("flag", "test_legacy_completion_flag_func") + + // Test that completion handles legacy custom flag completion + output, err := executeCommand(rootCmd, ShellCompNoDescRequestCmd, "child1", "child2", "--flag", "") + if err != nil { + t.Errorf("Unexpected error: %v", err) + } + + expected := strings.Join([]string{ + "last_command=root_child1_child2", + "nouns=()", + "test_legacy_completion_flag_func", + ":32", + "Completion ended with directive: shellCompDirectiveLegacyCustomComp", ""}, "\n") + + if output != expected { + t.Errorf("expected: %q, got: %q", expected, output) + } + + // Test that completion handles legacy custom flag completion with args + output, err = executeCommand(rootCmd, ShellCompNoDescRequestCmd, "child1", "child2", "arg1", "arg2", "--flag", "") + if err != nil { + t.Errorf("Unexpected error: %v", err) + } + + expected = strings.Join([]string{ + "last_command=root_child1_child2", + "nouns=(arg1 arg2)", + "test_legacy_completion_flag_func", + ":32", + "Completion ended with directive: shellCompDirectiveLegacyCustomComp", ""}, "\n") + + if output != expected { + t.Errorf("expected: %q, got: %q", expected, output) + } +} + +func TestNoLegacyCustomCompArgs(t *testing.T) { + rootCmd := &Command{ + Use: "root", + Run: emptyRun, + } + child1Cmd := &Command{ + Use: "child1", + Run: emptyRun, + } + rootCmd.AddCommand(child1Cmd) + + // Test that legacy custom completion is not triggered for args when command.BashCompletionFunction is empty + rootCmd.BashCompletionFunction = "" + + output, err := executeCommand(rootCmd, ShellCompNoDescRequestCmd, "child1", "") + if err != nil { + t.Errorf("Unexpected error: %v", err) + } + + expected := strings.Join([]string{ + ":0", + "Completion ended with directive: ShellCompDirectiveDefault", ""}, "\n") + + if output != expected { + t.Errorf("expected: %q, got: %q", expected, output) + } + + // Test that legacy custom completion is not triggered for args when + // __root_custom_func or __custom_func is not found + rootCmd.BashCompletionFunction = "__root_wrong__func() {}" + + output, err = executeCommand(rootCmd, ShellCompNoDescRequestCmd, "child1", "") + if err != nil { + t.Errorf("Unexpected error: %v", err) + } + + expected = strings.Join([]string{ + ":0", + "Completion ended with directive: ShellCompDirectiveDefault", ""}, "\n") + + if output != expected { + t.Errorf("expected: %q, got: %q", expected, output) + } + + // Test that legacy custom completion is not triggered for args when other completions are found + rootCmd.BashCompletionFunction = "__root_custom_func() {}" + child1Cmd.ValidArgsFunction = func(cmd *Command, args []string, toComplete string) ([]string, ShellCompDirective) { + return []string{"value"}, ShellCompDirectiveDefault + } + + output, err = executeCommand(rootCmd, ShellCompNoDescRequestCmd, "child1", "") + if err != nil { + t.Errorf("Unexpected error: %v", err) + } + + expected = strings.Join([]string{ + "value", + ":0", + "Completion ended with directive: ShellCompDirectiveDefault", ""}, "\n") + + if output != expected { + t.Errorf("expected: %q, got: %q", expected, output) + } +} + +func TestNoLegacyCustomCompFlag(t *testing.T) { + rootCmd := &Command{ + Use: "root", + Run: emptyRun, + } + rootCmd.Flags().String("flag", "", "Enter a value") + rootCmd.MarkFlagCustom("flag", "test_legacy_completion_flag_func") + + // Test that legacy custom completion is not triggered for flags when command.BashCompletionFunction is empty + rootCmd.BashCompletionFunction = "" + + output, err := executeCommand(rootCmd, ShellCompNoDescRequestCmd, "--flag", "") + if err != nil { + t.Errorf("Unexpected error: %v", err) + } + + expected := strings.Join([]string{ + ":0", + "Completion ended with directive: ShellCompDirectiveDefault", ""}, "\n") + + if output != expected { + t.Errorf("expected: %q, got: %q", expected, output) + } + + // Test that legacy custom completion is not triggered for flags when other completions are found + rootCmd.BashCompletionFunction = "test_legacy_completion_flag_func() {}" + rootCmd.RegisterFlagCompletionFunc("flag", func(cmd *Command, args []string, toComplete string) ([]string, ShellCompDirective) { + return []string{"value"}, ShellCompDirectiveDefault + }) + + output, err = executeCommand(rootCmd, ShellCompNoDescRequestCmd, "--flag", "") + if err != nil { + t.Errorf("Unexpected error: %v", err) + } + + expected = strings.Join([]string{ + "value", + ":0", + "Completion ended with directive: ShellCompDirectiveDefault", ""}, "\n") + + if output != expected { + t.Errorf("expected: %q, got: %q", expected, output) + } +} diff --git a/fish_completions.go b/fish_completions.go index 66a6357cb8..d9faaef9ba 100644 --- a/fish_completions.go +++ b/fish_completions.go @@ -110,6 +110,8 @@ function __%[1]s_prepare_completions set shellCompDirectiveNoFileComp %[6]d set shellCompDirectiveFilterFileExt %[7]d set shellCompDirectiveFilterDirs %[8]d + set shellCompDirectiveLegacyCustomComp %[9]d + set shellCompDirectiveLegacyCustomArgsComp %[10]d if test -z "$directive" set directive 0 @@ -123,6 +125,15 @@ function __%[1]s_prepare_completions return 1 end + set legacyCustom (math (math --scale 0 $directive / $shellCompDirectiveLegacyCustomComp) %% 2) + set legacyCustomArgs (math (math --scale 0 $directive / $shellCompDirectiveLegacyCustomArgsComp) %% 2) + if test $legacyCustom -eq 1; or test $legacyCustomArgs -eq 1 + __%[1]s_debug "Legacy bash custom completion not applicable to fish" + # Do full file completion instead + set --global __%[1]s_comp_do_file_comp 1 + return 1 + end + set filefilter (math (math --scale 0 $directive / $shellCompDirectiveFilterFileExt) %% 2) set dirfilter (math (math --scale 0 $directive / $shellCompDirectiveFilterDirs) %% 2) if test $filefilter -eq 1; or test $dirfilter -eq 1 @@ -183,7 +194,8 @@ complete -c %[2]s -n '__%[1]s_prepare_completions' -f -a '$__%[1]s_comp_results' `, nameForVar, name, compCmd, ShellCompDirectiveError, ShellCompDirectiveNoSpace, ShellCompDirectiveNoFileComp, - ShellCompDirectiveFilterFileExt, ShellCompDirectiveFilterDirs)) + ShellCompDirectiveFilterFileExt, ShellCompDirectiveFilterDirs, + shellCompDirectiveLegacyCustomComp, shellCompDirectiveLegacyCustomArgsComp)) } // GenFishCompletion generates fish completion file and writes to the passed writer. diff --git a/zsh_completions.go b/zsh_completions.go index c25ce680ce..38259afae5 100644 --- a/zsh_completions.go +++ b/zsh_completions.go @@ -94,6 +94,8 @@ _%[1]s() local shellCompDirectiveNoFileComp=%[5]d local shellCompDirectiveFilterFileExt=%[6]d local shellCompDirectiveFilterDirs=%[7]d + local shellCompDirectiveLegacyCustomComp=%[8]d + local shellCompDirectiveLegacyCustomArgsComp=%[9]d local lastParam lastChar flagPrefix requestComp out directive compCount comp lastComp local -a completions @@ -163,6 +165,14 @@ _%[1]s() return fi + if [ $((directive & shellCompDirectiveLegacyCustomComp)) -ne 0 ] || + [ $((directive & shellCompDirectiveLegacyCustomArgsComp)) -ne 0 ]; then + __%[1]s_debug "Legacy bash custom completion not applicable to zsh" + # Do file completion instead + _arguments '*:filename:_files'" ${flagPrefix}" + return + fi + compCount=0 while IFS='\n' read -r comp; do if [ -n "$comp" ]; then @@ -231,5 +241,6 @@ _%[1]s() } `, name, compCmd, ShellCompDirectiveError, ShellCompDirectiveNoSpace, ShellCompDirectiveNoFileComp, - ShellCompDirectiveFilterFileExt, ShellCompDirectiveFilterDirs)) + ShellCompDirectiveFilterFileExt, ShellCompDirectiveFilterDirs, + shellCompDirectiveLegacyCustomComp, shellCompDirectiveLegacyCustomArgsComp)) }