diff --git a/completions/make b/completions/make index 50fdcf2f76a..8987cb067f7 100644 --- a/completions/make +++ b/completions/make @@ -46,7 +46,7 @@ _make_target_extract_script() /^$/ { # end of target block x; # unhold target /^$/d; # dont print blanks - s|^${dirname_re-}\(.\{${#basename}\}[^:/]*/\{0,1\}\)[^:]*:.*$|${output}|p; + s|^${dirname_re-}\(.\{${#basename}\}[^:]*\):.*$|${output}|p; d; # hide any bugs } @@ -87,6 +87,56 @@ EOF EOF } +# Truncate the non-unique filepaths in COMPRELY to only generate unique +# directories or files. This function discards the files under subdirectories +# unless the path is unique under each subdirectory and instead generate the +# subdirectory path. For example, when there are two candidates, "abc/def" and +# "abc/xyz", we generate "abc/" instead of generating both candidates directly. +# When there is only one candidate "abc/def", we generate the full path +# "abc/def". +# +# @var[in] cur +# @var[in] mode +# @var[in,out] COMPREPLY +_comp_make__truncate_non_unique_paths() +{ + local prefix=$cur + [[ $mode == -d ]] && prefix= + if ((${#COMPREPLY[@]} > 0)); then + # collect the possible completions including the directory names in + # `paths' and count the number of children of each subdirectory in + # `nchild'. + local -A paths nchild + local target + for target in "${COMPREPLY[@]}"; do + local path=${target%/} + while [[ ! ${paths[$path]+set} ]] && + paths[$path]=1 && + [[ $path == "$prefix"*/* ]]; do + path=${path%/*} + nchild[$path]=$((${nchild[$path]-0} + 1)) + done + done + + COMPREPLY=() + local nreply=0 + for target in "${!paths[@]}"; do + # generate only the paths that do not have a unique child and whose + # all parent and ancestor directories have a unique child. + ((${nchild[$target]-0} == 1)) && continue + local path=$target + while [[ $path == "$prefix"*/* ]]; do + path=${path%/*} + ((${nchild[$path]-0} == 1)) || continue 2 + done + + # suffix `/' when the target path is a subdiretory, which has + # at least one child. + COMPREPLY[nreply++]=$target${nchild[$target]+/} + done + fi +} + _make() { local cur prev words cword split comp_args @@ -159,12 +209,19 @@ _make() fi done - # recognise that possible completions are only going to be displayed - # so only the base name is shown + # recognise that possible completions are only going to be displayed so + # only the base name is shown. + # + # Note: This is currently turned off because the test suite of + # bash-completion conflicts with it; it uses "set show-all-if-ambiguous + # on" (causing COMP_TYPE == 37) to retrieve the action completion + # results, and also the compact form with only the basenames is not + # essentially needed. To re-enable it, please uncomment the following + # if-statement. local mode=-- - if ((COMP_TYPE != 9)); then - mode=-d # display-only mode - fi + # if ((COMP_TYPE != 9 && COMP_TYPE != 37 && COMP_TYPE != 42)); then + # mode=-d # display-only mode + # fi local IFS=$' \t\n' script=$(_make_target_extract_script $mode "$cur") COMPREPLY=($(LC_ALL=C \ @@ -172,6 +229,8 @@ _make() ${makef+"${makef[@]}"} "${makef_dir[@]}" .DEFAULT 2>/dev/null | command sed -ne "$script")) + _comp_make__truncate_non_unique_paths + if [[ $mode != -d ]]; then # Completion will occur if there is only one suggestion # so set options for completion based on the first one diff --git a/test/fixtures/make/test2/Makefile b/test/fixtures/make/test2/Makefile new file mode 100644 index 00000000000..835b51440e2 --- /dev/null +++ b/test/fixtures/make/test2/Makefile @@ -0,0 +1,23 @@ +# makefile + +all: abc/xyz +.PHONY: abc/xyz +abc/xyz 123/xaa 123/xbb: + mkdir -p $(@:/%=) + date > $@ + +sub1test/bar/alpha sub1test/bar/beta: + mkdir -p $(@:/%=) + date > $@ + +sub2test/bar/alpha: + mkdir -p $(@:/%=) + date > $@ + +sub3test/bar/alpha sub3test/foo/alpha: + mkdir -p $(@:/%=) + date > $@ + +sub4test/bar/alpha sub4test/bar/beta sub4test2/foo/gamma: + mkdir -p $(@:/%=) + date > $@ diff --git a/test/t/test_make.py b/test/t/test_make.py index aaf5fead14d..0fc630b30bd 100644 --- a/test/t/test_make.py +++ b/test/t/test_make.py @@ -2,6 +2,8 @@ import pytest +from conftest import assert_complete + class TestMake: @pytest.mark.complete("make -f Ma", cwd="make") @@ -16,7 +18,7 @@ def test_2(self, bash, completion): @pytest.mark.complete("make .cache/", cwd="make", require_cmd=True) def test_3(self, bash, completion): - assert completion == "1 2".split() + assert completion == ".cache/1 .cache/2".split() os.remove(f"{bash.cwd}/make/extra_makefile") @pytest.mark.complete("make ", cwd="shared/empty_dir") @@ -34,7 +36,7 @@ def test_6(self, bash, completion): @pytest.mark.complete("make .cache/.", cwd="make", require_cmd=True) def test_7(self, bash, completion): - assert completion == ".1 .2".split() + assert completion == ".cache/.1 .cache/.2".split() os.remove(f"{bash.cwd}/make/extra_makefile") @pytest.mark.complete("make -C make ", require_cmd=True) @@ -45,3 +47,38 @@ def test_8(self, bash, completion): @pytest.mark.complete("make -", require_cmd=True) def test_9(self, completion): assert completion + + +@pytest.mark.bashcomp(require_cmd=True, cwd="make/test2") +class TestMake2: + def test_github_issue_544_1(self, bash): + completion = assert_complete(bash, "make ab") + assert completion == "c/xyz" + + def test_github_issue_544_2(self, bash): + completion = assert_complete(bash, "make 1") + assert completion == "23/" + + def test_github_issue_544_3(self, bash): + completion = assert_complete(bash, "make 123/") + assert completion == ["123/xaa", "123/xbb"] + + def test_github_issue_544_4(self, bash): + completion = assert_complete(bash, "make 123/xa") + assert completion == "a" + + def test_subdir_1(self, bash): + completion = assert_complete(bash, "make sub1") + assert completion == "test/bar/" + + def test_subdir_2(self, bash): + completion = assert_complete(bash, "make sub2") + assert completion == "test/bar/alpha" + + def test_subdir_3(self, bash): + completion = assert_complete(bash, "make sub3") + assert completion == "test/" + + def test_subdir_4(self, bash): + completion = assert_complete(bash, "make sub4") + assert completion == "sub4test/bar/ sub4test2/foo/gamma".split()