diff --git a/cmd/helm/load_plugins.go b/cmd/helm/load_plugins.go index 5eb9f40b7..36ef0aec3 100644 --- a/cmd/helm/load_plugins.go +++ b/cmd/helm/load_plugins.go @@ -33,7 +33,11 @@ import ( "helm.sh/helm/v3/pkg/plugin" ) -const pluginStaticCompletionFile = "completion.yaml" +const ( + pluginStaticCompletionFile = "completion.yaml" + pluginDynamicCompletionExecutable = "plugin.complete" + pluginDynamicCompletionIndicator = "__complete__" +) type pluginError struct { error @@ -106,6 +110,9 @@ func loadPlugins(baseCmd *cobra.Command, out io.Writer) { env = append(env, fmt.Sprintf("%s=%s", k, v)) } + // Check if the plugin has been called to provide completions + main, argv = checkForCompletionRequest(plug, main, argv) + prog := exec.Command(main, argv...) prog.Env = env prog.Stdin = os.Stdin @@ -305,3 +312,18 @@ func loadFile(path string) (*pluginCommand, error) { err = yaml.Unmarshal(b, cmds) return cmds, err } + +func checkForCompletionRequest(plug *plugin.Plugin, main string, args []string) (string, []string) { + if len(args) > 0 && args[len(args)-1] == pluginDynamicCompletionIndicator { + // The plugin was called to request completion choices. + // Call the dynamic completion script of the plugin instead of the main plugin script + newMain := strings.Join([]string{plug.Dir, pluginDynamicCompletionExecutable}, string(filepath.Separator)) + // Remove the temporary pluginDynamicCompletionIndicator parameter + newArgs := args[:len(args)-1] + if settings.Debug { + log.Output(2, fmt.Sprintf("[info] calling %s with args %v\n", newMain, newArgs)) + } + return newMain, newArgs + } + return main, args +} diff --git a/cmd/helm/plugin_test.go b/cmd/helm/plugin_test.go index 2d9a24ba9..acc154739 100644 --- a/cmd/helm/plugin_test.go +++ b/cmd/helm/plugin_test.go @@ -242,6 +242,34 @@ func checkCommand(t *testing.T, plugins []*cobra.Command, tests []staticCompleti } } +func TestExecuteCompletionCall(t *testing.T) { + settings.PluginsDirectory = "testdata/helmhome/helm/plugins" + + var ( + out bytes.Buffer + cmd cobra.Command + ) + loadPlugins(&cmd, &out) + + pluginCmd, _, err := cmd.Find([]string{"env"}) + if err != nil { + t.Errorf("Unexpected error %s", err) + } + + // Currently, plugins assume a Linux subsystem. Skip the execution + // tests until this is fixed + if runtime.GOOS != "windows" { + if err := pluginCmd.RunE(pluginCmd, []string{pluginDynamicCompletionIndicator}); err == nil { + expected := "plugin.complete was called\n" + if out.String() != expected { + t.Errorf("Expected %s to output:\n%s\ngot\n%s", pluginCmd.Name(), expected, out.String()) + } + } else { + t.Errorf("Unexpected error %s", err.Error()) + } + } +} + func TestLoadPlugins_HelmNoPlugins(t *testing.T) { settings.PluginsDirectory = "testdata/helmhome/helm/plugins" settings.RepositoryConfig = "testdata/helmhome/helm/repository" diff --git a/cmd/helm/root.go b/cmd/helm/root.go index f0a034d45..ef21b1be3 100644 --- a/cmd/helm/root.go +++ b/cmd/helm/root.go @@ -257,6 +257,35 @@ __helm_list_releases_then_charts() { fi } +__helm_call_plugin_completion() +{ + __helm_debug "${FUNCNAME[0]}: c is $c #words is ${#words[@]} words[@] is ${words[@]}" + __helm_debug "${FUNCNAME[0]}: last_command is $last_command" + + # Need to split the following into two operations for bash 3 + local pluginName=${last_command#*_} + pluginName=${pluginName%%%%_*} + + # We double cut. The first is to separate by tabs, and the second trims trailing whitespaces. + if ! eval $(__helm_binary_name) plugin list 2>/dev/null | tail +2 | cut -f1 | cut -f1 -d" " | \grep -q ^${pluginName}$; then + # Not a plugin. + return + fi + + # If the last parameter is complete (there is a space following it), we + # add an extra fake parameter at the end to indicate this to the plugin. + # For example "helm 2to3 convert " should complete differently than "helm 2to3 con" + extraParam="__complete__" + [ $c -eq ${#words[@]} ] && extraParam="_ $extraParam" + + toExec="${words[@]} $extraParam" + __helm_debug "${FUNCNAME[0]}: Executing the following to get plugin completions: $toExec" + if out=$(eval $toExec 2>/dev/null); then + __helm_debug "${FUNCNAME[0]}: plugin returned: ${out[*]}" + COMPREPLY+=( $( compgen -W "${out[*]}" -- "$cur" ) ) + fi +} + __helm_custom_func() { __helm_debug "${FUNCNAME[0]}: last_command is $last_command" @@ -291,6 +320,8 @@ __helm_custom_func() return ;; *) + __helm_call_plugin_completion + return ;; esac } diff --git a/cmd/helm/testdata/helmhome/helm/plugins/env/plugin.complete b/cmd/helm/testdata/helmhome/helm/plugins/env/plugin.complete new file mode 100755 index 000000000..33fe6c3c1 --- /dev/null +++ b/cmd/helm/testdata/helmhome/helm/plugins/env/plugin.complete @@ -0,0 +1,3 @@ +#!/usr/bin/env sh + +echo "plugin.complete was called"