fix(comp): Adapt plugin completion for fish

Completion for the fish shell uses the __complete command not only for
custom completion but also for basic command and flag completion.
For plugins, this requires that both the plugin command be created
(to trigger dynamic completion) and the fake static commands be created.

Signed-off-by: Marc Khouzam <marc.khouzam@montreal.ca>
pull/7690/head
Marc Khouzam 6 years ago
parent f1d76cbcce
commit dd0d1a7506

@ -316,7 +316,7 @@ function __helm_get_completions
set -l compErr (math (math $directive / %[2]d) %% 2) set -l compErr (math (math $directive / %[2]d) %% 2)
if test $compErr -eq 1 if test $compErr -eq 1
__helm_debug "Receive error directive: aborting." __helm_debug "Received error directive: aborting."
return 0 return 0
end end

@ -64,24 +64,6 @@ func loadPlugins(baseCmd *cobra.Command, out io.Writer) {
return return
} }
processParent := func(cmd *cobra.Command, args []string) ([]string, error) {
k, u := manuallyProcessArgs(args)
if err := cmd.Parent().ParseFlags(k); err != nil {
return nil, err
}
return u, nil
}
// If we are dealing with the completion command, we try to load more details about the plugins
// if available, so as to allow for command and flag completion.
// For cases where the __complete command is used for command and flag completion (e.g., Fish completion),
// we also want to load the extra plugin information
subCmd, _, err := baseCmd.Find(os.Args[1:])
if err == nil && (subCmd.Name() == "completion" || subCmd.Name() == completion.CompRequestCmd) {
loadPluginsForCompletion(baseCmd, found)
return
}
// Now we create commands for all of these. // Now we create commands for all of these.
for _, plug := range found { for _, plug := range found {
plug := plug plug := plug
@ -90,33 +72,6 @@ func loadPlugins(baseCmd *cobra.Command, out io.Writer) {
md.Usage = fmt.Sprintf("the %q plugin", md.Name) md.Usage = fmt.Sprintf("the %q plugin", md.Name)
} }
// This function is used to setup the environment for the plugin and then
// call the executable specified by the parameter 'main'
callPluginExecutable := func(cmd *cobra.Command, main string, argv []string, out io.Writer) error {
env := os.Environ()
for k, v := range settings.EnvVars() {
env = append(env, fmt.Sprintf("%s=%s", k, v))
}
prog := exec.Command(main, argv...)
prog.Env = env
prog.Stdin = os.Stdin
prog.Stdout = out
prog.Stderr = os.Stderr
if err := prog.Run(); err != nil {
if eerr, ok := err.(*exec.ExitError); ok {
os.Stderr.Write(eerr.Stderr)
status := eerr.Sys().(syscall.WaitStatus)
return pluginError{
error: errors.Errorf("plugin %q exited with error", md.Name),
code: status.ExitStatus(),
}
}
return err
}
return nil
}
c := &cobra.Command{ c := &cobra.Command{
Use: md.Name, Use: md.Name,
Short: md.Usage, Short: md.Usage,
@ -137,62 +92,59 @@ func loadPlugins(baseCmd *cobra.Command, out io.Writer) {
return errors.Errorf("plugin %q exited with error", md.Name) return errors.Errorf("plugin %q exited with error", md.Name)
} }
return callPluginExecutable(cmd, main, argv, out) return callPluginExecutable(md.Name, main, argv, out)
}, },
// This passes all the flags to the subcommand. // This passes all the flags to the subcommand.
DisableFlagParsing: true, DisableFlagParsing: true,
} }
// TODO: Make sure a command with this name does not already exist.
baseCmd.AddCommand(c)
// Setup dynamic completion for the plugin // For completion, we try to load more details about the plugins so as to allow for command and
completion.RegisterValidArgsFunc(c, func(cmd *cobra.Command, args []string, toComplete string) ([]string, completion.BashCompDirective) { // flag completion of the plugin itself.
u, err := processParent(cmd, args) // This applies for the 'completion' command, which generates the completion script (bash and zsh)
if err != nil { // and for '__complete' command which is used directly for command and flag completion (fish shell).
return nil, completion.BashCompDirectiveError // We also add dynamic completion hooks.
subCmd, _, err := baseCmd.Find(os.Args[1:])
if err == nil && (subCmd.Name() == "completion" || subCmd.Name() == completion.CompRequestCmd) {
loadCompletionForPlugin(c, plug)
} }
// We will call the dynamic completion script of the plugin
main := strings.Join([]string{plug.Dir, pluginDynamicCompletionExecutable}, string(filepath.Separator))
argv := []string{}
if !md.IgnoreFlags {
argv = append(argv, u...)
argv = append(argv, toComplete)
} }
plugin.SetupPluginEnv(settings, md.Name, plug.Dir) }
completion.CompDebugln(fmt.Sprintf("calling %s with args %v", main, argv)) func processParent(cmd *cobra.Command, args []string) ([]string, error) {
buf := new(bytes.Buffer) k, u := manuallyProcessArgs(args)
if err := callPluginExecutable(cmd, main, argv, buf); err != nil { if err := cmd.Parent().ParseFlags(k); err != nil {
return nil, completion.BashCompDirectiveError return nil, err
} }
return u, nil
}
var completions []string // This function is used to setup the environment for the plugin and then
for _, comp := range strings.Split(buf.String(), "\n") { // call the executable specified by the parameter 'main'
// Remove any empty lines func callPluginExecutable(pluginName string, main string, argv []string, out io.Writer) error {
if len(comp) > 0 { env := os.Environ()
completions = append(completions, comp) for k, v := range settings.EnvVars() {
} env = append(env, fmt.Sprintf("%s=%s", k, v))
} }
// Check if the last line of output is of the form :<integer>, which prog := exec.Command(main, argv...)
// indicates the BashCompletionDirective. prog.Env = env
directive := completion.BashCompDirectiveDefault prog.Stdin = os.Stdin
if len(completions) > 0 { prog.Stdout = out
lastLine := completions[len(completions)-1] prog.Stderr = os.Stderr
if len(lastLine) > 1 && lastLine[0] == ':' { if err := prog.Run(); err != nil {
if strInt, err := strconv.Atoi(lastLine[1:]); err == nil { if eerr, ok := err.(*exec.ExitError); ok {
directive = completion.BashCompDirective(strInt) os.Stderr.Write(eerr.Stderr)
completions = completions[:len(completions)-1] status := eerr.Sys().(syscall.WaitStatus)
} return pluginError{
error: errors.Errorf("plugin %q exited with error", pluginName),
code: status.ExitStatus(),
} }
} }
return err
return completions, directive
})
// TODO: Make sure a command with this name does not already exist.
baseCmd.AddCommand(c)
} }
return nil
} }
// manuallyProcessArgs processes an arg array, removing special args. // manuallyProcessArgs processes an arg array, removing special args.
@ -263,35 +215,33 @@ type pluginCommand struct {
Commands []pluginCommand `json:"commands"` Commands []pluginCommand `json:"commands"`
} }
// loadPluginsForCompletion will load and parse any completion.yaml provided by the plugins // loadCompletionForPlugin will load and parse any completion.yaml provided by the plugin
func loadPluginsForCompletion(baseCmd *cobra.Command, plugins []*plugin.Plugin) { // and add the dynamic completion hook to call the optional plugin.complete
for _, plug := range plugins { func loadCompletionForPlugin(pluginCmd *cobra.Command, plugin *plugin.Plugin) {
// Parse the yaml file providing the plugin's subcmds and flags // Parse the yaml file providing the plugin's sub-commands and flags
cmds, err := loadFile(strings.Join( cmds, err := loadFile(strings.Join(
[]string{plug.Dir, pluginStaticCompletionFile}, string(filepath.Separator))) []string{plugin.Dir, pluginStaticCompletionFile}, string(filepath.Separator)))
if err != nil { if err != nil {
// The file could be missing or invalid. Either way, we at least create the command // The file could be missing or invalid. No static completion for this plugin.
// for the plugin name.
if settings.Debug { if settings.Debug {
log.Output(2, fmt.Sprintf("[info] %s\n", err.Error())) log.Output(2, fmt.Sprintf("[info] %s\n", err.Error()))
} }
cmds = &pluginCommand{Name: plug.Metadata.Name} // Continue to setup dynamic completion.
cmds = &pluginCommand{}
} }
// We know what the plugin name must be. // We know what the plugin name must be.
// Let's set it in case the Name field was not specified correctly in the file. // Let's set it in case the very first Name field was not specified correctly in the file,
// This insures that we will at least get the plugin name to complete, even if // or if there was no file at all.
// there is a problem with the completion.yaml file cmds.Name = pluginCmd.Name()
cmds.Name = plug.Metadata.Name
addPluginCommands(baseCmd, cmds) addPluginCommands(plugin, pluginCmd, cmds)
}
} }
// addPluginCommands is a recursive method that adds the different levels // addPluginCommands is a recursive method that adds each different level
// of sub-commands and flags for the plugins that provide such information // of sub-commands and flags for the plugins that have provided such information
func addPluginCommands(baseCmd *cobra.Command, cmds *pluginCommand) { func addPluginCommands(plugin *plugin.Plugin, baseCmd *cobra.Command, cmds *pluginCommand) {
if cmds == nil { if cmds == nil {
return return
} }
@ -304,14 +254,8 @@ func addPluginCommands(baseCmd *cobra.Command, cmds *pluginCommand) {
return return
} }
// Create a fake command just so the completion script will include it baseCmd.Use = cmds.Name
c := &cobra.Command{ baseCmd.ValidArgs = cmds.ValidArgs
Use: cmds.Name,
ValidArgs: cmds.ValidArgs,
// A Run is required for it to be a valid command without subcommands
Run: func(cmd *cobra.Command, args []string) {},
}
baseCmd.AddCommand(c)
// Create fake flags. // Create fake flags.
if len(cmds.Flags) > 0 { if len(cmds.Flags) > 0 {
@ -331,7 +275,7 @@ func addPluginCommands(baseCmd *cobra.Command, cmds *pluginCommand) {
} }
} }
f := c.Flags() f := baseCmd.Flags()
if len(longs) >= len(shorts) { if len(longs) >= len(shorts) {
for i := range longs { for i := range longs {
if i < len(shorts) { if i < len(shorts) {
@ -353,9 +297,25 @@ func addPluginCommands(baseCmd *cobra.Command, cmds *pluginCommand) {
} }
} }
// Setup the same dynamic completion for each plugin sub-command.
// This is because if dynamic completion is triggered, there is a single executable
// to call (plugin.complete), so every sub-commands calls it in the same fashion.
completion.RegisterValidArgsFunc(baseCmd, func(cmd *cobra.Command, args []string, toComplete string) ([]string, completion.BashCompDirective) {
return pluginDynamicComp(plugin, cmd, args, toComplete)
})
// Recursively add any sub-commands // Recursively add any sub-commands
for _, cmd := range cmds.Commands { for _, cmd := range cmds.Commands {
addPluginCommands(c, &cmd) // Create a fake command so that completion can be done for the sub-commands of the plugin
subCmd := &cobra.Command{
// This prevents Cobra from removing the flags. We want to keep the flags to pass them
// to the dynamic completion script of the plugin.
DisableFlagParsing: true,
// A Run is required for it to be a valid command without subcommands
Run: func(cmd *cobra.Command, args []string) {},
}
baseCmd.AddCommand(subCmd)
addPluginCommands(plugin, subCmd, &cmd)
} }
} }
@ -370,3 +330,59 @@ func loadFile(path string) (*pluginCommand, error) {
err = yaml.Unmarshal(b, cmds) err = yaml.Unmarshal(b, cmds)
return cmds, err return cmds, err
} }
// pluginDynamicComp call the plugin.complete script of the plugin (if available)
// to obtain the dynamic completion choices. It must pass all the flags and sub-commands
// specified in the command-line to the plugin.complete executable (except helm's global flags)
func pluginDynamicComp(plug *plugin.Plugin, cmd *cobra.Command, args []string, toComplete string) ([]string, completion.BashCompDirective) {
md := plug.Metadata
u, err := processParent(cmd, args)
if err != nil {
return nil, completion.BashCompDirectiveError
}
// We will call the dynamic completion script of the plugin
main := strings.Join([]string{plug.Dir, pluginDynamicCompletionExecutable}, string(filepath.Separator))
// We must include all sub-commands passed on the command-line.
// To do that, we pass-in the entire CommandPath, except the first two elements
// which are 'helm' and 'pluginName'.
argv := strings.Split(cmd.CommandPath(), " ")[2:]
if !md.IgnoreFlags {
argv = append(argv, u...)
argv = append(argv, toComplete)
}
plugin.SetupPluginEnv(settings, md.Name, plug.Dir)
completion.CompDebugln(fmt.Sprintf("calling %s with args %v", main, argv))
buf := new(bytes.Buffer)
if err := callPluginExecutable(md.Name, main, argv, buf); err != nil {
// The dynamic completion file is optional for a plugin, so this error is ok.
completion.CompDebugln(fmt.Sprintf("Unable to call %s: %v", main, err.Error()))
return nil, completion.BashCompDirectiveDefault
}
var completions []string
for _, comp := range strings.Split(buf.String(), "\n") {
// Remove any empty lines
if len(comp) > 0 {
completions = append(completions, comp)
}
}
// Check if the last line of output is of the form :<integer>, which
// indicates the BashCompletionDirective.
directive := completion.BashCompDirectiveDefault
if len(completions) > 0 {
lastLine := completions[len(completions)-1]
if len(lastLine) > 1 && lastLine[0] == ':' {
if strInt, err := strconv.Atoi(lastLine[1:]); err == nil {
directive = completion.BashCompDirective(strInt)
completions = completions[:len(completions)-1]
}
}
}
return completions, directive
}

