diff --git a/completions/cd b/completions/cd index 67dc3de869f..8de903a68ff 100644 --- a/completions/cd +++ b/completions/cd @@ -1,59 +1,78 @@ # cd(1) completion -*- shell-script -*- -# This meta-cd function observes the CDPATH variable, so that `cd` -# additionally completes on directories under those specified in CDPATH. -_comp_cmd_cd() +_comp_cmd_cd__compgen_cdable_vars() { - local cur prev words cword comp_args - _comp_initialize -- "$@" || return + shopt -q cdable_vars || return 1 - if [[ $cur == -* ]]; then - _comp_compgen_help -c help "$1" - compopt +o nospace - return - fi + local vars + _comp_compgen -v vars -- -v || return "$?" - local i j k + # Remove variables that do not contain a valid directory path. + local _i + for _i in "${!vars[@]}"; do + # Note: ${!vars[_i]} produces the "nounset" error when vars[_i] is an + # empty array name. + [[ -d ${!vars[_i]-} ]] || unset -v 'vars[_i]' + done - compopt -o filenames + _comp_compgen -U vars set "${vars[@]}" +} + +# This generator function observes the CDPATH variable, to additionally +# complete directories under those specified in CDPATH. +_comp_cmd_cd__compgen_cdpath() +{ + local _p - # Use standard dir completion if no CDPATH or parameter starts with /, + # Generate CDPATH completions when the parameter does not start with /, # ./ or ../ - if [[ ! ${CDPATH-} || $cur == ?(.)?(.)/* ]]; then - _comp_compgen_filedir -d - return - fi + [[ ! ${CDPATH-} || $cur == ?(.)?(.)/* ]] && return 1 - local mark_dirs="" mark_symdirs="" - _comp_readline_variable_on mark-directories && mark_dirs=set - _comp_readline_variable_on mark-symlinked-directories && mark_symdirs=set + local _mark_dirs="" _mark_symdirs="" + _comp_readline_variable_on mark-directories && _mark_dirs=set + _comp_readline_variable_on mark-symlinked-directories && _mark_symdirs=set + + local -a _cdpaths=() # we have a CDPATH, so loop on its contents - local paths dirs + local paths dirs _d _comp_split -F : paths "$CDPATH" - for i in "${paths[@]}"; do + for _p in "${paths[@]}"; do # create an array of matched subdirs - k=${#COMPREPLY[@]} - _comp_compgen -v dirs -c "$i/$cur" -- -d - for j in "${dirs[@]}"; do - if [[ ($mark_symdirs && -L $j || $mark_dirs && ! -L $j) && ! -d ${j#"$i/"} ]]; then - j+="/" + _comp_compgen -v dirs -c "$_p/$cur" -- -d + for _d in "${dirs[@]}"; do + if [[ ($_mark_symdirs && -L $_d || $_mark_dirs && ! -L $_d) && ! -d ${_d#"$_p/"} ]]; then + _d+="/" fi - COMPREPLY[k++]=${j#"$i/"} + _cdpaths+=("${_d#"$_p/"}") done done + _comp_unlocal paths dirs - _comp_compgen -a filedir -d - - if ((${#COMPREPLY[@]} == 1)); then - i=${COMPREPLY[0]} - if [[ $i == "$cur" && $i != "*/" ]]; then - COMPREPLY[0]="${i}/" + if ((${#_cdpaths[@]} == 1)); then + _p=${_cdpaths[0]} + if [[ $_p == "$cur" && $_p != */ ]]; then + _cdpaths[0]=$_p/ fi fi + + _comp_compgen_set "${_cdpaths[@]}" +} + +_comp_cmd_cd() +{ + local cur prev words cword comp_args + _comp_initialize -- "$@" || return + + if [[ $cur == -* ]]; then + _comp_compgen_help -c help "$1" + compopt +o nospace + return + fi + + compopt -o filenames + _comp_cmd_cd__compgen_cdable_vars + _comp_cmd_cd__compgen_cdpath + _comp_compgen -a filedir -d } -if shopt -q cdable_vars; then - complete -v -F _comp_cmd_cd -o nospace cd pushd -else - complete -F _comp_cmd_cd -o nospace cd pushd -fi +complete -F _comp_cmd_cd -o nospace cd pushd diff --git a/test/t/test_cd.py b/test/t/test_cd.py index 7243b93d937..b39c4c680b2 100644 --- a/test/t/test_cd.py +++ b/test/t/test_cd.py @@ -1,5 +1,7 @@ import pytest +from conftest import assert_complete, bash_env_saved + @pytest.mark.bashcomp(ignore_env=r"^\+CDPATH=$") class TestCd: @@ -28,3 +30,15 @@ def test_dir_at_point(self, completion): @pytest.mark.complete("cd -") def test_options(self, completion): assert completion + + def test_cdable_vars(self, bash): + with bash_env_saved(bash) as bash_env: + bash_env.shopt("cdable_vars", True) + bash_env.write_variable("foo1", "shared") + bash_env.write_variable("foo2", "shared/default") + bash_env.write_variable("foo3", "nonexistent") + bash_env.write_variable("foo4", "nonexistent") + bash_env.write_variable("foo5", "shared/default/foo") + bash_env.write_variable("foo6", "shared/default/bar") + completion = assert_complete(bash, "cd f") + assert completion == ["foo1", "foo2"]