Fix multiple fish completion issues (#1249)

* Fix fish for ShellDirectiveNoSpace and file comp

For fish shell we achieve ShellDirectiveNoSpace by outputing a fake
second completion with an extra character.  However, this extra
character was being added after the description string, instead of
before.  This commit fixes that.

It also cleans up the script of useless code, now that fish completion
details are better understood.

Signed-off-by: Marc Khouzam <marc.khouzam@montreal.ca>

* Handle case when completion starts with a space

Fixes #1303

Signed-off-by: Marc Khouzam <marc.khouzam@montreal.ca>

* Support fish completion with env vars in the path

Fixes https://github.com/spf13/cobra/issues/1214
Fixes https://github.com/spf13/cobra/issues/1306

Signed-off-by: Marc Khouzam <marc.khouzam@montreal.ca>

* Update based on review

1- We use `set -l` for local variable to make sure there are no
   conflicts with global variables
2- We use `commandline -opc` which:
   a) splits the command line into tokens (-o)
   b) only considers the current command (-p) (e.g., echo hello; helm <TAB>)
   c) stops at the cursor (-c)
3- We extract the last arg with `commandline -ct` and escape it to handle
   the case where it is a space, or unmatched quote.
4- We avoid looping when filtering on prefix.
5- We don't add a fake comp for ShellCompDirectiveNoSpace when the
   completion ends with any of @=/:., as fish won't add a space

Signed-off-by: Marc Khouzam <marc.khouzam@montreal.ca>
This commit is contained in:
Marc Khouzam 2021-05-03 14:00:01 -04:00 committed by GitHub
parent 95d23d24ff
commit c2e21bdc10
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23

View file

