From 64630aaeca0a5013e4b438577138f0f707c68cb1 Mon Sep 17 00:00:00 2001 From: Koichi Murase Date: Sun, 20 Jun 2021 11:00:54 +0900 Subject: [PATCH 1/4] test(make): add test cases for GitHub #544 --- test/fixtures/make/test2/Makefile | 23 +++++++++++++++++++ test/t/test_make.py | 37 +++++++++++++++++++++++++++++++ 2 files changed, 60 insertions(+) create mode 100644 test/fixtures/make/test2/Makefile 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..74efb4a9a35 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") @@ -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 == ["xaa", "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() From a1a94f60e90e8e0fc87c398136528e8a7b3b73f9 Mon Sep 17 00:00:00 2001 From: Koichi Murase Date: Sun, 20 Jun 2021 09:32:57 +0900 Subject: [PATCH 2/4] fix(make): do not strip dirnames for COMP_TYPE=37 (%) and 42 (*) --- completions/make | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/completions/make b/completions/make index 50fdcf2f76a..3579802dc5f 100644 --- a/completions/make +++ b/completions/make @@ -162,7 +162,7 @@ _make() # recognise that possible completions are only going to be displayed # so only the base name is shown local mode=-- - if ((COMP_TYPE != 9)); then + if ((COMP_TYPE != 9 && COMP_TYPE != 37 && COMP_TYPE != 42)); then mode=-d # display-only mode fi From 1ea3326e67ae44e3eb46d8501007f27f4b26f2e6 Mon Sep 17 00:00:00 2001 From: Koichi Murase Date: Sun, 20 Jun 2021 09:33:49 +0900 Subject: [PATCH 3/4] fix(make): show unique targets in subdirs When many targets in subdirectories are defined in Makefile, we typically want to list the files or subdirectories in the current directory. However, when the file in the subdirectory is unique, we may generate the full path. See also the discussion on GitHub: https://github.com/scop/bash-completion/issues/544 https://github.com/scop/bash-completion/pull/546#issuecomment-1357674064 --- completions/make | 54 +++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 53 insertions(+), 1 deletion(-) diff --git a/completions/make b/completions/make index 3579802dc5f..9a60162e0e4 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 @@ -172,6 +222,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 From 5eb1042ffcbbcfa7156e2624ff9c6b626a35c25e Mon Sep 17 00:00:00 2001 From: Koichi Murase Date: Sun, 18 Dec 2022 03:03:57 +0900 Subject: [PATCH 4/4] fix(make): deactivate the display-only mode In the display-only mode where COMP_TYPE=9, 37, or 42, only a part of the completion is stored in COMPREPLY for displaying purposes. For example, "xyz" is stored in COMPREPLY when "abc/xyz" is the candidate and "abc/" is already inserted. However, the test framework extracts generated completions using COMP_TYPE=37 by setting "set show-all-if-ambiguous off", which would be broken by the display-only mode. There is no simple way to make it work with the test framework, and the display-only mode does not seem to be essential. For the time being, we deactivate the display-only mode. See also discussions in the following links: https://github.com/scop/bash-completion/issues/544 https://github.com/scop/bash-completion/pull/546 --- completions/make | 17 ++++++++++++----- test/t/test_make.py | 6 +++--- 2 files changed, 15 insertions(+), 8 deletions(-) diff --git a/completions/make b/completions/make index 9a60162e0e4..8987cb067f7 100644 --- a/completions/make +++ b/completions/make @@ -209,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 && COMP_TYPE != 37 && COMP_TYPE != 42)); 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 \ diff --git a/test/t/test_make.py b/test/t/test_make.py index 74efb4a9a35..0fc630b30bd 100644 --- a/test/t/test_make.py +++ b/test/t/test_make.py @@ -18,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") @@ -36,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) @@ -61,7 +61,7 @@ def test_github_issue_544_2(self, bash): def test_github_issue_544_3(self, bash): completion = assert_complete(bash, "make 123/") - assert completion == ["xaa", "xbb"] + assert completion == ["123/xaa", "123/xbb"] def test_github_issue_544_4(self, bash): completion = assert_complete(bash, "make 123/xa")