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

Make target completion doesn't work with subdirectories

Open PhilipRoman opened this issue 3 years ago • 4 comments

Describe the bug

Make target completion doesn't work when target files are in subdirectories.

To reproduce

There are two behaviours that happen here. Sample makefile:

.PHONY: abc/xyz

abc/xyz:
	mkdir -p abc
	date > $@

For clarity, i've marked spaces with ·

  1. When typing make·TAB the result is make·abc/·
  2. When typing make·abc/TAB the result is make·xyz·

Expected behavior

Both described cases should auto complete to make·abc/xyz· because it is unambiguous.

Versions (please complete the following information)

  • [ ] Operating system name/distribution and version: Artix Linux on WSL
  • [ ] bash version: 5.1.8(1)-release
  • [ ] bash-completion version: 2.11 (md5sum completions/make = 84866af8ff59c9e1698f90a8f3a67f22)

Additional context

Debug trace

+++ _on_command_start
[?2004h[38;5;214mᛋ [97;1mmake abc/+ 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
+ (( OPTIND += 1 ))
+ [[ 6 -ge 4 ]]
+ case ${!OPTIND} in
+ vprev=prev
+ (( OPTIND += 1 ))
+ [[ 6 -ge 5 ]]
+ case ${!OPTIND} in
+ vwords=words
+ (( OPTIND += 1 ))
+ [[ 6 -ge 6 ]]
+ case ${!OPTIND} in
+ vcword=cword
+ (( 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='[=<>&]'
+ printf -v cword %s 1
+ [[ -v exclude ]]
+ line='make abc/'
+ (( i = 0, j = 0 ))
+ (( i < 2 ))
+ [[ 0 -gt 0 ]]
+ ref='words[0]'
+ printf -v 'words[0]' %s make
+ line=' abc/'
+ (( i == COMP_CWORD ))
+ (( i++, j++ ))
+ (( i < 2 ))
+ [[ 1 -gt 0 ]]
+ [[ abc/ == +([=<>&]) ]]
+ ref='words[1]'
+ printf -v 'words[1]' %s abc/
+ line=
+ (( i == COMP_CWORD ))
+ printf -v cword %s 1
+ (( i++, j++ ))
+ (( i < 2 ))
+ (( i == COMP_CWORD ))
+ local i cur= index=9 'lead=make abc/'
+ [[ 9 -gt 0 ]]
+ [[ -n make abc/ ]]
+ [[ -n makeabc/ ]]
+ cur='make abc/'
+ (( i = 0 ))
+ (( i <= cword ))
+ [[ 9 -ge 4 ]]
+ [[ make != \m\a\k\e ]]
+ (( i < cword ))
+ local old_size=9
+ cur=' abc/'
+ local new_size=5
+ (( index -= old_size - new_size ))
+ (( ++i ))
+ (( i <= cword ))
+ [[ 5 -ge 4 ]]
+ [[  abc != \a\b\c\/ ]]
+ cur=abc/
+ (( index > 0 ))
+ (( index-- ))
+ [[ 4 -ge 4 ]]
+ [[ abc/ != \a\b\c\/ ]]
+ (( i < cword ))
+ (( ++i ))
+ (( i <= cword ))
+ [[ -n abc/ ]]
+ [[ ! -n abc/ ]]
+ (( index < 0 ))
+ local words cword cur
+ _upvars -a2 words make abc/ -v cword 1 -v cur abc/
+ (( 10 ))
+ (( 10 ))
+ case $1 in
+ [[ -n 2 ]]
+ printf %d 2
+ [[ -n words ]]
+ unset -v words
+ eval 'words=("${@:3:2}")'
++ words=("${@:3:2}")
+ shift 4
+ (( 6 ))
+ case $1 in
+ [[ -n cword ]]
+ unset -v cword
+ eval 'cword="$3"'
++ cword=1
+ shift 3
+ (( 3 ))
+ case $1 in
+ [[ -n cur ]]
+ unset -v cur
+ eval 'cur="$3"'
++ cur=abc/
+ shift 3
+ (( 0 ))
+ [[ -v vcur ]]
+ upvars+=("$vcur")
+ upargs+=(-v $vcur "$cur")
+ [[ -v vcword ]]
+ upvars+=("$vcword")
+ upargs+=(-v $vcword "$cword")
+ [[ -v vprev ]]
+ [[ 1 -ge 1 ]]
+ upvars+=("$vprev")
+ upargs+=(-v $vprev "${words[cword - 1]}")
+ [[ -v vwords ]]
+ upvars+=("$vwords")
+ upargs+=(-a${#words[@]} $vwords ${words+"${words[@]}"})
+ (( 4 ))
+ local cur cword prev words
+ _upvars -v cur abc/ -v cword 1 -v prev make -a2 words make abc/
+ (( 13 ))
+ (( 13 ))
+ case $1 in
+ [[ -n cur ]]
+ unset -v cur
+ eval 'cur="$3"'
++ cur=abc/
+ shift 3
+ (( 10 ))
+ case $1 in
+ [[ -n cword ]]
+ unset -v cword
+ eval 'cword="$3"'
++ cword=1
+ shift 3
+ (( 7 ))
+ case $1 in
+ [[ -n prev ]]
+ unset -v prev
+ eval 'prev="$3"'
++ prev=make
+ shift 3
+ (( 4 ))
+ case $1 in
+ [[ -n 2 ]]
+ printf %d 2
+ [[ -n words ]]
+ unset -v words
+ eval 'words=("${@:3:2}")'
++ words=("${@:3:2}")
+ shift 4
+ (( 0 ))
+ _variables
+ [[ abc/ =~ ^(\$(\{[!#]?)?)([A-Za-z0-9_]*)$ ]]
+ [[ abc/ =~ ^(\$\{[#!]?)([A-Za-z0-9_]*)\[([^]]*)$ ]]
+ [[ abc/ =~ ^\$\{[#!]?[A-Za-z0-9_]*\[.*]$ ]]
+ case ${prev-} in
+ return 1
+ [[ abc/ == @(?([0-9])<|?([0-9&])>?(>)|>&)* ]]
+ [[ make == @(?([0-9])<|?([0-9&])>?(>)|>&) ]]
+ local i skip
+ (( i = 1 ))
+ (( i < 2 ))
+ [[ abc/ == @(?([0-9])<|?([0-9&])>?(>)|>&)* ]]
+ (( i++ ))
+ (( 1 ))
+ (( i < 2 ))
+ (( cword <= 0 ))
+ prev=make
+ [[ -n false ]]
+ _split_longopt
+ [[ abc/ == --?*=* ]]
+ return 1
+ return 0
+ makef_dir=('-C' '.')
+ local makef makef_dir i
+ case $prev in
+ false
+ [[ abc/ == -* ]]
+ [[ abc/ == *=* ]]
+ (( i = 1 ))
+ (( i < 2 ))
+ [[ abc/ == -@(C|-directory) ]]
+ (( i++ ))
+ (( i < 2 ))
+ (( i = 1 ))
+ (( i < 2 ))
+ [[ abc/ == -@(f|-?(make)file) ]]
+ (( i++ ))
+ (( i < 2 ))
+ local mode=--
+ (( COMP_TYPE != 9 ))
+ mode=-d
++ _make_target_extract_script -d abc/
++ local mode=-d
++ shift
++ local prefix=abc/
+++ command sed 's/[][\,.*^$(){}?+|/]/\\&/g'
++ local 'prefix_pat=abc\/'
++ local basename=
++ local dirname_len=4
++ [[ -d == -d ]]
++ local 'output=\2'
++ cat
++ [[ -z abc\/ ]]
++ [[ abc\/ == */ ]]
++ 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
    /^abc\//,/^$/!            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|^\(.\{4\}\)\(.\{0\}[^:/]*/\{0,1\}\)[^:]*:.*$|\2|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
      /^abc\/[^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 -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
    /^abc\//,/^$/!            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|^\(.\{4\}\)\(.\{0\}[^:/]*/\{0,1\}\)[^:]*:.*$|\2|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
      /^abc\/[^a-zA-Z0-9]/d;            # convention for hidden tgt
      h;                                        # hold target
      d;                                        # delete line
    }'
+ [[ -d != -d ]]
xyz 
[C[C[Kset +x && exec 2>/dev/tty
[?2004l
++ _on_command_start
+ set +x

PhilipRoman avatar Jun 09 '21 14:06 PhilipRoman

For clarity, i've marked spaces with ·

  1. When typing make·TAB the result is make·abc/·
  2. When typing make·abc/TAB the result is make·xyz·

I could reproduce 1 but not 2.

  • (md5sum completions/make = 84866af8ff59c9e1698f90a8f3a67f22)

This seems to be completions/make of 1a633f3a551154ac3c70bfdc5263d73d8b3d9f3f

$ git cat-file -p 1a633f3a551154ac3c70bfdc5263d73d8b3d9f3f:completions/make | md5sum
84866af8ff59c9e1698f90a8f3a67f22  -

I tried with this version but still cannot reproduce behavior 2 (with bash 5.1.8 on Fedora 31). This is the diff of the trace (trace1.txt is from @PhilipRoman, trace2.txt is mine):

--- trace1.txt^I2021-06-13 21:58:06.993893157 +0900
+++ trace2.txt^I2021-06-13 21:57:38.461345637 +0900
@@ -1,5 +1,4 @@
-+++ _on_command_start
-�[?2004h�[38;5;214mᛋ �[97;1mmake abc/+ local cur prev words cword split
+$ make abc/+ 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
@@ -174,7 +173,6 @@
 + [[ abc/ =~ ^(\$(\{[!#]?)?)([A-Za-z0-9_]*)$ ]]
 + [[ abc/ =~ ^(\$\{[#!]?)([A-Za-z0-9_]*)\[([^]]*)$ ]]
 + [[ abc/ =~ ^\$\{[#!]?[A-Za-z0-9_]*\[.*]$ ]]
-+ case ${prev-} in
 + return 1
 + [[ abc/ == @(?([0-9])<|?([0-9&])>?(>)|>&)* ]]
 + [[ make == @(?([0-9])<|?([0-9&])>?(>)|>&) ]]
@@ -210,17 +208,16 @@
 + (( i < 2 ))
 + local mode=--
 + (( COMP_TYPE != 9 ))
-+ mode=-d
-++ _make_target_extract_script -d abc/
-++ local mode=-d
+++ _make_target_extract_script -- abc/
+++ local mode=--
 ++ shift
 ++ local prefix=abc/
 +++ command sed 's/[][\,.*^$(){}?+|/]/\\&/g'
 ++ local 'prefix_pat=abc\/'
 ++ local basename=
 ++ local dirname_len=4
-++ [[ -d == -d ]]
-++ local 'output=\2'
+++ [[ -- == -d ]]
+++ local 'output=\1\2'
 ++ cat
 ++ [[ -z abc\/ ]]
 ++ [[ abc\/ == */ ]]
@@ -248,7 +245,7 @@
     /^$/ {                                      # end of target block
       x;                                        # unhold target
       /^$/d;                                    # dont print blanks
-      s|^\(.\{4\}\)\(.\{0\}[^:/]*/\{0,1\}\)[^:]*:.*$|\2|p;
+      s|^\(.\{4\}\)\(.\{0\}[^:/]*/\{0,1\}\)[^:]*:.*$|\1\2|p;
       d;                                        # hide any bugs
     }

@@ -301,7 +298,7 @@
     /^$/ {                                      # end of target block
       x;                                        # unhold target
       /^$/d;                                    # dont print blanks
-      s|^\(.\{4\}\)\(.\{0\}[^:/]*/\{0,1\}\)[^:]*:.*$|\2|p;
+      s|^\(.\{4\}\)\(.\{0\}[^:/]*/\{0,1\}\)[^:]*:.*$|\1\2|p;
       d;                                        # hide any bugs
     }

@@ -329,9 +326,5 @@
       h;                                        # hold target
       d;                                        # delete line
     }'
-+ [[ -d != -d ]]
-����xyz
-�[C�[C�[Kset +x && exec 2>/dev/tty
-�[?2004l
-++ _on_command_start
-+ set +x
++ [[ -- != -d ]]
++ [[ abc/xyz == */ ]]

This implies that COMP_TYPE for tab completions has some other values (different from 9) in @PhilipRoman's environment. What will be shown with the following command?

$ _make_debug() { printf 'COMP_TYPE=%q\n' "$COMP_TYPE" >/dev/tty; }
$ complete -F _make_debug make
$ make abc/TAB

akinomyoga avatar Jun 13 '21 13:06 akinomyoga

This implies that COMP_TYPE for tab completions has some other values (different from 9) in @PhilipRoman's environment. What will be shown with the following command?

$ _make_debug() { printf 'COMP_TYPE=%q\n' "$COMP_TYPE" >/dev/tty; }
$ complete -F _make_debug make
$ make abc/TAB
COMP_TYPE=37

I have show-all-if-ambiguous on and tab is bound to menu-complete. When I reset these settings back to default, everything happens as you described.

PhilipRoman avatar Jun 13 '21 13:06 PhilipRoman

OK. Thank you for the information! I could reproduce the behavior with bind 'set show-all-if-ambiguous on'.

I guess we should also check for COMP_TYPE == 37 (%, menu-complete). I tried to search what the other completions do with COMP_TYPE but found that the only completion that references COMP_TYPE is completions/make. This special behavior for COMP_TYPE != 9 is introduced in commit 39f00f92e52b783e7e9e43aac4b4274cc9dee152 by @code5hot. Maybe @code5hot has comments though he/she seems to be inactive in recent years.

Besides, it is a good suggestion to complete the full path abc/xyz when unambiguous.

akinomyoga avatar Jun 13 '21 13:06 akinomyoga

It seems there's no reply from @code5hot, so I've made a PR #546.

akinomyoga avatar Jun 20 '21 02:06 akinomyoga