scop / bash-completion

Programmable completion functions for bash
GNU General Public License v2.0
2.91k stars 380 forks source link

Completion for `make` doesn't work for `-f` when whitespace is present in file name #693

Open xonixx opened 2 years ago

xonixx commented 2 years ago

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)

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 ]]
+ [[ '' == */ ]]
akinomyoga commented 2 years ago

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.