diff --git a/libexec/pyenv-exec b/libexec/pyenv-exec index 6ed5ac20..2368bdd3 100755 --- a/libexec/pyenv-exec +++ b/libexec/pyenv-exec @@ -29,6 +29,14 @@ if [ -z "$PYENV_COMMAND" ]; then exit 1 fi +if [[ -n $_PYENV_SHIM_PATH ]]; then + PROGRAM="$(echo "$PYENV_COMMAND" | tr a-z- A-Z_ | sed 's/[^A-Z0-9_]/_/g')" + NR_PYENV_SHIM_PATHS_PROGRAM="_PYENV_SHIM_PATHS_${PROGRAM}" + export -- "$NR_PYENV_SHIM_PATHS_PROGRAM=$_PYENV_SHIM_PATH${!NR_PYENV_SHIM_PATHS_PROGRAM:+:${!NR_PYENV_SHIM_PATHS_PROGRAM}}" + unset PROGRAM NR_PYENV_SHIM_PATHS_PROGRAM + unset _PYENV_SHIM_PATH +fi + PYENV_COMMAND_PATH="$(pyenv-which "$PYENV_COMMAND")" PYENV_BIN_PATH="${PYENV_COMMAND_PATH%/*}" export PYENV_VERSION diff --git a/libexec/pyenv-rehash b/libexec/pyenv-rehash index 745cfb8e..61000ff6 100755 --- a/libexec/pyenv-rehash +++ b/libexec/pyenv-rehash @@ -82,6 +82,10 @@ set -e program="\${0##*/}" export PYENV_ROOT="$PYENV_ROOT" +SHIM_PATH=\${0%/*} +if [[ \$SHIM_PATH != "$PYENV_ROOT/shims" ]]; then + export _PYENV_SHIM_PATH="\$SHIM_PATH" +fi exec "$(command -v pyenv)" exec "\$program" "\$@" SH chmod +x "$PROTOTYPE_SHIM_PATH" diff --git a/libexec/pyenv-which b/libexec/pyenv-which index 9ff3ddae..0ebf8219 100755 --- a/libexec/pyenv-which +++ b/libexec/pyenv-which @@ -42,15 +42,19 @@ done remove_from_path() { - local path_to_remove="$1" - local path_before + local -a paths_to_remove + IFS=: paths_to_remove=($1) + local path_to_remove path_before local result=":${PATH//\~/$HOME}:" - while [ "$path_before" != "$result" ]; do - path_before="$result" - result="${result//:$path_to_remove:/:}" + for path_to_remove in "${paths_to_remove[@]}"; do + while true; do + path_before="$result" + result="${result//:$path_to_remove:/:}" + if [[ ${#path_before} == "${#result}" ]]; then break; fi + done done - result="${result%:}" - echo "${result#:}" + result="${result:1:${#result}-2}" + echo "$result" } if [ -z "$PYENV_COMMAND" ]; then @@ -66,8 +70,10 @@ declare -a nonexistent_versions for version in "${versions[@]}" "$system"; do if [ "$version" = "system" ]; then - PATH="$(remove_from_path "${PYENV_ROOT}/shims")" - PYENV_COMMAND_PATH="$(command -v "$PYENV_COMMAND" || true)" + PROGRAM="$(echo "$PYENV_COMMAND" | tr a-z- A-Z_ | sed 's/[^A-Z0-9_]/_/g')" + NR_CUSTOM_SHIM_PATHS="_PYENV_SHIM_PATHS_$PROGRAM" + SEARCH_PATH="$(remove_from_path "${PYENV_ROOT}/shims${!NR_CUSTOM_SHIM_PATHS:+:${!NR_CUSTOM_SHIM_PATHS}}")" + PYENV_COMMAND_PATH="$(PATH="$SEARCH_PATH" command -v "$PYENV_COMMAND" || true)" else # $version may be a prefix to be resolved by pyenv-latest version_path="$(pyenv-prefix "${version}" 2>/dev/null)" || \ diff --git a/test/exec.bats b/test/exec.bats index cf818dbe..ff49912b 100644 --- a/test/exec.bats +++ b/test/exec.bats @@ -2,18 +2,6 @@ load test_helper -create_executable() { - name="${1?}" - shift 1 - bin="${PYENV_ROOT}/versions/${PYENV_VERSION}/bin" - mkdir -p "$bin" - { if [ $# -eq 0 ]; then cat - - else printf '%s\n' "$@" - fi - } | sed -Ee '1s/^ +//' > "${bin}/$name" - chmod +x "${bin}/$name" -} - @test "fails with invalid version" { bats_require_minimum_version 1.5.0 export PYENV_VERSION="3.4" @@ -38,8 +26,8 @@ EOF @test "completes with names of executables" { export PYENV_VERSION="3.4" - create_executable "fab" "#!/bin/sh" - create_executable "python" "#!/bin/sh" + create_alt_executable "fab" + create_alt_executable "python" pyenv-rehash run pyenv-completions exec @@ -65,7 +53,7 @@ SH @test "forwards all arguments" { export PYENV_VERSION="3.4" - create_executable "python" < "${PYENV_TEST_DIR}/python3" < "${bin}/$name" - chmod +x "${bin}/$name" -} - @test "creates shims and versions directories" { assert [ ! -d "${PYENV_ROOT}/shims" ] assert [ ! -d "${PYENV_ROOT}/versions" ] @@ -179,16 +167,14 @@ echo "\$PATH" } @test "outputs sh-compatible case syntax" { - create_executable pyenv-commands < "${bin}/$name" - chmod +x "${bin}/$name" -} - @test "read from installed" { - create_executable pyenv-versions < "${bin}/$1" - chmod +x "${bin}/$1" -} - copy_src_pyenvd() { mkdir -p "${PYENV_ROOT}" cp -r "${BATS_TEST_DIRNAME}/../pyenv.d" "${PYENV_ROOT}" @@ -16,8 +9,8 @@ copy_src_pyenvd() { @test "pip-rehash triggered when using 'pip'" { export PYENV_VERSION="3.7.14" - create_executable "example" - create_executable "pip" + create_alt_executable "example" + create_alt_executable "pip" copy_src_pyenvd run command -v example 2> /dev/null assert_failure @@ -29,8 +22,8 @@ copy_src_pyenvd() { @test "pip-rehash triggered when using 'pip3'" { export PYENV_VERSION="3.7.14" - create_executable "example" - create_executable "pip3" + create_alt_executable "example" + create_alt_executable "pip3" copy_src_pyenvd run command -v example 2> /dev/null assert_failure @@ -42,8 +35,8 @@ copy_src_pyenvd() { @test "pip-rehash triggered when using 'pip3.x'" { export PYENV_VERSION="3.7.14" - create_executable "example" - create_executable "pip3.7" + create_alt_executable "example" + create_alt_executable "pip3.7" copy_src_pyenvd run command -v example 2> /dev/null assert_failure @@ -55,8 +48,8 @@ copy_src_pyenvd() { @test "pip-rehash triggered when using 'python -m pip install'" { export PYENV_VERSION="3.7.14" - create_executable "example" - create_executable "python" + create_alt_executable "example" + create_alt_executable "python" copy_src_pyenvd run command -v example 2> /dev/null assert_failure diff --git a/test/rehash.bats b/test/rehash.bats index 860d9d79..cda2a074 100755 --- a/test/rehash.bats +++ b/test/rehash.bats @@ -2,13 +2,6 @@ load test_helper -create_executable() { - local bin="${PYENV_ROOT}/versions/${1}/bin" - mkdir -p "$bin" - touch "${bin}/$2" - chmod +x "${bin}/$2" -} - @test "empty rehash" { assert [ ! -d "${PYENV_ROOT}/shims" ] run pyenv-rehash @@ -42,10 +35,10 @@ create_executable() { } @test "creates shims" { - create_executable "2.7" "python" - create_executable "2.7" "fab" - create_executable "3.4" "python" - create_executable "3.4" "py.test" + create_alt_executable_in_version "2.7" "python" + create_alt_executable_in_version "2.7" "fab" + create_alt_executable_in_version "3.4" "python" + create_alt_executable_in_version "3.4" "py.test" assert [ ! -e "${PYENV_ROOT}/shims/fab" ] assert [ ! -e "${PYENV_ROOT}/shims/python" ] @@ -68,8 +61,8 @@ OUT touch "${PYENV_ROOT}/shims/oldshim1" chmod +x "${PYENV_ROOT}/shims/oldshim1" - create_executable "3.4" "fab" - create_executable "3.4" "python" + create_alt_executable_in_version "3.4" "fab" + create_alt_executable_in_version "3.4" "python" run pyenv-rehash assert_success "" @@ -78,8 +71,8 @@ OUT } @test "binary install locations containing spaces" { - create_executable "dirname1 p247" "python" - create_executable "dirname2 preview1" "py.test" + create_alt_executable_in_version "dirname1 p247" "python" + create_alt_executable_in_version "dirname2 preview1" "py.test" assert [ ! -e "${PYENV_ROOT}/shims/python" ] assert [ ! -e "${PYENV_ROOT}/shims/py.test" ] @@ -108,29 +101,54 @@ SH } @test "sh-rehash in bash" { - create_executable "3.4" "python" + create_alt_executable_in_version "3.4" "python" PYENV_SHELL=bash run pyenv-sh-rehash assert_success "command pyenv rehash hash -r 2>/dev/null || true" } @test "sh-rehash in bash (integration)" { - create_executable "3.4" "python" + create_alt_executable_in_version "3.4" "python" run eval "$(pyenv-sh-rehash)" assert_success assert [ -x "${PYENV_ROOT}/shims/python" ] } @test "sh-rehash in fish" { - create_executable "3.4" "python" + create_alt_executable_in_version "3.4" "python" PYENV_SHELL=fish run pyenv-sh-rehash assert_success "command pyenv rehash" } @test "sh-rehash in fish (integration)" { command -v fish >/dev/null || skip "-- fish not installed" - create_executable "3.4" "python" + create_alt_executable_in_version "3.4" "python" run fish -Nc "eval (pyenv-sh-rehash)" assert_success assert [ -x "${PYENV_ROOT}/shims/python" ] } + +@test "shim sets _PYENV_SHIM_PATH when linked from elsewhere" { + export PYENV_VERSION="custom" + create_alt_executable python3 + #must stub pyenv before rehash 'cuz the path is hardcoded into shims + create_stub pyenv </dev/null; then if [ -n "$PYENV_NATIVE_EXT" ]; then echo "pyenv: failed to load \`realpath' builtin" >&2 @@ -27,7 +28,7 @@ setup() { PATH="${BATS_TEST_DIRNAME%/*}/libexec:$PATH" PATH="${BATS_TEST_DIRNAME}/libexec:$PATH" PATH="${PYENV_ROOT}/shims:$PATH" - export PATH + PATH="${BATS_TEST_TMPDIR}/stubs:$PATH" for xdg_var in `env 2>/dev/null | grep ^XDG_ | cut -d= -f1`; do unset "$xdg_var"; done unset xdg_var @@ -127,7 +128,7 @@ path_without() { if [ "$found" != "${PYENV_ROOT}/shims" ]; then alt="${PYENV_TEST_DIR}/$(echo "${found#/}" | tr '/' '-')" mkdir -p "$alt" - for util in bash head cut readlink greadlink; do + for util in bash head cut readlink greadlink tr sed; do if [ -x "${found}/$util" ]; then ln -s "${found}/$util" "${alt}/$util" fi @@ -141,6 +142,43 @@ path_without() { echo "$path" } +create_path_executable() { + create_executable "${PYENV_TEST_DIR}/bin" "$@" +} + +create_alt_executable() { + create_alt_executable_in_version "${PYENV_VERSION}" "$@" +} + +create_alt_executable_in_version() { + local version="${1:?}" + shift 1 + create_executable "${PYENV_ROOT}/versions/$version/bin" "$@" +} + +create_stub() { + create_executable "${BATS_TEST_TMPDIR}/stubs" "$@" +} + +create_executable() { + local bin="${1:?}" + local name="${2:?}" + shift 2 + mkdir -p "$bin" + local payload + # Bats doesn't redirect stdin + if [[ $# -eq 0 && ! -t 0 ]]; then + payload="$(cat -)" + else + payload="$(printf '%s\n' "$@")" + fi + if [[ $payload != "#!/"* ]]; then + payload="#!$BASH"$'\n'"$payload" + fi + echo "$payload" > "${bin}/$name" + chmod +x "${bin}/$name" +} + create_hook() { mkdir -p "${PYENV_HOOK_PATH}/$1" touch "${PYENV_HOOK_PATH}/$1/$2" diff --git a/test/versions.bats b/test/versions.bats index e14285fc..58ab032c 100644 --- a/test/versions.bats +++ b/test/versions.bats @@ -22,14 +22,6 @@ stub_system_python() { touch "$stub" && chmod +x "$stub" } -create_executable() { - local name="$1" - local bin="${PYENV_TEST_DIR}/bin" - mkdir -p "$bin" - sed -Ee '1s/^ +//' > "${bin}/$name" - chmod +x "${bin}/$name" -} - @test "no versions installed" { stub_system_python assert [ ! -d "${PYENV_ROOT}/versions" ] @@ -199,7 +191,7 @@ OUT create_version "1.9.0" create_version "1.53.0" create_version "1.218.0" - create_executable sort </dev/null if [ "\$1" == "--version-sort" ]; then @@ -223,7 +215,7 @@ OUT create_version "1.9.0" create_version "1.53.0" create_version "1.218.0" - create_executable sort < "${PYENV_ROOT}/version" <<<"3.4" - create_executable "3.4" "python" + create_alt_executable_in_version "3.4" "python" mkdir -p "$PYENV_TEST_DIR" cd "$PYENV_TEST_DIR" @@ -165,7 +153,7 @@ SH 2.7 3.4 EOF - create_executable "3.4" "python" + create_alt_executable_in_version "3.4" "python" mkdir -p "$PYENV_TEST_DIR" cd "$PYENV_TEST_DIR" @@ -175,7 +163,7 @@ EOF } @test "resolves pyenv-latest prefixes" { - create_executable "3.4.2" "python" + create_alt_executable_in_version "3.4.2" "python" PYENV_VERSION=3.4 run pyenv-which python assert_success "${PYENV_ROOT}/versions/3.4.2/bin/python" @@ -187,7 +175,7 @@ echo version=\$version exit ! - create_executable "3.4.2" "python" + create_alt_executable_in_version "3.4.2" "python" PYENV_VERSION=3.4 run pyenv-which python assert_success "version=3.4.2" @@ -195,9 +183,9 @@ exit @test "skip advice supresses error messages" { bats_require_minimum_version 1.5.0 - create_executable "2.7" "python" - create_executable "3.3" "py.test" - create_executable "3.4" "py.test" + create_alt_executable_in_version "2.7" "python" + create_alt_executable_in_version "3.3" "py.test" + create_alt_executable_in_version "3.4" "py.test" PYENV_VERSION=2.7 run -127 pyenv-which py.test --skip-advice assert_failure @@ -205,3 +193,29 @@ exit pyenv: py.test: command not found OUT } + +@test "excludes paths in _PYENV_SHIM_PATHS_{PROGRAM} from search only for PROGRAM" { + progname='123;wacky-prog.name ^%$#' + envvarname="_PYENV_SHIM_PATHS_123_WACKY_PROG_NAME_____" + create_path_executable "$progname" + + for dir_ in "$BATS_TEST_TMPDIR/alt-path"{1,2}; do + mkdir -p "$dir_" + ln -s "${PYENV_TEST_DIR}/bin/$progname" "$dir_/$progname" + eval 'export '"$envvarname"'="$dir_${'"$envvarname"':+:$'"$envvarname"'}"' + PATH="$dir_:$PATH" + done + create_executable "$dir_" "normal_program" + + run pyenv-which "$progname" + assert_success + assert_output <