diff --git a/bash_completion b/bash_completion index bf42e00885b..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. @@ -3125,6 +3132,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 +3196,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 +3338,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") 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() == "/"