From e1a70c66433788e2d9d08e89d16329bb2fb340b9 Mon Sep 17 00:00:00 2001 From: Koichi Murase Date: Tue, 18 Jun 2024 09:01:38 +0900 Subject: [PATCH 1/2] fix: source files using absolute paths for absolute BASH_SOURCE Some completion functions use BASH_SOURCE to identify the path to the file where the functions are defined. However, if the file was sourced with the relative path (e.g. `. ./completions/make`), BASH_SOURCE referenced by the function contains the relative path. This causes the problem after the current working directory is changed from the one where the file was sourced. To make BASH_SOURCE available to the completion files, we should replace a relative path to the absolute path before passing the path to `source` or `.`. To supply the absolute path, we add a new global variable `_comp__base_directory`, which contains the directory where `bash_completion` is located. --- bash_completion | 26 ++++++++++++++++---------- 1 file changed, 16 insertions(+), 10 deletions(-) diff --git a/bash_completion b/bash_completion index bf42e00885b..428a5043a9d 100644 --- a/bash_completion +++ b/bash_completion @@ -3125,6 +3125,18 @@ _comp_complete_minimal() # https://lists.gnu.org/archive/html/bug-bash/2012-01/msg00045.html complete -F _comp_complete_minimal '' +# Initialize the variable "_comp__base_directory" +# @var[out] _comp__base_directory +_comp__init_base_directory() +{ + local REPLY + _comp_abspath "${BASH_SOURCE[0]-./bash_completion}" + _comp__base_directory=${REPLY%/*} + [[ $_comp__base_directory ]] || _comp__base_directory=/ + unset -f "$FUNCNAME" +} +_comp__init_base_directory + # @since 2.12 _comp_load() { @@ -3177,11 +3189,7 @@ _comp_load() # we want to prefer in-tree completions over ones possibly coming with a # system installed bash-completion. (Due to usual install layouts, this # often hits the correct completions in system installations, too.) - if [[ $BASH_SOURCE == */* ]]; then - dirs+=("${BASH_SOURCE%/*}/completions") - else - dirs+=(./completions) - fi + dirs+=("$_comp__base_directory/completions") # 3) From bin directories extracted from the specified path to the command, # the real path to the command, and $PATH @@ -3323,12 +3331,10 @@ _comp__init_collect_startup_configs() # run-in-place-from-git-clone setups. Notably we do it after the # system location here, in order to prefer in-tree variables and # functions. - if [[ ${base_path%/*} == */share/bash-completion ]]; then - compat_dir=${base_path%/share/bash-completion/*}/etc/bash_completion.d - elif [[ $base_path == */* ]]; then - compat_dir="${base_path%/*}/bash_completion.d" + if [[ $_comp__base_directory == */share/bash-completion ]]; then + compat_dir=${_comp__base_directory%/share/bash-completion}/etc/bash_completion.d else - compat_dir=./bash_completion.d + compat_dir=$_comp__base_directory/bash_completion.d fi [[ ${compat_dirs[0]} == "$compat_dir" ]] || compat_dirs+=("$compat_dir") From d599dcfb7f33146baa143d99067c7e71d2992ee6 Mon Sep 17 00:00:00 2001 From: Koichi Murase Date: Tue, 18 Jun 2024 20:01:49 +0900 Subject: [PATCH 2/2] feat(_comp_abspath): handle ".." --- bash_completion | 23 ++++-- test/t/unit/test_unit_abspath.py | 130 ++++++++++++++++++++++++++++++- 2 files changed, 143 insertions(+), 10 deletions(-) diff --git a/bash_completion b/bash_completion index 428a5043a9d..8d2030250cd 100644 --- a/bash_completion +++ b/bash_completion @@ -2181,15 +2181,22 @@ _comp_compgen_fstypes() _comp_abspath() { REPLY=$1 - case $REPLY in - /*) ;; - ../*) REPLY=$PWD/${REPLY:3} ;; - *) REPLY=$PWD/$REPLY ;; - esac - while [[ $REPLY == */./* ]]; do - REPLY=${REPLY//\/.\//\/} - done + [[ $REPLY == /* ]] || REPLY=$PWD/$REPLY REPLY=${REPLY//+(\/)/\/} + while true; do + # Process "." and "..". To avoid reducing "/../../ => /", we convert + # "/*/../" one by one. "/.." at the beginning is ignored. Then, /*/../ + # in the middle is processed. Finally, /*/.. at the end is removed. + case $REPLY in + */./*) REPLY=${REPLY//\/.\//\/} ;; + */.) REPLY=${REPLY%/.} ;; + /..?(/*)) REPLY=${REPLY#/..} ;; + */+([^/])/../*) REPLY=${REPLY/\/+([^\/])\/..\//\/} ;; + */+([^/])/..) REPLY=${REPLY%/+([^/])/..} ;; + *) break ;; + esac + done + [[ $REPLY ]] || REPLY=/ } # Get real command. diff --git a/test/t/unit/test_unit_abspath.py b/test/t/unit/test_unit_abspath.py index 97d506cce3a..88c9f6b5d32 100644 --- a/test/t/unit/test_unit_abspath.py +++ b/test/t/unit/test_unit_abspath.py @@ -46,7 +46,7 @@ def test_relative(self, bash, functions): ) assert output.strip().endswith("/shared/foo/bar") - def test_cwd(self, bash, functions): + def test_cwd1(self, bash, functions): output = assert_bash_exec( bash, "__tester ./foo/./bar", @@ -55,7 +55,34 @@ def test_cwd(self, bash, functions): ) assert output.strip().endswith("/shared/foo/bar") - def test_parent(self, bash, functions): + def test_cwd2(self, bash, functions): + output = assert_bash_exec( + bash, + "__tester /.", + want_output=True, + want_newline=False, + ) + assert output.strip() == "/" + + def test_cwd3(self, bash, functions): + output = assert_bash_exec( + bash, + "__tester /foo/.", + want_output=True, + want_newline=False, + ) + assert output.strip() == "/foo" + + def test_cwd4(self, bash, functions): + output = assert_bash_exec( + bash, + "__tester /././.", + want_output=True, + want_newline=False, + ) + assert output.strip() == "/" + + def test_parent1(self, bash, functions): output = assert_bash_exec( bash, "__tester ../shared/foo/bar", @@ -65,3 +92,102 @@ def test_parent(self, bash, functions): assert output.strip().endswith( "/shared/foo/bar" ) and not output.strip().endswith("../shared/foo/bar") + + def test_parent2(self, bash, functions): + output = assert_bash_exec( + bash, + "__tester /foo/..", + want_output=True, + want_newline=False, + ) + assert output.strip() == "/" + + def test_parent3(self, bash, functions): + output = assert_bash_exec( + bash, + "__tester /..", + want_output=True, + want_newline=False, + ) + assert output.strip() == "/" + + def test_parent4(self, bash, functions): + output = assert_bash_exec( + bash, + "__tester /../foo/bar", + want_output=True, + want_newline=False, + ) + assert output.strip() == "/foo/bar" + + def test_parent5(self, bash, functions): + output = assert_bash_exec( + bash, + "__tester /../../foo/bar", + want_output=True, + want_newline=False, + ) + assert output.strip() == "/foo/bar" + + def test_parent6(self, bash, functions): + output = assert_bash_exec( + bash, + "__tester /foo/../bar", + want_output=True, + want_newline=False, + ) + assert output.strip() == "/bar" + + def test_parent7(self, bash, functions): + output = assert_bash_exec( + bash, + "__tester /foo/../../bar", + want_output=True, + want_newline=False, + ) + assert output.strip() == "/bar" + + def test_parent8(self, bash, functions): + output = assert_bash_exec( + bash, + "__tester /dir1/dir2/dir3/../dir4/../../foo", + want_output=True, + want_newline=False, + ) + assert output.strip() == "/dir1/foo" + + def test_parent9(self, bash, functions): + output = assert_bash_exec( + bash, + "__tester //dir1/dir2///../foo", + want_output=True, + want_newline=False, + ) + assert output.strip() == "/dir1/foo" + + def test_parent10(self, bash, functions): + output = assert_bash_exec( + bash, + "__tester /dir1/dir2/dir3/..", + want_output=True, + want_newline=False, + ) + assert output.strip() == "/dir1/dir2" + + def test_parent11(self, bash, functions): + output = assert_bash_exec( + bash, + "__tester /dir1/dir2/dir3/../..", + want_output=True, + want_newline=False, + ) + assert output.strip() == "/dir1" + + def test_parent12(self, bash, functions): + output = assert_bash_exec( + bash, + "__tester /dir1/dir2/dir3/../../../..", + want_output=True, + want_newline=False, + ) + assert output.strip() == "/"