mirror of
https://github.com/pyenv/pyenv.git
synced 2026-09-10 15:46:17 -04:00
Replace the use of `xargs -r` and `basename -a` in the `--executables` fast path with Bash 3.2-compatible parameter expansion. The previous implementation relied on non-portable utility options, causing `pyenv rehash` and shell initialization to fail on illumos, OpenBSD, and other non-GNU userlands. Preserve the optimized executable discovery path by continuing to discover executables directly from the versions directory and deduplicate their basenames.
210 lines
5.5 KiB
Bash
Executable file
210 lines
5.5 KiB
Bash
Executable file
#!/usr/bin/env bash
|
|
# Summary: List all Python versions available to pyenv
|
|
# Usage: pyenv versions [--bare] [--skip-aliases] [--skip-envs] [--executables]
|
|
#
|
|
# Lists all Python versions found in `$PYENV_ROOT/versions/*'.
|
|
#
|
|
# --bare List just the names, omit `system'
|
|
# --skip-aliases Skip symlinks to other versions and to virtual environments
|
|
# --skip-envs Skip virtual environments (under <version>/envs)
|
|
# --executables Internal. Overrides other options.
|
|
# Optimally get a deduplicated list of all executable names in Pyenv-managed
|
|
# versions and environments for `pyenv rehash'
|
|
#
|
|
|
|
set -e
|
|
[ -n "$PYENV_DEBUG" ] && set -x
|
|
|
|
unset bare skip_aliases skip_envs executables
|
|
# Provide pyenv completions
|
|
for arg; do
|
|
case "$arg" in
|
|
--complete )
|
|
echo --bare
|
|
echo --skip-aliases
|
|
echo --skip-envs
|
|
exit ;;
|
|
--executables ) executables=1; break ;;
|
|
--bare ) bare=1 ;;
|
|
--skip-aliases ) skip_aliases=1 ;;
|
|
--skip-envs ) skip_envs=1 ;;
|
|
* )
|
|
pyenv-help --usage versions >&2
|
|
exit 1
|
|
;;
|
|
esac
|
|
done
|
|
|
|
|
|
versions_dir="${PYENV_ROOT}/versions"
|
|
|
|
# Fast path for rehash: skip filtering and link resolution
|
|
if [[ -n "$executables" ]]; then
|
|
if [ -d "$versions_dir" ]; then
|
|
shopt -s dotglob nullglob
|
|
# Fast path for rehash: skip filtering and link resolution.
|
|
# Discover executables directly from the versions directory and
|
|
# deduplicate their basenames. Use bash 3.2 conventions to keep code portable.
|
|
for path in \
|
|
"$versions_dir"/*/bin/* \
|
|
"$versions_dir"/*/envs/*/bin/*
|
|
do
|
|
printf '%s\n' "${path##*/}"
|
|
done | sort -u
|
|
shopt -u dotglob nullglob
|
|
fi
|
|
exit 0
|
|
fi
|
|
|
|
if ! enable -f "${BASH_SOURCE%/*}"/pyenv-realpath.dylib realpath 2>/dev/null; then
|
|
if [ -n "$PYENV_NATIVE_EXT" ]; then
|
|
echo "pyenv: failed to load \`realpath' builtin" >&2
|
|
exit 1
|
|
fi
|
|
|
|
READLINK=$(type -P readlink)
|
|
if [ -z "$READLINK" ]; then
|
|
echo "pyenv: cannot find readlink - are you missing GNU coreutils?" >&2
|
|
exit 1
|
|
fi
|
|
|
|
resolve_link() {
|
|
$READLINK "$1"
|
|
}
|
|
|
|
realpath() {
|
|
local path="$1"
|
|
local name
|
|
|
|
# Use a subshell to avoid changing the current path
|
|
(
|
|
while [ -n "$path" ]; do
|
|
name="${path##*/}"
|
|
[ "$name" = "$path" ] || cd "${path%/*}"
|
|
path="$(resolve_link "$name" || true)"
|
|
done
|
|
|
|
echo "${PWD}/$name"
|
|
)
|
|
}
|
|
fi
|
|
|
|
if [ -d "$versions_dir" ]; then
|
|
versions_dir="$(realpath "$versions_dir")"
|
|
fi
|
|
|
|
if ((${BASH_VERSINFO[0]} > 3)); then
|
|
declare -A current_versions
|
|
else
|
|
current_versions=()
|
|
fi
|
|
if [ -n "$bare" ]; then
|
|
include_system=""
|
|
else
|
|
hit_prefix="* "
|
|
miss_prefix=" "
|
|
OLDIFS="$IFS"
|
|
IFS=:
|
|
# VAR=() of unquoted value does glob expansion (against the current dir's contents) after IFS-splitting
|
|
# which is undesired
|
|
set -f
|
|
if ((${BASH_VERSINFO[0]} > 3)); then
|
|
for i in $(pyenv-version-name || true); do
|
|
current_versions["$i"]="1"
|
|
done
|
|
else
|
|
current_versions=($(pyenv-version-name || true))
|
|
fi
|
|
set +f
|
|
IFS="$OLDIFS"
|
|
include_system="1"
|
|
fi
|
|
|
|
num_versions=0
|
|
|
|
exists() {
|
|
local car="$1"
|
|
local cdar
|
|
shift
|
|
for cdar in "$@"; do
|
|
if [ "${car}" == "${cdar}" ]; then
|
|
return 0
|
|
fi
|
|
done
|
|
return 1
|
|
}
|
|
|
|
print_version() {
|
|
local version="${1:?}"
|
|
if [[ -n $bare ]]; then
|
|
echo "$version"
|
|
return
|
|
fi
|
|
local path="${2:?}"
|
|
if [[ -L "$path" ]]; then
|
|
# Only resolve the link itself for printing, do not resolve further.
|
|
# Doing otherwise would misinform the user of what the link contains.
|
|
version_repr="$version --> $(readlink "$path")"
|
|
else
|
|
version_repr="$version"
|
|
fi
|
|
if [[ ${BASH_VERSINFO[0]} -ge 4 && ${current_versions["$1"]} ]]; then
|
|
echo "${hit_prefix}${version_repr} (set by $(pyenv-version-origin))"
|
|
elif (( ${BASH_VERSINFO[0]} <= 3 )) && exists "$1" "${current_versions[@]}"; then
|
|
echo "${hit_prefix}${version_repr} (set by $(pyenv-version-origin))"
|
|
else
|
|
echo "${miss_prefix}${version_repr}"
|
|
fi
|
|
num_versions=$((num_versions + 1))
|
|
}
|
|
|
|
# Include "system" in the non-bare output, if it exists
|
|
if [ -n "$include_system" ] && \
|
|
(PYENV_VERSION=system pyenv-which python --skip-advice >/dev/null 2>&1 || \
|
|
PYENV_VERSION=system pyenv-which python3 --skip-advice >/dev/null 2>&1 || \
|
|
PYENV_VERSION=system pyenv-which python2 --skip-advice >/dev/null 2>&1) ; then
|
|
print_version system "/"
|
|
fi
|
|
|
|
shopt -s dotglob nullglob
|
|
versions_dir_entries=("$versions_dir"/*)
|
|
if sort --version-sort </dev/null >/dev/null 2>&1; then
|
|
# system sort supports version sorting
|
|
OLDIFS="$IFS"
|
|
# VAR=() of unquoted value does glob expansion (against the current dir's contents) after IFS-splitting
|
|
# which is undesired
|
|
set -f
|
|
IFS=$'\n'
|
|
versions_dir_entries=($(
|
|
printf "%s\n" "${versions_dir_entries[@]}" |
|
|
sort --version-sort
|
|
))
|
|
set +f
|
|
IFS="$OLDIFS"
|
|
fi
|
|
|
|
for path in "${versions_dir_entries[@]}"; do
|
|
if [ -d "$path" ]; then
|
|
if [ -n "$skip_aliases" ] && [ -L "$path" ]; then
|
|
target="$(realpath "$path")"
|
|
[ "${target%/*}" == "$versions_dir" ] && continue
|
|
[ "${target%/*/envs/*}" == "$versions_dir" ] && continue
|
|
fi
|
|
print_version "${path##*/}" "$path"
|
|
# virtual environments created by anaconda/miniconda/pyenv-virtualenv
|
|
if [[ -z $skip_envs ]]; then
|
|
for env_path in "${path}/envs/"*; do
|
|
if [ -d "${env_path}" ]; then
|
|
print_version "${env_path#${PYENV_ROOT}/versions/}" "${env_path}"
|
|
fi
|
|
done
|
|
fi
|
|
fi
|
|
done
|
|
shopt -u dotglob nullglob
|
|
|
|
if [ "$num_versions" -eq 0 ] && [ -n "$include_system" ]; then
|
|
echo "Warning: no Python detected on the system" >&2
|
|
exit 1
|
|
fi
|