diff --git a/libexec/pyenv-init b/libexec/pyenv-init index 34f1173e..3bd00605 100755 --- a/libexec/pyenv-init +++ b/libexec/pyenv-init @@ -189,8 +189,76 @@ function help_() { } >&2 } +# True when tips/install should keep the portable default root form. +function pyenv_root_is_default() { + [[ ${PYENV_ROOT} == "${HOME}/.pyenv" ]] +} + +# Escape a value for inclusion inside double quotes in a POSIX shell script. +function escape_for_double_quoted_posix() { + local s="$1" + s="${s//\\/\\\\}" + s="${s//\"/\\\"}" + s="${s//\$/\\\$}" + s="${s//\`/\\\`}" + printf '%s' "$s" +} + +# Escape a value for inclusion inside double quotes in a fish script. +function escape_for_double_quoted_fish() { + local s="$1" + s="${s//\\/\\\\}" + s="${s//\"/\\\"}" + s="${s//\$/\\\$}" + printf '%s' "$s" +} + +# Escape a value for inclusion inside single quotes in a PowerShell script. +function escape_for_single_quoted_pwsh() { + local s="$1" + # Bash 3.2 (macOS / CI docker): avoid \' in ${//} replacement — + # it becomes a literal backslash. Double single-quotes via a var instead. + local q="'" + s="${s//${q}/${q}${q}}" + printf '%s' "$s" +} + +# Emit the PYENV_ROOT assignment line for tips/--install (posix|fish|pwsh). +# Portable default keeps $HOME / $Env:HOME; custom roots are quoted per target shell. +function print_pyenv_root_assignment() { + case "$1" in + posix ) + if pyenv_root_is_default; then + echo 'export PYENV_ROOT="$HOME/.pyenv"' + else + printf 'export PYENV_ROOT="%s"\n' "$(escape_for_double_quoted_posix "${PYENV_ROOT}")" + fi + ;; + fish ) + if pyenv_root_is_default; then + # Keep master's expandable $HOME form (fish expands unquoted $HOME). + echo 'set -Ux PYENV_ROOT $HOME/.pyenv' + else + printf 'set -Ux PYENV_ROOT "%s"\n' "$(escape_for_double_quoted_fish "${PYENV_ROOT}")" + fi + ;; + pwsh ) + if pyenv_root_is_default; then + echo '$Env:PYENV_ROOT="$Env:HOME/.pyenv"' + else + # Single-quoted PowerShell literal: no $ / backtick expansion. + printf "\$Env:PYENV_ROOT='%s'\n" "$(escape_for_single_quoted_pwsh "${PYENV_ROOT}")" + fi + ;; + * ) + echo "pyenv: internal error: unknown shell family for PYENV_ROOT assignment: $1" >&2 + return 1 + ;; + esac +} + function print_posix_shell_setup() { - echo 'export PYENV_ROOT="$HOME/.pyenv"' + print_pyenv_root_assignment posix echo '[[ -d $PYENV_ROOT/bin ]] && export PATH="$PYENV_ROOT/bin:$PATH"' echo 'eval "$(pyenv init - '$shell')"' } @@ -200,7 +268,7 @@ function print_fish_shell_setup() { } function print_fish_user_path_setup() { - echo 'set -Ux PYENV_ROOT $HOME/.pyenv' + print_pyenv_root_assignment fish echo 'if functions -q fish_add_path' echo ' test -d $PYENV_ROOT/bin; and fish_add_path $PYENV_ROOT/bin' echo 'else' @@ -209,7 +277,7 @@ function print_fish_user_path_setup() { } function print_pwsh_shell_setup() { - echo '$Env:PYENV_ROOT="$Env:HOME/.pyenv"' + print_pyenv_root_assignment pwsh echo 'if (Test-Path -LP "$Env:PYENV_ROOT/bin" -PathType Container) {' echo ' $Env:PATH="$Env:PYENV_ROOT/bin:$Env:PATH" }' echo 'iex ((pyenv init -) -join "`n")' diff --git a/test/init.bats b/test/init.bats index cb1ef20d..908a5dab 100755 --- a/test/init.bats +++ b/test/init.bats @@ -93,8 +93,9 @@ OUT assert_line "--install" } -@test "install setup for detected shell startup files" { - mkdir -p "$HOME" +@test "install setup for detected shell startup files (default PYENV_ROOT)" { + export PYENV_ROOT="$HOME/.pyenv" + mkdir -p "$PYENV_ROOT" run pyenv-init --install assert_success @@ -104,8 +105,9 @@ OUT assert_equal "$expected_setup" "$(cat "$HOME/.profile")" } -@test "install setup for bash uses existing bash_profile" { - mkdir -p "$HOME" +@test "install setup for bash uses existing bash_profile (default PYENV_ROOT)" { + export PYENV_ROOT="$HOME/.pyenv" + mkdir -p "$PYENV_ROOT" touch "$HOME/.bash_profile" run pyenv-init --install bash @@ -117,8 +119,9 @@ OUT assert [ ! -e "$HOME/.profile" ] } -@test "install setup for zsh startup files" { - mkdir -p "$HOME" +@test "install setup for zsh startup files (default PYENV_ROOT)" { + export PYENV_ROOT="$HOME/.pyenv" + mkdir -p "$PYENV_ROOT" run pyenv-init --install zsh assert_success @@ -128,8 +131,9 @@ OUT assert_equal "$expected_setup" "$(cat "$HOME/.zprofile")" } -@test "install setup for fish startup file" { - mkdir -p "$HOME" +@test "install setup for fish startup file (default PYENV_ROOT)" { + export PYENV_ROOT="$HOME/.pyenv" + mkdir -p "$PYENV_ROOT" create_stub fish < "$PYENV_TEST_DIR/fish-script" OUT @@ -143,8 +147,9 @@ OUT assert_equal "$expected_setup" "$(cat "$HOME/.config/fish/config.fish")" } -@test "install setup for pwsh startup file" { - mkdir -p "$HOME" +@test "install setup for pwsh startup file (default PYENV_ROOT)" { + export PYENV_ROOT="$HOME/.pyenv" + mkdir -p "$PYENV_ROOT" run pyenv-init --install pwsh assert_success @@ -153,6 +158,22 @@ OUT assert_equal "$expected_setup" "$(cat "$HOME/.config/powershell/profile.ps1")" } +@test "init honors custom PYENV_ROOT and quotes it according to each shell's rules" { + export PYENV_ROOT='/tmp/pyenv$root/"quote"' + run pyenv-init bash + assert [ "$status" -eq 1 ] + assert_line 'export PYENV_ROOT="/tmp/pyenv\$root/\"quote\""' + + run pyenv-init fish + assert [ "$status" -eq 1 ] + assert_line 'set -Ux PYENV_ROOT "/tmp/pyenv\$root/\"quote\""' + + export PYENV_ROOT="/tmp/pyenv'root" + run pyenv-init pwsh + assert [ "$status" -eq 1 ] + assert_line "\$Env:PYENV_ROOT='/tmp/pyenv''root'" +} + @test "install refuses to modify files with pyenv-related code" { mkdir -p "$HOME" echo 'eval "$(pyenv init -)"' > "$HOME/.bashrc"