Completion for `make` doesn't work for `-f` when whitespace is present in file name
Describe the bug
Completion for make doesn't work for -f when whitespace is present in file name
To reproduce
Completion works:
$ make -f tmp/Makefile
aaa bbb ccc
Completion doesn't work:
$ make -f tmp/Makefile\ with\ spaces.txt
Completion doesn't work:
$ make -f 'tmp/Makefile with spaces.txt'
Expected behavior
Completion yields targets from the referenced makefile with spaces in path
Versions (please complete the following information)
- [x] Operating system name/distribution and version:
- Linux Mint 18.3 Sylvia (Ubuntu 16.04.1)
- [x] bash version,
echo "$BASH_VERSION":- 4.3.48(1)-release
- [x] bash-completion version,
(IFS=.; echo "${BASH_COMPLETION_VERSINFO[*]}"):- this outputs empty string to me but I tested with the latest script from https://github.com/scop/bash-completion/blob/master/completions/make
Additional context
I was working on the bash completion for utility similar to make that needs to auto-complete based on -f/--file provided and decided to check the implementation for make since it's doing similar thing.
Debug trace
+ local cur prev words cword split
+ _init_completion -s
+ local exclude= flag outx errx inx OPTIND=1
+ getopts n:e:o:i:s flag -s
+ case $flag in
+ split=false
+ exclude+==
+ getopts n:e:o:i:s flag -s
+ COMPREPLY=()
+ local 'redir=@(?([0-9])<|?([0-9&])>?(>)|>&)'
+ _get_comp_words_by_ref -n '=<>&' cur prev words cword
+ local exclude flag i OPTIND=1
+ words=()
+ local cur cword words
+ upargs=()
+ upvars=()
+ local upargs upvars vcur vcword vprev vwords
+ getopts c:i:n:p:w: flag -n '=<>&' cur prev words cword
+ case $flag in
+ exclude='=<>&'
+ getopts c:i:n:p:w: flag -n '=<>&' cur prev words cword
+ [[ 6 -ge 3 ]]
+ case ${!OPTIND} in
+ vcur=cur
+ let 'OPTIND += 1'
+ [[ 6 -ge 4 ]]
+ case ${!OPTIND} in
+ vprev=prev
+ let 'OPTIND += 1'
+ [[ 6 -ge 5 ]]
+ case ${!OPTIND} in
+ vwords=words
+ let 'OPTIND += 1'
+ [[ 6 -ge 6 ]]
+ case ${!OPTIND} in
+ vcword=cword
+ let 'OPTIND += 1'
+ [[ 6 -ge 7 ]]
+ __get_cword_at_cursor_by_ref '=<>&' words cword cur
+ words=()
+ local cword words
+ __reassemble_comp_words_by_ref '=<>&' words cword
+ local exclude i j line ref
+ [[ -n =<>& ]]
+ exclude='=<>&'
+ eval cword=3
++ cword=3
+ [[ -n =<>& ]]
+ line='make -f tmp/Makefile\ with\ spaces.txt '
+ (( i=0, j=0 ))
+ (( i < 4 ))
+ [[ 0 -gt 0 ]]
+ ref='words[0]'
+ eval 'words[0]=${!ref}${COMP_WORDS[i]}'
++ words[0]=make
+ line=' -f tmp/Makefile\ with\ spaces.txt '
+ [[ 0 == 3 ]]
+ (( i++, j++ ))
+ (( i < 4 ))
+ [[ 1 -gt 0 ]]
+ [[ -f == +([=<>&]) ]]
+ ref='words[1]'
+ eval 'words[1]=${!ref}${COMP_WORDS[i]}'
++ words[1]=-f
+ line=' tmp/Makefile\ with\ spaces.txt '
+ [[ 1 == 3 ]]
+ (( i++, j++ ))
+ (( i < 4 ))
+ [[ 2 -gt 0 ]]
+ [[ tmp/Makefile\ with\ spaces.txt == +([=<>&]) ]]
+ ref='words[2]'
+ eval 'words[2]=${!ref}${COMP_WORDS[i]}'
++ words[2]='tmp/Makefile\ with\ spaces.txt'
+ line=' '
+ [[ 2 == 3 ]]
+ (( i++, j++ ))
+ (( i < 4 ))
+ [[ 3 -gt 0 ]]
+ [[ '' == +([=<>&]) ]]
+ ref='words[3]'
+ eval 'words[3]=${!ref}${COMP_WORDS[i]}'
++ words[3]=
+ line=' '
+ [[ 3 == 3 ]]
+ eval cword=3
++ cword=3
+ (( i++, j++ ))
+ (( i < 4 ))
+ [[ 4 == 3 ]]
+ local i cur index=39 'lead=make -f tmp/Makefile\ with\ spaces.txt '
+ [[ 39 -gt 0 ]]
+ [[ -n make -f tmp/Makefile\ with\ spaces.txt ]]
+ [[ -n make-ftmp/Makefile\with\spaces.txt ]]
+ cur='make -f tmp/Makefile\ with\ spaces.txt '
+ (( i = 0 ))
+ (( i <= cword ))
+ [[ 39 -ge 4 ]]
+ [[ make != \m\a\k\e ]]
+ [[ 0 -lt 3 ]]
+ local old_size=39
+ cur=' -f tmp/Makefile\ with\ spaces.txt '
+ local new_size=35
+ index=35
+ (( ++i ))
+ (( i <= cword ))
+ [[ 35 -ge 2 ]]
+ [[ - != \-\f ]]
+ cur='-f tmp/Makefile\ with\ spaces.txt '
+ (( index-- ))
+ [[ 34 -ge 2 ]]
+ [[ -f != \-\f ]]
+ [[ 1 -lt 3 ]]
+ local old_size=34
+ cur=' tmp/Makefile\ with\ spaces.txt '
+ local new_size=32
+ index=32
+ (( ++i ))
+ (( i <= cword ))
+ [[ 32 -ge 30 ]]
+ [[ tmp/Makefile\ with\ spaces.tx != \t\m\p\/\M\a\k\e\f\i\l\e\\\ \w\i\t\h\\\ \s\p\a\c\e\s\.\t\x\t ]]
+ cur='tmp/Makefile\ with\ spaces.txt '
+ (( index-- ))
+ [[ 31 -ge 30 ]]
+ [[ tmp/Makefile\ with\ spaces.txt != \t\m\p\/\M\a\k\e\f\i\l\e\\\ \w\i\t\h\\\ \s\p\a\c\e\s\.\t\x\t ]]
+ [[ 2 -lt 3 ]]
+ local old_size=31
+ cur=' '
+ local new_size=1
+ index=1
+ (( ++i ))
+ (( i <= cword ))
+ [[ 1 -ge 0 ]]
+ [[ '' != '' ]]
+ [[ 3 -lt 3 ]]
+ (( ++i ))
+ (( i <= cword ))
+ [[ -n ]]
+ [[ ! -n '' ]]
+ cur=
+ [[ 1 -lt 0 ]]
+ local words cword cur
+ _upvars -a4 words make -f 'tmp/Makefile\ with\ spaces.txt' '' -v cword 3 -v cur ''
+ (( 12 ))
+ (( 12 ))
+ case $1 in
+ [[ -n 4 ]]
+ printf %d 4
+ [[ -n words ]]
+ unset -v words
+ eval 'words=("${@:3:4}")'
++ words=("${@:3:4}")
+ shift 6
+ (( 6 ))
+ case $1 in
+ [[ -n cword ]]
+ unset -v cword
+ eval 'cword="$3"'
++ cword=3
+ shift 3
+ (( 3 ))
+ case $1 in
+ [[ -n cur ]]
+ unset -v cur
+ eval 'cur="$3"'
++ cur=
+ shift 3
+ (( 0 ))
+ [[ -n cur ]]
+ upvars+=("$vcur")
+ upargs+=(-v $vcur "$cur")
+ [[ -n cword ]]
+ upvars+=("$vcword")
+ upargs+=(-v $vcword "$cword")
+ [[ -n prev ]]
+ [[ 3 -ge 1 ]]
+ upvars+=("$vprev")
+ upargs+=(-v $vprev "${words[cword - 1]}")
+ [[ -n words ]]
+ upvars+=("$vwords")
+ upargs+=(-a${#words[@]} $vwords "${words[@]}")
+ (( 4 ))
+ local cur cword prev words
+ _upvars -v cur '' -v cword 3 -v prev 'tmp/Makefile\ with\ spaces.txt' -a4 words make -f 'tmp/Makefile\ with\ spaces.txt' ''
+ (( 15 ))
+ (( 15 ))
+ case $1 in
+ [[ -n cur ]]
+ unset -v cur
+ eval 'cur="$3"'
++ cur=
+ shift 3
+ (( 12 ))
+ case $1 in
+ [[ -n cword ]]
+ unset -v cword
+ eval 'cword="$3"'
++ cword=3
+ shift 3
+ (( 9 ))
+ case $1 in
+ [[ -n prev ]]
+ unset -v prev
+ eval 'prev="$3"'
++ prev='tmp/Makefile\ with\ spaces.txt'
+ shift 3
+ (( 6 ))
+ case $1 in
+ [[ -n 4 ]]
+ printf %d 4
+ [[ -n words ]]
+ unset -v words
+ eval 'words=("${@:3:4}")'
++ words=("${@:3:4}")
+ shift 6
+ (( 0 ))
+ _variables
+ [[ '' =~ ^(\$\{?)([A-Za-z0-9_]*)$ ]]
+ return 1
+ [[ '' == @(?([0-9])<|?([0-9&])>?(>)|>&)* ]]
+ [[ tmp/Makefile\ with\ spaces.txt == @(?([0-9])<|?([0-9&])>?(>)|>&) ]]
+ local i skip
+ (( i=1 ))
+ (( i < 4 ))
+ [[ -f == @(?([0-9])<|?([0-9&])>?(>)|>&)* ]]
+ i=2
+ (( 1 ))
+ (( i < 4 ))
+ [[ tmp/Makefile\ with\ spaces.txt == @(?([0-9])<|?([0-9&])>?(>)|>&)* ]]
+ i=3
+ (( 1 ))
+ (( i < 4 ))
+ [[ '' == @(?([0-9])<|?([0-9&])>?(>)|>&)* ]]
+ i=4
+ (( 1 ))
+ (( i < 4 ))
+ [[ 3 -le 0 ]]
+ prev='tmp/Makefile\ with\ spaces.txt'
+ [[ -n false ]]
+ _split_longopt
+ [[ '' == --?*=* ]]
+ return 1
+ return 0
+ makef_dir=("-C" ".")
+ local makef makef_dir i
+ case $prev in
+ false
+ [[ '' == -* ]]
+ [[ '' == *=* ]]
+ (( i = 1 ))
+ (( i < 4 ))
+ [[ -f == -@(C|-directory) ]]
+ (( i++ ))
+ (( i < 4 ))
+ [[ tmp/Makefile\ with\ spaces.txt == -@(C|-directory) ]]
+ (( i++ ))
+ (( i < 4 ))
+ [[ '' == -@(C|-directory) ]]
+ (( i++ ))
+ (( i < 4 ))
+ (( i = 1 ))
+ (( i < 4 ))
+ [[ -f == -@(f|-?(make)file) ]]
+ eval 'makef=( -f "tmp/Makefile\ with\ spaces.txt" )'
++ makef=(-f "tmp/Makefile\ with\ spaces.txt")
+ break
+ local mode=--
+ (( COMP_TYPE != 9 ))
++ _make_target_extract_script -- ''
++ local mode=--
++ shift
++ local prefix=
+++ command sed 's/[][\,.*^$(){}?+|/]/\\&/g'
+++ sed 's/[][\,.*^$(){}?+|/]/\\&/g'
++ local prefix_pat=
++ local basename=
++ local dirname_len=0
++ local dirname_re
++ (( dirname_len > 0 ))
++ [[ ! -v dirname_re ]]
++ local 'output=\1'
++ cat
++ [[ -z '' ]]
++ cat
++ cat
+ local 'IFS=
' 'script= 1,/^# * Make data base/ d; # skip any makefile output
/^# * Finished Make data base/,/^# * Make data base/{
d; # skip any makefile output
}
/^# * Variables/,/^# * Files/ d; # skip until files section
/^# * Not a target/,/^$/ d; # skip not target blocks
/^/,/^$/! d; # skip anything user dont want
# The stuff above here describes lines that are not
# explicit targets or not targets other than special ones
# The stuff below here decides whether an explicit target
# should be output.
/^# * File is an intermediate prerequisite/ {
s/^.*$//;x; # unhold target
d; # delete line
}
/^$/ { # end of target block
x; # unhold target
/^$/d; # dont print blanks
s|^\(.\{0\}[^:/]*/\{0,1\}\)[^:]*:.*$|\1|p;
d; # hide any bugs
}
# This pattern includes a literal tab character as \t is not a portable
# representation and fails with BSD sed
/^[^# :%]\{1,\}:/ { # found target block
/^\.PHONY:/ d; # special target
/^\.SUFFIXES:/ d; # special target
/^\.DEFAULT:/ d; # special target
/^\.PRECIOUS:/ d; # special target
/^\.INTERMEDIATE:/ d; # special target
/^\.SECONDARY:/ d; # special target
/^\.SECONDEXPANSION:/ d; # special target
/^\.DELETE_ON_ERROR:/ d; # special target
/^\.IGNORE:/ d; # special target
/^\.LOW_RESOLUTION_TIME:/ d; # special target
/^\.SILENT:/ d; # special target
/^\.EXPORT_ALL_VARIABLES:/ d; # special target
/^\.NOTPARALLEL:/ d; # special target
/^\.ONESHELL:/ d; # special target
/^\.POSIX:/ d; # special target
/^\.NOEXPORT:/ d; # special target
/^\.MAKE:/ d; # special target
/^[^a-zA-Z0-9]/d; # convention for hidden tgt
h; # hold target
d; # delete line
}'
+ COMPREPLY=($(LC_ALL=C $1 -npq __BASH_MAKE_COMPLETION__=1 ${makef+"${makef[@]}"} "${makef_dir[@]}" .DEFAULT 2>/dev/null |
command sed -ne "$script"))
++ LC_ALL=C
++ make -npq __BASH_MAKE_COMPLETION__=1 -f 'tmp/Makefile\ with\ spaces.txt' -C . .DEFAULT
++ command sed -ne ' 1,/^# * Make data base/ d; # skip any makefile output
/^# * Finished Make data base/,/^# * Make data base/{
d; # skip any makefile output
}
/^# * Variables/,/^# * Files/ d; # skip until files section
/^# * Not a target/,/^$/ d; # skip not target blocks
/^/,/^$/! d; # skip anything user dont want
# The stuff above here describes lines that are not
# explicit targets or not targets other than special ones
# The stuff below here decides whether an explicit target
# should be output.
/^# * File is an intermediate prerequisite/ {
s/^.*$//;x; # unhold target
d; # delete line
}
/^$/ { # end of target block
x; # unhold target
/^$/d; # dont print blanks
s|^\(.\{0\}[^:/]*/\{0,1\}\)[^:]*:.*$|\1|p;
d; # hide any bugs
}
# This pattern includes a literal tab character as \t is not a portable
# representation and fails with BSD sed
/^[^# :%]\{1,\}:/ { # found target block
/^\.PHONY:/ d; # special target
/^\.SUFFIXES:/ d; # special target
/^\.DEFAULT:/ d; # special target
/^\.PRECIOUS:/ d; # special target
/^\.INTERMEDIATE:/ d; # special target
/^\.SECONDARY:/ d; # special target
/^\.SECONDEXPANSION:/ d; # special target
/^\.DELETE_ON_ERROR:/ d; # special target
/^\.IGNORE:/ d; # special target
/^\.LOW_RESOLUTION_TIME:/ d; # special target
/^\.SILENT:/ d; # special target
/^\.EXPORT_ALL_VARIABLES:/ d; # special target
/^\.NOTPARALLEL:/ d; # special target
/^\.ONESHELL:/ d; # special target
/^\.POSIX:/ d; # special target
/^\.NOEXPORT:/ d; # special target
/^\.MAKE:/ d; # special target
/^[^a-zA-Z0-9]/d; # convention for hidden tgt
h; # hold target
d; # delete line
}'
++ sed -ne ' 1,/^# * Make data base/ d; # skip any makefile output
/^# * Finished Make data base/,/^# * Make data base/{
d; # skip any makefile output
}
/^# * Variables/,/^# * Files/ d; # skip until files section
/^# * Not a target/,/^$/ d; # skip not target blocks
/^/,/^$/! d; # skip anything user dont want
# The stuff above here describes lines that are not
# explicit targets or not targets other than special ones
# The stuff below here decides whether an explicit target
# should be output.
/^# * File is an intermediate prerequisite/ {
s/^.*$//;x; # unhold target
d; # delete line
}
/^$/ { # end of target block
x; # unhold target
/^$/d; # dont print blanks
s|^\(.\{0\}[^:/]*/\{0,1\}\)[^:]*:.*$|\1|p;
d; # hide any bugs
}
# This pattern includes a literal tab character as \t is not a portable
# representation and fails with BSD sed
/^[^# :%]\{1,\}:/ { # found target block
/^\.PHONY:/ d; # special target
/^\.SUFFIXES:/ d; # special target
/^\.DEFAULT:/ d; # special target
/^\.PRECIOUS:/ d; # special target
/^\.INTERMEDIATE:/ d; # special target
/^\.SECONDARY:/ d; # special target
/^\.SECONDEXPANSION:/ d; # special target
/^\.DELETE_ON_ERROR:/ d; # special target
/^\.IGNORE:/ d; # special target
/^\.LOW_RESOLUTION_TIME:/ d; # special target
/^\.SILENT:/ d; # special target
/^\.EXPORT_ALL_VARIABLES:/ d; # special target
/^\.NOTPARALLEL:/ d; # special target
/^\.ONESHELL:/ d; # special target
/^\.POSIX:/ d; # special target
/^\.NOEXPORT:/ d; # special target
/^\.MAKE:/ d; # special target
/^[^a-zA-Z0-9]/d; # convention for hidden tgt
h; # hold target
d; # delete line
}'
+ [[ -- != -d ]]
+ [[ '' == */ ]]
Thank you for the report. This is related to the following line in the code.
https://github.com/scop/bash-completion/blob/7e7928d0e8bf2071e09749e4e005a972acf2aa53/completions/make#L149
This must be
eval "makef=( -f ${words[i + 1]} )"
However, I don't think we should use eval here to begin with. This executes command substitutions in the argument. If the command is incomplete, for example, this may break the user's data.