feat(comp): Dynamic completion for plugins

If the completion script determines there are no known completions
it will look for a plugin that matches the command line.  If found,
the completion script will call the plugin through helm with the
special __complete__ parameter.  Helm will detect that special
parameter and then call an executable called plugin.complete which is
optionally provided by the plugin in its root directory.

Helm will set the plugin environment variable and pass the entire
current command line to that script file.
That executable file should output the possible completions
based on the command-line it received.

Signed-off-by: Marc Khouzam <marc.khouzam@montreal.ca>
pull/7078/head
Marc Khouzam 6 years ago
parent 3d1d5a67b0
commit edcc798484

@ -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
}

@ -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"

@ -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 <TAB>" should complete differently than "helm 2to3 con<TAB>"
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
}

@ -0,0 +1,3 @@
#!/usr/bin/env sh
echo "plugin.complete was called"
Loading…
Cancel
Save