From 57ac377dd8fbaa3e932ba34dea5e3b44389d552e Mon Sep 17 00:00:00 2001 From: "Christopher O. Caldwell" Date: Mon, 20 Apr 2026 17:38:22 -0600 Subject: [PATCH 1/5] Bring back POSIX sh (dash) support gh-525 Assisted by Qwen3-Coder-30B-A3B-Instruct via OpenCode. --- bin/pyenv-virtualenv-init | 12 ++++++------ test/init.bats | 34 ++++++++++++++++++++++------------ 2 files changed, 28 insertions(+), 18 deletions(-) diff --git a/bin/pyenv-virtualenv-init b/bin/pyenv-virtualenv-init index 138fb48e..634ce6f8 100755 --- a/bin/pyenv-virtualenv-init +++ b/bin/pyenv-virtualenv-init @@ -214,7 +214,7 @@ EOS return \$ret fi if [ "\${PWD}" = "\${_PYENV_VH_PWD-}" ] \\ - && [ "\$(stat ${_stat_fmt} "\${_PYENV_VH_PATHS[@]}" 2>/dev/null)" = "\${_PYENV_VH_MTIMES-}" ]; then + && [ "\$(stat ${_stat_fmt} \${_PYENV_VH_PATHS} 2>/dev/null)" = "\${_PYENV_VH_MTIMES-}" ]; then return \$ret fi fi @@ -227,25 +227,25 @@ EOS _PYENV_VH_VERSION="\${PYENV_VERSION-}" _PYENV_VH_VENV="\${VIRTUAL_ENV-}" local _pvh_d="\${PWD}" _pvh_found_local=0 - _PYENV_VH_PATHS=() + _PYENV_VH_PATHS="" while :; do if [ -f "\${_pvh_d}/.python-version" ] || [ -L "\${_pvh_d}/.python-version" ]; then - _PYENV_VH_PATHS+=("\${_pvh_d}/.python-version") + _PYENV_VH_PATHS="\${_PYENV_VH_PATHS} \${_pvh_d}/.python-version" if [ -f "\${_pvh_d}/.python-version" ]; then _pvh_found_local=1 break fi else - _PYENV_VH_PATHS+=("\${_pvh_d}") + _PYENV_VH_PATHS="\${_PYENV_VH_PATHS} \${_pvh_d}" fi [ "\${_pvh_d}" = "/" ] && break _pvh_d="\${_pvh_d%/*}" [ -z "\${_pvh_d}" ] && _pvh_d="/" done if [ "\${_pvh_found_local}" = "0" ]; then - _PYENV_VH_PATHS+=("\${PYENV_ROOT}/version") + _PYENV_VH_PATHS="\${_PYENV_VH_PATHS} \${PYENV_ROOT}/version" fi - _PYENV_VH_MTIMES="\$(stat ${_stat_fmt} "\${_PYENV_VH_PATHS[@]}" 2>/dev/null)" + _PYENV_VH_MTIMES="\$(stat ${_stat_fmt} \${_PYENV_VH_PATHS} 2>/dev/null)" return \$ret }; EOS diff --git a/test/init.bats b/test/init.bats index 2aacc8fc..1b942cfc 100644 --- a/test/init.bats +++ b/test/init.bats @@ -39,6 +39,16 @@ load test_helper assert_output_contains 'eval "$(pyenv virtualenv-init -)"' } +@test "generated hook code is valid POSIX sh" { + # Get output directly to avoid bats env affecting shell detection + # The bash output should be POSIX sh-compatible (no bash arrays, etc.) + output=$(bash bin/pyenv-virtualenv-init - bash 2>/dev/null) + # Validate syntax with dash (POSIX sh) + result=$(echo "$output" | dash -n - 2>&1) + status=$? + [ $status -eq 0 ] || echo "dash failed: $result" >&2 +} + @test "fish instructions" { run pyenv-virtualenv-init fish assert [ "$status" -eq 1 ] @@ -61,7 +71,7 @@ _pyenv_virtualenv_hook() { return \$ret fi if [ "\${PWD}" = "\${_PYENV_VH_PWD-}" ] \\ - && [ "\$(stat ${_stat_fmt} "\${_PYENV_VH_PATHS[@]}" 2>/dev/null)" = "\${_PYENV_VH_MTIMES-}" ]; then + && [ "\$(stat ${_stat_fmt} \${_PYENV_VH_PATHS} 2>/dev/null)" = "\${_PYENV_VH_MTIMES-}" ]; then return \$ret fi fi @@ -74,25 +84,25 @@ _pyenv_virtualenv_hook() { _PYENV_VH_VERSION="\${PYENV_VERSION-}" _PYENV_VH_VENV="\${VIRTUAL_ENV-}" local _pvh_d="\${PWD}" _pvh_found_local=0 - _PYENV_VH_PATHS=() + _PYENV_VH_PATHS="" while :; do if [ -f "\${_pvh_d}/.python-version" ] || [ -L "\${_pvh_d}/.python-version" ]; then - _PYENV_VH_PATHS+=("\${_pvh_d}/.python-version") + _PYENV_VH_PATHS="\${_PYENV_VH_PATHS} \${_pvh_d}/.python-version" if [ -f "\${_pvh_d}/.python-version" ]; then _pvh_found_local=1 break fi else - _PYENV_VH_PATHS+=("\${_pvh_d}") + _PYENV_VH_PATHS="\${_PYENV_VH_PATHS} \${_pvh_d}" fi [ "\${_pvh_d}" = "/" ] && break _pvh_d="\${_pvh_d%/*}" [ -z "\${_pvh_d}" ] && _pvh_d="/" done if [ "\${_pvh_found_local}" = "0" ]; then - _PYENV_VH_PATHS+=("\${PYENV_ROOT}/version") + _PYENV_VH_PATHS="\${_PYENV_VH_PATHS} \${PYENV_ROOT}/version" fi - _PYENV_VH_MTIMES="\$(stat ${_stat_fmt} "\${_PYENV_VH_PATHS[@]}" 2>/dev/null)" + _PYENV_VH_MTIMES="\$(stat ${_stat_fmt} \${_PYENV_VH_PATHS} 2>/dev/null)" return \$ret }; if ! [[ "\${PROMPT_COMMAND-}" =~ _pyenv_virtualenv_hook ]]; then @@ -172,7 +182,7 @@ _pyenv_virtualenv_hook() { return \$ret fi if [ "\${PWD}" = "\${_PYENV_VH_PWD-}" ] \\ - && [ "\$(stat ${_stat_fmt} "\${_PYENV_VH_PATHS[@]}" 2>/dev/null)" = "\${_PYENV_VH_MTIMES-}" ]; then + && [ "\$(stat ${_stat_fmt} \${_PYENV_VH_PATHS} 2>/dev/null)" = "\${_PYENV_VH_MTIMES-}" ]; then return \$ret fi fi @@ -185,25 +195,25 @@ _pyenv_virtualenv_hook() { _PYENV_VH_VERSION="\${PYENV_VERSION-}" _PYENV_VH_VENV="\${VIRTUAL_ENV-}" local _pvh_d="\${PWD}" _pvh_found_local=0 - _PYENV_VH_PATHS=() + _PYENV_VH_PATHS="" while :; do if [ -f "\${_pvh_d}/.python-version" ] || [ -L "\${_pvh_d}/.python-version" ]; then - _PYENV_VH_PATHS+=("\${_pvh_d}/.python-version") + _PYENV_VH_PATHS="\${_PYENV_VH_PATHS} \${_pvh_d}/.python-version" if [ -f "\${_pvh_d}/.python-version" ]; then _pvh_found_local=1 break fi else - _PYENV_VH_PATHS+=("\${_pvh_d}") + _PYENV_VH_PATHS="\${_PYENV_VH_PATHS} \${_pvh_d}" fi [ "\${_pvh_d}" = "/" ] && break _pvh_d="\${_pvh_d%/*}" [ -z "\${_pvh_d}" ] && _pvh_d="/" done if [ "\${_pvh_found_local}" = "0" ]; then - _PYENV_VH_PATHS+=("\${PYENV_ROOT}/version") + _PYENV_VH_PATHS="\${_PYENV_VH_PATHS} \${PYENV_ROOT}/version" fi - _PYENV_VH_MTIMES="\$(stat ${_stat_fmt} "\${_PYENV_VH_PATHS[@]}" 2>/dev/null)" + _PYENV_VH_MTIMES="\$(stat ${_stat_fmt} \${_PYENV_VH_PATHS} 2>/dev/null)" return \$ret }; typeset -g -a precmd_functions From 118718c83442dc39ac1fe49dab9e738c69f51c2a Mon Sep 17 00:00:00 2001 From: Ivan Pozdeev Date: Tue, 21 Apr 2026 17:52:45 +0300 Subject: [PATCH 2/5] Revert "Bring back POSIX sh (dash) support" This reverts commit 57ac377dd8fbaa3e932ba34dea5e3b44389d552e. --- bin/pyenv-virtualenv-init | 12 ++++++------ test/init.bats | 34 ++++++++++++---------------------- 2 files changed, 18 insertions(+), 28 deletions(-) diff --git a/bin/pyenv-virtualenv-init b/bin/pyenv-virtualenv-init index 634ce6f8..138fb48e 100755 --- a/bin/pyenv-virtualenv-init +++ b/bin/pyenv-virtualenv-init @@ -214,7 +214,7 @@ EOS return \$ret fi if [ "\${PWD}" = "\${_PYENV_VH_PWD-}" ] \\ - && [ "\$(stat ${_stat_fmt} \${_PYENV_VH_PATHS} 2>/dev/null)" = "\${_PYENV_VH_MTIMES-}" ]; then + && [ "\$(stat ${_stat_fmt} "\${_PYENV_VH_PATHS[@]}" 2>/dev/null)" = "\${_PYENV_VH_MTIMES-}" ]; then return \$ret fi fi @@ -227,25 +227,25 @@ EOS _PYENV_VH_VERSION="\${PYENV_VERSION-}" _PYENV_VH_VENV="\${VIRTUAL_ENV-}" local _pvh_d="\${PWD}" _pvh_found_local=0 - _PYENV_VH_PATHS="" + _PYENV_VH_PATHS=() while :; do if [ -f "\${_pvh_d}/.python-version" ] || [ -L "\${_pvh_d}/.python-version" ]; then - _PYENV_VH_PATHS="\${_PYENV_VH_PATHS} \${_pvh_d}/.python-version" + _PYENV_VH_PATHS+=("\${_pvh_d}/.python-version") if [ -f "\${_pvh_d}/.python-version" ]; then _pvh_found_local=1 break fi else - _PYENV_VH_PATHS="\${_PYENV_VH_PATHS} \${_pvh_d}" + _PYENV_VH_PATHS+=("\${_pvh_d}") fi [ "\${_pvh_d}" = "/" ] && break _pvh_d="\${_pvh_d%/*}" [ -z "\${_pvh_d}" ] && _pvh_d="/" done if [ "\${_pvh_found_local}" = "0" ]; then - _PYENV_VH_PATHS="\${_PYENV_VH_PATHS} \${PYENV_ROOT}/version" + _PYENV_VH_PATHS+=("\${PYENV_ROOT}/version") fi - _PYENV_VH_MTIMES="\$(stat ${_stat_fmt} \${_PYENV_VH_PATHS} 2>/dev/null)" + _PYENV_VH_MTIMES="\$(stat ${_stat_fmt} "\${_PYENV_VH_PATHS[@]}" 2>/dev/null)" return \$ret }; EOS diff --git a/test/init.bats b/test/init.bats index 1b942cfc..2aacc8fc 100644 --- a/test/init.bats +++ b/test/init.bats @@ -39,16 +39,6 @@ load test_helper assert_output_contains 'eval "$(pyenv virtualenv-init -)"' } -@test "generated hook code is valid POSIX sh" { - # Get output directly to avoid bats env affecting shell detection - # The bash output should be POSIX sh-compatible (no bash arrays, etc.) - output=$(bash bin/pyenv-virtualenv-init - bash 2>/dev/null) - # Validate syntax with dash (POSIX sh) - result=$(echo "$output" | dash -n - 2>&1) - status=$? - [ $status -eq 0 ] || echo "dash failed: $result" >&2 -} - @test "fish instructions" { run pyenv-virtualenv-init fish assert [ "$status" -eq 1 ] @@ -71,7 +61,7 @@ _pyenv_virtualenv_hook() { return \$ret fi if [ "\${PWD}" = "\${_PYENV_VH_PWD-}" ] \\ - && [ "\$(stat ${_stat_fmt} \${_PYENV_VH_PATHS} 2>/dev/null)" = "\${_PYENV_VH_MTIMES-}" ]; then + && [ "\$(stat ${_stat_fmt} "\${_PYENV_VH_PATHS[@]}" 2>/dev/null)" = "\${_PYENV_VH_MTIMES-}" ]; then return \$ret fi fi @@ -84,25 +74,25 @@ _pyenv_virtualenv_hook() { _PYENV_VH_VERSION="\${PYENV_VERSION-}" _PYENV_VH_VENV="\${VIRTUAL_ENV-}" local _pvh_d="\${PWD}" _pvh_found_local=0 - _PYENV_VH_PATHS="" + _PYENV_VH_PATHS=() while :; do if [ -f "\${_pvh_d}/.python-version" ] || [ -L "\${_pvh_d}/.python-version" ]; then - _PYENV_VH_PATHS="\${_PYENV_VH_PATHS} \${_pvh_d}/.python-version" + _PYENV_VH_PATHS+=("\${_pvh_d}/.python-version") if [ -f "\${_pvh_d}/.python-version" ]; then _pvh_found_local=1 break fi else - _PYENV_VH_PATHS="\${_PYENV_VH_PATHS} \${_pvh_d}" + _PYENV_VH_PATHS+=("\${_pvh_d}") fi [ "\${_pvh_d}" = "/" ] && break _pvh_d="\${_pvh_d%/*}" [ -z "\${_pvh_d}" ] && _pvh_d="/" done if [ "\${_pvh_found_local}" = "0" ]; then - _PYENV_VH_PATHS="\${_PYENV_VH_PATHS} \${PYENV_ROOT}/version" + _PYENV_VH_PATHS+=("\${PYENV_ROOT}/version") fi - _PYENV_VH_MTIMES="\$(stat ${_stat_fmt} \${_PYENV_VH_PATHS} 2>/dev/null)" + _PYENV_VH_MTIMES="\$(stat ${_stat_fmt} "\${_PYENV_VH_PATHS[@]}" 2>/dev/null)" return \$ret }; if ! [[ "\${PROMPT_COMMAND-}" =~ _pyenv_virtualenv_hook ]]; then @@ -182,7 +172,7 @@ _pyenv_virtualenv_hook() { return \$ret fi if [ "\${PWD}" = "\${_PYENV_VH_PWD-}" ] \\ - && [ "\$(stat ${_stat_fmt} \${_PYENV_VH_PATHS} 2>/dev/null)" = "\${_PYENV_VH_MTIMES-}" ]; then + && [ "\$(stat ${_stat_fmt} "\${_PYENV_VH_PATHS[@]}" 2>/dev/null)" = "\${_PYENV_VH_MTIMES-}" ]; then return \$ret fi fi @@ -195,25 +185,25 @@ _pyenv_virtualenv_hook() { _PYENV_VH_VERSION="\${PYENV_VERSION-}" _PYENV_VH_VENV="\${VIRTUAL_ENV-}" local _pvh_d="\${PWD}" _pvh_found_local=0 - _PYENV_VH_PATHS="" + _PYENV_VH_PATHS=() while :; do if [ -f "\${_pvh_d}/.python-version" ] || [ -L "\${_pvh_d}/.python-version" ]; then - _PYENV_VH_PATHS="\${_PYENV_VH_PATHS} \${_pvh_d}/.python-version" + _PYENV_VH_PATHS+=("\${_pvh_d}/.python-version") if [ -f "\${_pvh_d}/.python-version" ]; then _pvh_found_local=1 break fi else - _PYENV_VH_PATHS="\${_PYENV_VH_PATHS} \${_pvh_d}" + _PYENV_VH_PATHS+=("\${_pvh_d}") fi [ "\${_pvh_d}" = "/" ] && break _pvh_d="\${_pvh_d%/*}" [ -z "\${_pvh_d}" ] && _pvh_d="/" done if [ "\${_pvh_found_local}" = "0" ]; then - _PYENV_VH_PATHS="\${_PYENV_VH_PATHS} \${PYENV_ROOT}/version" + _PYENV_VH_PATHS+=("\${PYENV_ROOT}/version") fi - _PYENV_VH_MTIMES="\$(stat ${_stat_fmt} \${_PYENV_VH_PATHS} 2>/dev/null)" + _PYENV_VH_MTIMES="\$(stat ${_stat_fmt} "\${_PYENV_VH_PATHS[@]}" 2>/dev/null)" return \$ret }; typeset -g -a precmd_functions From cdd19e9ba9e6d7f336e8ed40bbd0f6d20059afb0 Mon Sep 17 00:00:00 2001 From: Ivan Pozdeev Date: Tue, 21 Apr 2026 18:40:12 +0300 Subject: [PATCH 3/5] Refactor cache code generataion; don't generate prompt_command for non bash/zsh/fish --- bin/pyenv-virtualenv-init | 70 ++++++++++++++++++--------------------- test/init.bats | 26 +++++++++------ 2 files changed, 49 insertions(+), 47 deletions(-) diff --git a/bin/pyenv-virtualenv-init b/bin/pyenv-virtualenv-init index 138fb48e..1623970c 100755 --- a/bin/pyenv-virtualenv-init +++ b/bin/pyenv-virtualenv-init @@ -120,22 +120,13 @@ esac case "$shell" in fish ) - if [ -n "$_has_version_hooks" ]; then - cat </dev/null) - return \$ret -end EOS fi - ;; -ksh ) + cat </dev/null)" +EOS + fi + + cat < Date: Tue, 21 Apr 2026 18:44:01 +0300 Subject: [PATCH 4/5] fix --- bin/pyenv-virtualenv-init | 23 +++++++++++------------ 1 file changed, 11 insertions(+), 12 deletions(-) diff --git a/bin/pyenv-virtualenv-init b/bin/pyenv-virtualenv-init index 1623970c..03feec7d 100755 --- a/bin/pyenv-virtualenv-init +++ b/bin/pyenv-virtualenv-init @@ -247,24 +247,23 @@ EOS ;; esac - case "$shell" in - bash ) - cat < Date: Tue, 21 Apr 2026 18:48:10 +0300 Subject: [PATCH 5/5] adjust tests --- test/init.bats | 2 -- 1 file changed, 2 deletions(-) diff --git a/test/init.bats b/test/init.bats index 97a5d95b..11e3a1ba 100644 --- a/test/init.bats +++ b/test/init.bats @@ -50,7 +50,6 @@ export PATH="${TMP}/pyenv/plugins/pyenv-virtualenv/shims:\${PATH}"; export PYENV_VIRTUALENV_INIT=1; _pyenv_virtualenv_hook() { local ret=\$? - # Cache: env vars checked once, path list and stat rebuilt on miss only if [ "\${PYENV_VERSION-}" = "\${_PYENV_VH_VERSION-}" ] \\ && [ "\${VIRTUAL_ENV-}" = "\${_PYENV_VH_VENV-}" ]; then if [ -n "\${PYENV_VERSION-}" ]; then @@ -161,7 +160,6 @@ export PATH="${TMP}/pyenv/plugins/pyenv-virtualenv/shims:\${PATH}"; export PYENV_VIRTUALENV_INIT=1; _pyenv_virtualenv_hook() { local ret=\$? - # Cache: env vars checked once, path list and stat rebuilt on miss only if [ "\${PYENV_VERSION-}" = "\${_PYENV_VH_VERSION-}" ] \\ && [ "\${VIRTUAL_ENV-}" = "\${_PYENV_VH_VENV-}" ]; then if [ -n "\${PYENV_VERSION-}" ]; then