bash-completion icon indicating copy to clipboard operation
bash-completion copied to clipboard

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

Open xonixx opened this issue 4 years ago • 1 comments

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 ]]
+ [[ '' == */ ]]

xonixx avatar Feb 01 '22 16:02 xonixx

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.

akinomyoga avatar Feb 01 '22 21:02 akinomyoga