Skip to content

Commit

Permalink
Bash comp v2 backwards-compatibility
Browse files Browse the repository at this point in the history
Signed-off-by: Marc Khouzam <marc.khouzam@montreal.ca>
  • Loading branch information
marckhouzam committed Aug 11, 2020
1 parent 2b05a51 commit 93f3645
Show file tree
Hide file tree
Showing 5 changed files with 389 additions and 3 deletions.
39 changes: 38 additions & 1 deletion bash_completionsV2.go
Expand Up @@ -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
}
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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')
Expand Down Expand Up @@ -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.
Expand Down
70 changes: 70 additions & 0 deletions custom_completions.go
Expand Up @@ -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.
Expand Down Expand Up @@ -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")
}
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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 "__<program>_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
Expand Down

0 comments on commit 93f3645

Please sign in to comment.