init: add --install for shell setup (#3454)

This commit is contained in:
Ayush 2026-06-03 03:43:07 +05:30 committed by GitHub
parent efc77132f7
commit 455d1a31db
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 326 additions and 25 deletions

View File

@ -393,11 +393,15 @@ List existing pyenv shims.
Configure the shell environment for pyenv Configure the shell environment for pyenv
Usage: eval "$(pyenv init [-|--path] [--no-push-path] [--no-rehash] [<shell>])" Usage: eval "$(pyenv init [-|--path] [--no-push-path] [--no-rehash] [<shell>])"
pyenv init --install [<shell>]
pyenv init --detect-shell [<shell>]
- Initialize shims directory, print PYENV_SHELL variable, completions path - Initialize shims directory, print PYENV_SHELL variable, completions path
and shell function and shell function
--path Print shims path --path Print shims path
--install Configure detected shell startup files
--no-push-path Do not push shim to the start of PATH if they're already there --no-push-path Do not push shim to the start of PATH if they're already there
--detect-shell Print shell startup files detected for the current shell
--no-rehash Add no rehash command to output --no-rehash Add no rehash command to output
## `pyenv completions` ## `pyenv completions`

View File

@ -179,6 +179,24 @@ which does install native Windows Python versions.
The below setup should work for the vast majority of users for common use cases. The below setup should work for the vast majority of users for common use cases.
See [Advanced configuration](#advanced-configuration) for details and more configuration options. See [Advanced configuration](#advanced-configuration) for details and more configuration options.
If `pyenv` is already on `PATH`, you can configure the relevant shell startup
files automatically:
```sh
pyenv init --install
```
If `pyenv` is not on `PATH` yet, run the same command through the `pyenv`
executable in your chosen installation directory.
This uses the same shell detection as `pyenv init`. If a startup file already
contains Pyenv-related configuration, the command refuses to edit it; review the
file manually and run `pyenv init <shell>` to see the suggested setup.
For Bash, avoid the automatic `--install` path if your `BASH_ENV` points to
`.bashrc`; use the manual Bash instructions below so the `eval "$(pyenv init - bash)"`
line only goes in your login startup file.
#### Bash #### Bash
<details> <details>

View File

@ -1,6 +1,8 @@
#!/usr/bin/env bash #!/usr/bin/env bash
# Summary: Configure the shell environment for pyenv # Summary: Configure the shell environment for pyenv
# Usage: eval "$(pyenv init [-|--path] [--no-push-path] [--detect-shell] [--no-rehash] [<shell>])" # Usage: eval "$(pyenv init [-|--path] [--no-push-path] [--no-rehash] [<shell>])"
# pyenv init --install [<shell>]
# pyenv init --detect-shell [<shell>]
set -e set -e
[ -n "$PYENV_DEBUG" ] && set -x [ -n "$PYENV_DEBUG" ] && set -x
@ -9,6 +11,7 @@ set -e
if [ "$1" = "--complete" ]; then if [ "$1" = "--complete" ]; then
echo - echo -
echo --path echo --path
echo --install
echo --no-push-path echo --no-push-path
echo --no-rehash echo --no-rehash
echo --detect-shell echo --detect-shell
@ -30,6 +33,9 @@ while [ "$#" -gt 0 ]; do
--path) --path)
mode="path" mode="path"
;; ;;
--install)
mode="install"
;;
--detect-shell) --detect-shell)
mode="detect-shell" mode="detect-shell"
;; ;;
@ -81,21 +87,23 @@ function main() {
exit 0 exit 0
;; ;;
"detect-shell") "detect-shell")
detect_profile 1 detect_profile
print_detect_shell print_detect_shell
exit 0 exit 0
;; ;;
"install")
install_shell_startup_files
exit 0
;;
esac esac
# should never get here # should never get here
exit 2 exit 2
} }
function detect_profile() { function detect_profile() {
local detect_for_detect_shell="$1"
case "$shell" in case "$shell" in
bash ) bash )
if [ -e '~/.bash_profile' ]; then if [ -e "${HOME}/.bash_profile" ]; then
profile='~/.bash_profile' profile='~/.bash_profile'
else else
profile='~/.profile' profile='~/.profile'
@ -103,6 +111,10 @@ function detect_profile() {
profile_explain="~/.bash_profile if it exists, otherwise ~/.profile" profile_explain="~/.bash_profile if it exists, otherwise ~/.profile"
rc='~/.bashrc' rc='~/.bashrc'
;; ;;
fish )
profile='~/.config/fish/config.fish'
rc='~/.config/fish/config.fish'
;;
pwsh ) pwsh )
profile='~/.config/powershell/profile.ps1' profile='~/.config/powershell/profile.ps1'
rc='~/.config/powershell/profile.ps1' rc='~/.config/powershell/profile.ps1'
@ -121,13 +133,10 @@ function detect_profile() {
rc='~/.profile' rc='~/.profile'
;; ;;
* ) * )
if [ -n "$detect_for_detect_shell" ]; then profile=
profile= rc=
rc= profile_explain='your shell'\''s login startup file'
else rc_explain='your shell'\''s interactive startup file'
profile='your shell'\''s login startup file'
rc='your shell'\''s interactive startup file'
fi
;; ;;
esac esac
} }
@ -146,38 +155,32 @@ function help_() {
echo "# Add pyenv executable to PATH by running" echo "# Add pyenv executable to PATH by running"
echo "# the following interactively:" echo "# the following interactively:"
echo echo
echo 'set -Ux PYENV_ROOT $HOME/.pyenv' print_fish_user_path_setup
echo 'set -U fish_user_paths $PYENV_ROOT/bin $fish_user_paths'
echo echo
echo "# Load pyenv automatically by appending" echo "# Load pyenv automatically by appending"
echo "# the following to ~/.config/fish/config.fish:" echo "# the following to ~/.config/fish/config.fish:"
echo echo
echo 'pyenv init - fish | source' print_fish_shell_setup
echo echo
;; ;;
pwsh ) pwsh )
echo '# Load pyenv automatically by appending' echo '# Load pyenv automatically by appending'
echo "# the following to $profile :" echo "# the following to $profile :"
echo echo
echo '$Env:PYENV_ROOT="$Env:HOME/.pyenv"' print_pwsh_shell_setup
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")'
;; ;;
* ) * )
echo '# Load pyenv automatically by appending' echo '# Load pyenv automatically by appending'
echo -n "# the following to " echo -n "# the following to "
if [ "$profile" == "$rc" ]; then if [[ "$profile" == "$rc" && -z $rc_explain ]]; then
echo "$profile :" echo "${profile_explain:-$profile} :"
else else
echo echo
echo "# ${profile_explain:-$profile} (for login shells)" echo "# ${profile_explain:-$profile} (for login shells)"
echo "# and $rc (for interactive shells) :" echo "# and ${rc_explain:-$rc} (for interactive shells) :"
fi fi
echo echo
echo 'export PYENV_ROOT="$HOME/.pyenv"' print_posix_shell_setup
echo '[[ -d $PYENV_ROOT/bin ]] && export PATH="$PYENV_ROOT/bin:$PATH"'
echo 'eval "$(pyenv init - '$shell')"'
;; ;;
esac esac
echo echo
@ -186,6 +189,154 @@ function help_() {
} >&2 } >&2
} }
function print_posix_shell_setup() {
echo 'export PYENV_ROOT="$HOME/.pyenv"'
echo '[[ -d $PYENV_ROOT/bin ]] && export PATH="$PYENV_ROOT/bin:$PATH"'
echo 'eval "$(pyenv init - '$shell')"'
}
function print_fish_shell_setup() {
echo 'pyenv init - fish | source'
}
function print_fish_user_path_setup() {
echo 'set -Ux PYENV_ROOT $HOME/.pyenv'
echo 'if functions -q fish_add_path'
echo ' test -d $PYENV_ROOT/bin; and fish_add_path $PYENV_ROOT/bin'
echo 'else'
echo ' test -d $PYENV_ROOT/bin; and set -U fish_user_paths $PYENV_ROOT/bin $fish_user_paths'
echo 'end'
}
function print_pwsh_shell_setup() {
echo '$Env:PYENV_ROOT="$Env:HOME/.pyenv"'
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")'
}
function expand_home_path() {
local path="$1"
printf '%s\n' "${path/#\~/$HOME}"
}
function install_shell_startup_files() {
if [[ -z $HOME ]]; then
echo "pyenv: HOME must be set to configure shell startup files" >&2
return 1
fi
detect_profile
local files=()
local lines=()
local profile_path rc_path setup
case "$shell" in
bash | zsh | ksh | ksh93 | mksh )
rc_path="$(expand_home_path "$rc")"
profile_path="$(expand_home_path "$profile")"
setup="$(print_posix_shell_setup)"
files=("$rc_path")
lines=("$setup")
if [[ $profile_path != "$rc_path" ]]; then
files+=("$profile_path")
lines+=("$setup")
fi
;;
fish )
rc_path="$(expand_home_path "$rc")"
files=("$rc_path")
lines=("$(print_fish_shell_setup)")
;;
pwsh )
rc_path="$(expand_home_path "$rc")"
files=("$rc_path")
lines=("$(print_pwsh_shell_setup)")
;;
* )
echo "pyenv: cannot automatically configure startup files for $shell" >&2
return 1
;;
esac
local index
for ((index = 0; index < ${#files[@]}; index++)); do
check_startup_file "${files[$index]}" || return 1
done
if [[ $shell == fish ]]; then
install_fish_user_paths || return 1
fi
for ((index = 0; index < ${#files[@]}; index++)); do
append_lines "${files[$index]}" "${lines[$index]}"
done
}
function check_startup_file() {
local file="$1"
local grep_status
if [[ ! -e $file ]]; then
return 0
fi
if [[ ! -f $file || ! -r $file ]]; then
echo "pyenv: failed to inspect $file" >&2
return 1
fi
if grep -Fi pyenv "$file" >/dev/null; then
echo "pyenv: cannot automatically apply changes to $file: it appears to already contain Pyenv-related code." >&2
echo "pyenv: review the file's contents and apply changes manually if necessary." >&2
echo "pyenv: run \`pyenv init $shell\` to see the suggested setup." >&2
return 1
else
grep_status=$?
if [[ $grep_status == 1 ]]; then
return 0
fi
echo "pyenv: failed to inspect $file" >&2
return "$grep_status"
fi
}
function install_fish_user_paths() {
local fish_setup
if ! command -v fish >/dev/null; then
echo "pyenv: fish is not available to configure fish universal variables" >&2
return 1
fi
fish_setup="$(print_fish_user_path_setup)"
if ! fish -c "$fish_setup"; then
echo "pyenv: failed to configure fish universal variables" >&2
return 1
fi
}
function append_lines() {
local file="$1"
local lines="$2"
local dir last_char
dir="${file%/*}"
if [[ $dir != "$file" ]]; then
mkdir -p "$dir"
fi
if [[ -s $file ]]; then
last_char="$(tail -c 1 "$file")" || return 1
if [[ -n $last_char ]]; then
echo >> "$file"
fi
fi
printf '%s\n' "$lines" >> "$file"
}
function init_dirs() { function init_dirs() {
mkdir -p "${PYENV_ROOT}/"{shims,versions} mkdir -p "${PYENV_ROOT}/"{shims,versions}
} }

View File

@ -79,6 +79,134 @@ OUT
assert_line "PYENV_SHELL_DETECT=bash" assert_line "PYENV_SHELL_DETECT=bash"
} }
@test "shell detection for fish startup file" {
run pyenv-init --detect-shell fish
assert_success
assert_line "PYENV_SHELL_DETECT=fish"
assert_line "PYENV_PROFILE_DETECT=~/.config/fish/config.fish"
assert_line "PYENV_RC_DETECT=~/.config/fish/config.fish"
}
@test "completion includes install option" {
run pyenv-init --complete
assert_success
assert_line "--install"
}
@test "install setup for detected shell startup files" {
mkdir -p "$HOME"
run pyenv-init --install
assert_success
expected_setup=$'export PYENV_ROOT="$HOME/.pyenv"\n[[ -d $PYENV_ROOT/bin ]] && export PATH="$PYENV_ROOT/bin:$PATH"\neval "$(pyenv init - bash)"'
assert_equal "$expected_setup" "$(cat "$HOME/.bashrc")"
assert_equal "$expected_setup" "$(cat "$HOME/.profile")"
}
@test "install setup for bash uses existing bash_profile" {
mkdir -p "$HOME"
touch "$HOME/.bash_profile"
run pyenv-init --install bash
assert_success
expected_setup=$'export PYENV_ROOT="$HOME/.pyenv"\n[[ -d $PYENV_ROOT/bin ]] && export PATH="$PYENV_ROOT/bin:$PATH"\neval "$(pyenv init - bash)"'
assert_equal "$expected_setup" "$(cat "$HOME/.bashrc")"
assert_equal "$expected_setup" "$(cat "$HOME/.bash_profile")"
assert [ ! -e "$HOME/.profile" ]
}
@test "install setup for zsh startup files" {
mkdir -p "$HOME"
run pyenv-init --install zsh
assert_success
expected_setup=$'export PYENV_ROOT="$HOME/.pyenv"\n[[ -d $PYENV_ROOT/bin ]] && export PATH="$PYENV_ROOT/bin:$PATH"\neval "$(pyenv init - zsh)"'
assert_equal "$expected_setup" "$(cat "$HOME/.zshrc")"
assert_equal "$expected_setup" "$(cat "$HOME/.zprofile")"
}
@test "install setup for fish startup file" {
mkdir -p "$HOME"
create_stub fish <<OUT
printf '%s\n' "\$2" > "$PYENV_TEST_DIR/fish-script"
OUT
run pyenv-init --install fish
assert_success
expected_fish_script=$'set -Ux PYENV_ROOT $HOME/.pyenv\nif functions -q fish_add_path\n test -d $PYENV_ROOT/bin; and fish_add_path $PYENV_ROOT/bin\nelse\n test -d $PYENV_ROOT/bin; and set -U fish_user_paths $PYENV_ROOT/bin $fish_user_paths\nend'
expected_setup='pyenv init - fish | source'
assert_equal "$expected_fish_script" "$(cat "$PYENV_TEST_DIR/fish-script")"
assert_equal "$expected_setup" "$(cat "$HOME/.config/fish/config.fish")"
}
@test "install setup for pwsh startup file" {
mkdir -p "$HOME"
run pyenv-init --install pwsh
assert_success
expected_setup=$'$Env:PYENV_ROOT="$Env:HOME/.pyenv"\nif (Test-Path -LP "$Env:PYENV_ROOT/bin" -PathType Container) {\n $Env:PATH="$Env:PYENV_ROOT/bin:$Env:PATH" }\niex ((pyenv init -) -join "`n")'
assert_equal "$expected_setup" "$(cat "$HOME/.config/powershell/profile.ps1")"
}
@test "install refuses to modify files with pyenv-related code" {
mkdir -p "$HOME"
echo 'eval "$(pyenv init -)"' > "$HOME/.bashrc"
run pyenv-init --install bash
assert_failure
assert_line "pyenv: cannot automatically apply changes to $HOME/.bashrc: it appears to already contain Pyenv-related code."
assert_line "pyenv: review the file's contents and apply changes manually if necessary."
assert_line "pyenv: run \`pyenv init bash\` to see the suggested setup."
assert_equal 'eval "$(pyenv init -)"' "$(cat "$HOME/.bashrc")"
assert [ ! -e "$HOME/.profile" ]
}
@test "install treats PYENV_ROOT as pyenv-related code" {
mkdir -p "$HOME"
echo 'export PYENV_ROOT="$HOME/tools/python-env"' > "$HOME/.bashrc"
run pyenv-init --install bash
assert_failure
assert_line "pyenv: cannot automatically apply changes to $HOME/.bashrc: it appears to already contain Pyenv-related code."
}
@test "install refuses unreadable startup file without partial writes" {
mkdir -p "$HOME/.bashrc"
run pyenv-init --install bash
assert_failure
assert_line "pyenv: failed to inspect $HOME/.bashrc"
assert [ ! -e "$HOME/.profile" ]
}
@test "install setup keeps fish block intact when generic lines already exist" {
mkdir -p "$HOME/.config/fish"
create_stub fish <<OUT
exit 0
OUT
echo "end" > "$HOME/.config/fish/config.fish"
run pyenv-init --install fish
assert_success
expected_setup=$'end\npyenv init - fish | source'
assert_equal "$expected_setup" "$(cat "$HOME/.config/fish/config.fish")"
}
@test "install setup fails gracefully for unsupported shell" {
mkdir -p "$HOME"
run pyenv-init --install nu
assert_failure "pyenv: cannot automatically configure startup files for nu"
}
@test "option to skip rehash" { @test "option to skip rehash" {
run pyenv-init - --no-rehash run pyenv-init - --no-rehash
assert_success assert_success