From 9142a92cdc2d858c4ea3fe01328fabdbba91cc7d Mon Sep 17 00:00:00 2001 From: Jordan Harband Date: Wed, 8 Jul 2026 13:03:16 -0700 Subject: [PATCH] [Fix] `nvm_download`, `nvm_get_latest`, `install`: only select a downloader that exists as an executable `nvm_has` matches shell functions and aliases, but downloads now run via `command`, which skips them - a `curl` shell function with no curl binary on the PATH would select the curl path and fail with exit 127, instead of falling back to an available wget executable. The new `nvm_has_executable` helper resolves names the same way `command` does, so downloader selection and execution agree. --- install.sh | 22 +++++++++++++---- nvm.sh | 24 ++++++++++++++----- test/fast/Unit tests/nvm install --offline | 3 ++- ...load and curl helpers bypass shadowed curl | 18 ++++++++++++++ .../nvm_download wget Authorization header | 4 ++-- .../nvm_get_latest missing curl or wget | 3 ++- 6 files changed, 59 insertions(+), 15 deletions(-) diff --git a/install.sh b/install.sh index c16bc6aa..41fd22ac 100755 --- a/install.sh +++ b/install.sh @@ -6,6 +6,18 @@ nvm_has() { type "$1" > /dev/null 2>&1 } +# resolves like `command "${1}"` does: only executables on the PATH, +# ignoring shell functions and aliases +nvm_has_executable() { + ( + # `|| true` so that shells with errexit-style options (eg, zsh's ERR_RETURN) + # do not abort the subshell when the name is not an alias or a function + unalias "${1-}" 2>/dev/null || true + unset -f "${1-}" 2>/dev/null || true + command -v "${1-}" > /dev/null 2>&1 + ) +} + nvm_echo() { command printf %s\\n "$*" 2>/dev/null } @@ -105,9 +117,9 @@ nvm_node_version() { } nvm_download() { - if nvm_has "curl"; then + if nvm_has_executable "curl"; then curl --fail --compressed -q "$@" - elif nvm_has "wget"; then + elif nvm_has_executable "wget"; then # Emulate curl with wget ARGS=$(nvm_echo "$@" | command sed -e 's/--progress-bar /--progress=bar /' \ -e 's/--compressed //' \ @@ -401,7 +413,7 @@ nvm_do_install() { # Autodetect install method if nvm_has git; then install_nvm_from_git - elif nvm_has curl || nvm_has wget; then + elif nvm_has_executable curl || nvm_has_executable wget; then install_nvm_as_script else nvm_echo >&2 'You need git, curl, or wget to install nvm' @@ -414,7 +426,7 @@ nvm_do_install() { fi install_nvm_from_git elif [ "${METHOD}" = 'script' ]; then - if ! nvm_has curl && ! nvm_has wget; then + if ! nvm_has_executable curl && ! nvm_has_executable wget; then nvm_echo >&2 "You need curl or wget to install nvm" exit 1 fi @@ -496,7 +508,7 @@ nvm_do_install() { # during the execution of the install script # nvm_reset() { - unset -f nvm_has nvm_install_dir nvm_latest_version nvm_profile_is_bash_or_zsh \ + unset -f nvm_has nvm_has_executable nvm_install_dir nvm_latest_version nvm_profile_is_bash_or_zsh \ nvm_source nvm_node_version nvm_download install_nvm_from_git nvm_install_node \ install_nvm_as_script nvm_try_profile nvm_detect_profile nvm_check_global_modules \ nvm_do_install nvm_reset nvm_default_install_dir nvm_grep diff --git a/nvm.sh b/nvm.sh index c410569a..d291f119 100755 --- a/nvm.sh +++ b/nvm.sh @@ -49,6 +49,18 @@ nvm_has() { type "${1-}" >/dev/null 2>&1 } +# resolves like `command "${1}"` does: only executables on the PATH, +# ignoring shell functions and aliases +nvm_has_executable() { + ( + # `|| true` so that shells with errexit-style options (eg, zsh's ERR_RETURN) + # do not abort the subshell when the name is not an alias or a function + unalias "${1-}" 2>/dev/null || true + unset -f "${1-}" 2>/dev/null || true + command -v "${1-}" >/dev/null 2>&1 + ) +} + nvm_has_non_aliased() { nvm_has "${1-}" && ! nvm_is_alias "${1-}" } @@ -97,12 +109,12 @@ nvm_curl_use_compression() { nvm_get_latest() { local NVM_LATEST_URL local CURL_COMPRESSED_FLAG - if nvm_has "curl"; then + if nvm_has_executable "curl"; then if nvm_curl_use_compression; then CURL_COMPRESSED_FLAG="--compressed" fi NVM_LATEST_URL="$(curl ${CURL_COMPRESSED_FLAG:-} -q -w "%{url_effective}\\n" -L -s -S https://latest.nvm.sh -o /dev/null)" - elif nvm_has "wget"; then + elif nvm_has_executable "wget"; then NVM_LATEST_URL="$(command wget -q https://latest.nvm.sh --server-response -O /dev/null 2>&1 | command awk '/^ Location: /{DEST=$2} END{ print DEST }')" else nvm_err 'nvm needs curl or wget to proceed.' @@ -127,13 +139,13 @@ nvm_download() { local NVM_DOWNLOADER NVM_DOWNLOADER='' - if nvm_has "curl"; then + if nvm_has_executable "curl"; then NVM_DOWNLOADER='curl' set -- -q --fail "$@" if nvm_curl_use_compression; then set -- --compressed "$@" fi - elif nvm_has "wget"; then + elif nvm_has_executable "wget"; then NVM_DOWNLOADER='wget' # Emulate curl with wget local NVM_DOWNLOAD_WGET_COUNT @@ -3569,7 +3581,7 @@ nvm() { esac done - if [ "${NVM_OFFLINE}" != 1 ] && ! nvm_has "curl" && ! nvm_has "wget"; then + if [ "${NVM_OFFLINE}" != 1 ] && ! nvm_has_executable "curl" && ! nvm_has_executable "wget"; then nvm_err 'nvm needs curl or wget to proceed.' return 1 fi @@ -4688,7 +4700,7 @@ nvm() { nvm_version_greater nvm_version_greater_than_or_equal_to \ nvm_print_npm_version nvm_install_latest_npm nvm_npm_global_modules \ nvm_has_system_node nvm_has_system_iojs \ - nvm_download nvm_get_latest nvm_has nvm_install_default_packages nvm_get_default_packages \ + nvm_download nvm_get_latest nvm_has nvm_has_executable nvm_install_default_packages nvm_get_default_packages \ nvm_curl_use_compression nvm_curl_version \ nvm_auto nvm_supports_xz \ nvm_echo nvm_err nvm_grep nvm_cd \ diff --git a/test/fast/Unit tests/nvm install --offline b/test/fast/Unit tests/nvm install --offline index 7247163f..d4e7cbbc 100644 --- a/test/fast/Unit tests/nvm install --offline +++ b/test/fast/Unit tests/nvm install --offline @@ -30,9 +30,10 @@ nvm_echo "$CAPTURED_STDERR" | nvm_grep -q "${EXPECTED_ERR}" \ # --offline should not require curl or wget nvm_has() { return 1; } +nvm_has_executable() { return 1; } try_err nvm install --offline 999.999.999 # Should fail with "not found" not "nvm needs curl or wget" nvm_echo "$CAPTURED_STDERR" | nvm_grep -q "curl or wget" \ && die "nvm install --offline should not require curl or wget" alias nvm_has='\nvm_has' -unset -f nvm_has +unset -f nvm_has nvm_has_executable diff --git a/test/fast/Unit tests/nvm_download and curl helpers bypass shadowed curl b/test/fast/Unit tests/nvm_download and curl helpers bypass shadowed curl index d6746fe8..7ca8db85 100755 --- a/test/fast/Unit tests/nvm_download and curl helpers bypass shadowed curl +++ b/test/fast/Unit tests/nvm_download and curl helpers bypass shadowed curl @@ -45,4 +45,22 @@ nvm_curl_libz_support || die 'nvm_curl_libz_support used the shadowing curl func nvm_download -s 'http://example.com/' -o /dev/null || die 'nvm_download used the shadowing curl function and failed' grep -Fxq 'http://example.com/' "$ARGV_LOG" || die "nvm_download did not invoke the curl executable; got: $(cat "$ARGV_LOG")" +# when no curl executable exists, a shadowing curl shell function must not fool +# downloader selection: nvm_download must fall back to the wget executable +WGET_ONLY_BIN="$WORK/wget-only-bin" +mkdir -p "$WGET_ONLY_BIN" +{ + echo '#!/bin/sh' + echo ': > "$ARGV_LOG"' + echo 'for a in "$@"; do printf "%s\n" "$a" >> "$ARGV_LOG"; done' +} > "$WGET_ONLY_BIN/wget" +chmod +x "$WGET_ONLY_BIN/wget" + +( + PATH="$WGET_ONLY_BIN" + export PATH + nvm_download -s 'http://wget-fallback.example.com/' -o /dev/null +) || die 'nvm_download did not fall back to wget when curl is only a shell function' +grep -Fxq 'http://wget-fallback.example.com/' "$ARGV_LOG" || die "nvm_download did not invoke the wget executable; got: $(cat "$ARGV_LOG")" + cleanup diff --git a/test/fast/Unit tests/nvm_download wget Authorization header b/test/fast/Unit tests/nvm_download wget Authorization header index a291d278..f0900c0a 100755 --- a/test/fast/Unit tests/nvm_download wget Authorization header +++ b/test/fast/Unit tests/nvm_download wget Authorization header @@ -6,7 +6,7 @@ TEST_BIN="$WORK/bin" ARGV_LOG="$WORK/argv.log" cleanup() { - unset -f die cleanup nvm_has + unset -f die cleanup nvm_has_executable rm -rf "$WORK" export PATH="$OLDPATH" } @@ -30,7 +30,7 @@ chmod +x "$TEST_BIN/wget" export ARGV_LOG export PATH="$TEST_BIN:$OLDPATH" # force the wget path while keeping system tools (sed) available for sanitization -nvm_has() { [ "$1" != curl ] && command -v "$1" >/dev/null 2>&1; } +nvm_has_executable() { [ "$1" != curl ] && command -v "$1" >/dev/null 2>&1; } # given an Authorization credential in NVM_AUTH_HEADER # when nvm_download uses the wget path diff --git a/test/fast/Unit tests/nvm_get_latest missing curl or wget b/test/fast/Unit tests/nvm_get_latest missing curl or wget index 23788dac..f47e6e6f 100755 --- a/test/fast/Unit tests/nvm_get_latest missing curl or wget +++ b/test/fast/Unit tests/nvm_get_latest missing curl or wget @@ -3,7 +3,7 @@ die () { echo "$@" ; cleanup ; exit 1; } cleanup() { - unset -f nvm_has + unset -f nvm_has nvm_has_executable } : nvm.sh @@ -12,6 +12,7 @@ cleanup() { \. ../../common.sh nvm_has() { return 1 ; } +nvm_has_executable() { return 1 ; } try_err nvm_get_latest [ "_$CAPTURED_STDERR" = "_nvm needs curl or wget to proceed." ] \