@ -21,38 +21,27 @@ func genFishComp(buf io.StringWriter, name string, includeDesc bool) {
WriteStringAndCheck(buf, fmt.Sprintf("# fish completion for %-36s -*- shell-script -*-\n", name)) WriteStringAndCheck(buf, fmt.Sprintf("# fish completion for %-36s -*- shell-script -*-\n", name))
WriteStringAndCheck(buf, fmt.Sprintf(` WriteStringAndCheck(buf, fmt.Sprintf(`
function __%[1]s_debug function __%[1]s_debug
set file "$BASH_COMP_DEBUG_FILE" set -l file "$BASH_COMP_DEBUG_FILE"
if test -n "$file" if test -n "$file"
echo "$argv" >> $file echo "$argv" >> $file
end end
end end
function __%[1]s_perform_completion function __%[1]s_perform_completion
__%[1]s_debug "Starting __%[1]s_perform_completion with: $argv" __%[1]s_debug "Starting __%[1]s_perform_completion"
set args (string split -- " " "$argv") # Extract all args except the last one
set lastArg "$args[-1]" set -l args (commandline -opc)
# Extract the last arg and escape it in case it is a space
set -l lastArg (string escape -- (commandline -ct))
__%[1]s_debug "args: $args" __%[1]s_debug "args: $args"
__%[1]s_debug "last arg: $lastArg" __%[1]s_debug "last arg: $lastArg"
set emptyArg "" set -l requestComp "$args[1] %[3]s $args[2..-1] $lastArg"
if test -z "$lastArg"
__%[1]s_debug "Setting emptyArg"
set emptyArg \"\"
end
__%[1]s_debug "emptyArg: $emptyArg"
if not type -q "$args[1]"
# This can happen when "complete --do-complete %[2]s" is called when running this script.
__%[1]s_debug "Cannot find $args[1]. No completions."
return
end
set requestComp "$args[1] %[3]s $args[2..-1] $emptyArg"
__%[1]s_debug "Calling $requestComp" __%[1]s_debug "Calling $requestComp"
set -l results (eval $requestComp 2> /dev/null)
set results (eval $requestComp 2> /dev/null)
# Some programs may output extra empty lines after the directive. # Some programs may output extra empty lines after the directive.
# Let's ignore them or else it will break completion. # Let's ignore them or else it will break completion.
@ -66,12 +55,13 @@ function __%[1]s_perform_completion
break break
end end
end end
set comps $results[1..-2]
set directiveLine $results[-1] set -l comps $results[1..-2]
set -l directiveLine $results[-1]
# For Fish, when completing a flag with an = (e.g., <program> -n=<TAB>) # For Fish, when completing a flag with an = (e.g., <program> -n=<TAB>)
# completions must be prefixed with the flag # completions must be prefixed with the flag
set flagPrefix (string match -r -- '-.*=' "$lastArg") set -l flagPrefix (string match -r -- '-.*=' "$lastArg")
__%[1]s_debug "Comps: $comps" __%[1]s_debug "Comps: $comps"
__%[1]s_debug "DirectiveLine: $directiveLine" __%[1]s_debug "DirectiveLine: $directiveLine"
@ -84,115 +74,124 @@ function __%[1]s_perform_completion
printf "%%s\n" "$directiveLine" printf "%%s\n" "$directiveLine"
end end
# This function does three things: # This function does two things:
# 1- Obtain the completions and store them in the global __%[1]s_comp_results # - Obtain the completions and store them in the global __%[1]s_comp_results
# 2- Set the __%[1]s_comp_do_file_comp flag if file completion should be performed # - Return false if file completion should be performed
# and unset it otherwise
# 3- Return true if the completion results are not empty
function __%[1]s_prepare_completions function __%[1]s_prepare_completions
__%[1]s_debug ""
__%[1]s_debug "========= starting completion logic =========="
# Start fresh # Start fresh
set --erase __%[1]s_comp_do_file_comp
set --erase __%[1]s_comp_results set --erase __%[1]s_comp_results
# Check if the command-line is already provided. This is useful for testing. set -l results (__%[1]s_perform_completion)
if not set --query __%[1]s_comp_commandLine
# Use the -c flag to allow for completion in the middle of the line
set __%[1]s_comp_commandLine (commandline -c)
end
__%[1]s_debug "commandLine is: $__%[1]s_comp_commandLine"
set results (__%[1]s_perform_completion "$__%[1]s_comp_commandLine")
set --erase __%[1]s_comp_commandLine
__%[1]s_debug "Completion results: $results" __%[1]s_debug "Completion results: $results"
if test -z "$results" if test -z "$results"
__%[1]s_debug "No completion, probably due to a failure" __%[1]s_debug "No completion, probably due to a failure"
# Might as well do file completion, in case it helps # Might as well do file completion, in case it helps
set --global __%[1]s_comp_do_file_comp 1
return 1 return 1
end end
set directive (string sub --start 2 $results[-1]) set -l directive (string sub --start 2 $results[-1])
set --global __%[1]s_comp_results $results[1..-2] set --global __%[1]s_comp_results $results[1..-2]
__%[1]s_debug "Completions are: $__%[1]s_comp_results" __%[1]s_debug "Completions are: $__%[1]s_comp_results"
__%[1]s_debug "Directive is: $directive" __%[1]s_debug "Directive is: $directive"
set shellCompDirectiveError %[4]d set -l shellCompDirectiveError %[4]d
set shellCompDirectiveNoSpace %[5]d set -l shellCompDirectiveNoSpace %[5]d
set shellCompDirectiveNoFileComp %[6]d set -l shellCompDirectiveNoFileComp %[6]d
set shellCompDirectiveFilterFileExt %[7]d set -l shellCompDirectiveFilterFileExt %[7]d
set shellCompDirectiveFilterDirs %[8]d set -l shellCompDirectiveFilterDirs %[8]d
if test -z "$directive" if test -z "$directive"
set directive 0 set directive 0
end end
set compErr (math (math --scale 0 $directive / $shellCompDirectiveError) %% 2) set -l compErr (math (math --scale 0 $directive / $shellCompDirectiveError) %% 2)
if test $compErr -eq 1 if test $compErr -eq 1
__%[1]s_debug "Received error directive: aborting." __%[1]s_debug "Received error directive: aborting."
# Might as well do file completion, in case it helps # Might as well do file completion, in case it helps
set --global __%[1]s_comp_do_file_comp 1
return 1 return 1
end end
set filefilter (math (math --scale 0 $directive / $shellCompDirectiveFilterFileExt) %% 2) set -l filefilter (math (math --scale 0 $directive / $shellCompDirectiveFilterFileExt) %% 2)
set dirfilter (math (math --scale 0 $directive / $shellCompDirectiveFilterDirs) %% 2) set -l dirfilter (math (math --scale 0 $directive / $shellCompDirectiveFilterDirs) %% 2)
if test $filefilter -eq 1; or test $dirfilter -eq 1 if test $filefilter -eq 1; or test $dirfilter -eq 1
__%[1]s_debug "File extension filtering or directory filtering not supported" __%[1]s_debug "File extension filtering or directory filtering not supported"
# Do full file completion instead # Do full file completion instead
set --global __%[1]s_comp_do_file_comp 1
return 1 return 1
end end
set nospace (math (math --scale 0 $directive / $shellCompDirectiveNoSpace) %% 2) set -l nospace (math (math --scale 0 $directive / $shellCompDirectiveNoSpace) %% 2)
set nofiles (math (math --scale 0 $directive / $shellCompDirectiveNoFileComp) %% 2) set -l nofiles (math (math --scale 0 $directive / $shellCompDirectiveNoFileComp) %% 2)
__%[1]s_debug "nospace: $nospace, nofiles: $nofiles" __%[1]s_debug "nospace: $nospace, nofiles: $nofiles"
# If we want to prevent a space, or if file completion is NOT disabled,
# we need to count the number of valid completions.
# To do so, we will filter on prefix as the completions we have received
# may not already be filtered so as to allow fish to match on different
# criteria than the prefix.
if test $nospace -ne 0; or test $nofiles -eq 0
set -l prefix (commandline -t | string escape --style=regex)
__%[1]s_debug "prefix: $prefix"
set -l completions (string match -r -- "^$prefix.*" $__%[1]s_comp_results)
set --global __%[1]s_comp_results $completions
__%[1]s_debug "Filtered completions are: $__%[1]s_comp_results"
# Important not to quote the variable for count to work # Important not to quote the variable for count to work
set numComps (count $__%[1]s_comp_results) set -l numComps (count $__%[1]s_comp_results)
__%[1]s_debug "numComps: $numComps" __%[1]s_debug "numComps: $numComps"
if test $numComps -eq 1; and test $nospace -ne 0 if test $numComps -eq 1; and test $nospace -ne 0
# To support the "nospace" directive we trick the shell # We must first split on \t to get rid of the descriptions to be
# able to check what the actual completion will be.
# We don't need descriptions anyway since there is only a single
# real completion which the shell will expand immediately.
set -l split (string split --max 1 \t $__%[1]s_comp_results[1])
# Fish won't add a space if the completion ends with any
# of the following characters: @=/:.,
set -l lastChar (string sub -s -1 -- $split)
if not string match -r -q "[@=/:.,]" -- "$lastChar"
# In other cases, to support the "nospace" directive we trick the shell
# by outputting an extra, longer completion. # by outputting an extra, longer completion.
__%[1]s_debug "Adding second completion to perform nospace directive" __%[1]s_debug "Adding second completion to perform nospace directive"
set --append __%[1]s_comp_results $__%[1]s_comp_results[1]. set --global __%[1]s_comp_results $split[1] $split[1].
__%[1]s_debug "Completions are now: $__%[1]s_comp_results"
end
end end
if test $numComps -eq 0; and test $nofiles -eq 0 if test $numComps -eq 0; and test $nofiles -eq 0
# To be consistent with bash and zsh, we only trigger file
# completion when there are no other completions
__%[1]s_debug "Requesting file completion" __%[1]s_debug "Requesting file completion"
set --global __%[1]s_comp_do_file_comp 1 return 1
end
end end
# If we don't want file completion, we must return true even if there return 0
# are no completions found. This is because fish will perform the last
# completion command, even if its condition is false, if no other
# completion command was triggered
return (not set --query __%[1]s_comp_do_file_comp)
end end
# Since Fish completions are only loaded once the user triggers them, we trigger them ourselves # Since Fish completions are only loaded once the user triggers them, we trigger them ourselves
# so we can properly delete any completions provided by another script. # so we can properly delete any completions provided by another script.
# The space after the the program name is essential to trigger completion for the program # Only do this if the program can be found, or else fish may print some errors; besides,
# and not completion of the program name itself. # the existing completions will only be loaded if the program can be found.
complete --do-complete "%[2]s " > /dev/null 2>&1 if type -q "%[2]s"
# Using '> /dev/null 2>&1' since '&>' is not supported in older versions of fish. # The space after the program name is essential to trigger completion for the program
# and not completion of the program name itself.
# Also, we use '> /dev/null 2>&1' since '&>' is not supported in older versions of fish.
complete --do-complete "%[2]s " > /dev/null 2>&1
end
# Remove any pre-existing completions for the program since we will be handling all of them. # Remove any pre-existing completions for the program since we will be handling all of them.
complete -c %[2]s -e complete -c %[2]s -e
# The order in which the below two lines are defined is very important so that __%[1]s_prepare_completions # The call to __%[1]s_prepare_completions will setup __%[1]s_comp_results
# is called first. It is __%[1]s_prepare_completions that sets up the __%[1]s_comp_do_file_comp variable. # which provides the program's completion choices.
#
# This completion will be run second as complete commands are added FILO.
# It triggers file completion choices when __%[1]s_comp_do_file_comp is set.
complete -c %[2]s -n 'set --query __%[1]s_comp_do_file_comp'
# This completion will be run first as complete commands are added FILO.
# The call to __%[1]s_prepare_completions will setup both __%[1]s_comp_results and __%[1]s_comp_do_file_comp.
# It provides the program's completion choices.
complete -c %[2]s -n '__%[1]s_prepare_completions' -f -a '$__%[1]s_comp_results' complete -c %[2]s -n '__%[1]s_prepare_completions' -f -a '$__%[1]s_comp_results'
`, nameForVar, name, compCmd, `, nameForVar, name, compCmd,