@ -267,9 +267,12 @@ func getCompletions(cmd *cobra.Command, args []string) ([]string, BashCompDirect
} }
if flag == nil { if flag == nil {
subCmdExists := false
// Complete subcommand names // Complete subcommand names
for _, subCmd := range finalCmd.Commands() { for _, subCmd := range finalCmd.Commands() {
if subCmd.IsAvailableCommand() && strings.HasPrefix(subCmd.Name(), toComplete) { if subCmd.IsAvailableCommand() {
subCmdExists = true
if strings.HasPrefix(subCmd.Name(), toComplete) {
comp := subCmd.Name() comp := subCmd.Name()
if includeDesc { if includeDesc {
comp = fmt.Sprintf("%s\t%s", comp, subCmd.Short) comp = fmt.Sprintf("%s\t%s", comp, subCmd.Short)
@ -277,19 +280,25 @@ func getCompletions(cmd *cobra.Command, args []string) ([]string, BashCompDirect
completions = append(completions, comp) completions = append(completions, comp)
} }
} }
}
if subCmdExists {
// If there are sub-commands (even if they don't match), we stop completion; we shouldn't
// do any argument completion.
// A specific case where this is important is for plugin completion.
// If we allowed to continue on, plugin dynamic completion would always be triggered
// and would need to be handled by the plugin author.
return completions, BashCompDirectiveNoFileComp, nil
}
// Always complete ValidArgs, even if we are completing a subcommand name. // There are no sub-cmds, check for ValidArgs
// This is for commands that have both subcommands and validArgs.
for _, validArg := range finalCmd.ValidArgs { for _, validArg := range finalCmd.ValidArgs {
if strings.HasPrefix(validArg, toComplete) { if strings.HasPrefix(validArg, toComplete) {
completions = append(completions, validArg) completions = append(completions, validArg)
} }
} }
// Always let the logic continue to add any ValidArgsFunction completions, // Let the logic continue to add any ValidArgsFunction completions.
// even if we already found other completions already. // This is for commands that may choose to specify both ValidArgs and ValidArgsFunction.
// This is for commands that have subcommands and/or validArgs but also
// specify a ValidArgsFunction.
} }
// Parse the flags and extract the arguments to prepare for calling the completion function // Parse the flags and extract the arguments to prepare for calling the completion function
@ -316,7 +325,9 @@ func getCompletions(cmd *cobra.Command, args []string) ([]string, BashCompDirect
} }
completionFn, ok := validArgsFunctions[key] completionFn, ok := validArgsFunctions[key]
if !ok { if !ok {
return completions, BashCompDirectiveDefault, fmt.Errorf("Go custom completion not supported/needed for flag or command: %s", nameStr) CompDebugln(fmt.Sprintf("No dynamic completion registered for flag or command: %s", nameStr))
// No custom completion registered. This is ok.
return completions, BashCompDirectiveDefault, nil
} }
comps, directive := completionFn(finalCmd, finalArgs, toComplete) comps, directive := completionFn(finalCmd, finalArgs, toComplete)

Loading…
Cancel
Save