From 80e700ec217e8b5835f9c88dc02adb77c17d2f63 Mon Sep 17 00:00:00 2001 From: debaditya Date: Thu, 3 Sep 2026 20:54:28 -0700 Subject: [PATCH] [Fix] `nvm_alias`: unset zsh's `extendedglob` while reading an alias file `#` is a repetition operator under `extendedglob`, so `${VAR%%#*}` is an invalid pattern: zsh aborts the expansion and `nvm_alias` silently prints nothing, leaving the `default` alias unresolved when nvm.sh is sourced. The read loop moves into `nvm_print_alias_file` so that `local_options` scopes the option change to that section alone, rather than to everything `nvm_alias` does, and restores whatever the caller had set. Fixes #3885 Co-authored-by: debaditya Co-authored-by: Jordan Harband --- nvm.sh | 30 +++++++---- test/fast/Unit tests/nvm_print_alias_file | 53 +++++++++++++++++++ ...th zsh extendedglob should use the default | 33 ++++++++++++ 3 files changed, 105 insertions(+), 11 deletions(-) create mode 100755 test/fast/Unit tests/nvm_print_alias_file create mode 100755 test/sourcing/Sourcing nvm.sh with zsh extendedglob should use the default diff --git a/nvm.sh b/nvm.sh index 31d6c765..c2d869ab 100755 --- a/nvm.sh +++ b/nvm.sh @@ -1427,6 +1427,23 @@ nvm_list_aliases() { return } +nvm_print_alias_file() { + # a `#` pattern is a repetition operator under zsh's `extendedglob`, so it is + # unset for the length of this function and restored by `local_options` + nvm_is_zsh && setopt local_options noextendedglob + + local NVM_ALIAS_LINE + while IFS= read -r NVM_ALIAS_LINE || [ -n "${NVM_ALIAS_LINE}" ]; do + NVM_ALIAS_LINE="${NVM_ALIAS_LINE%%#*}" + case "${NVM_ALIAS_LINE}" in + *[![:space:]]*) ;; + *) continue ;; + esac + NVM_ALIAS_LINE="${NVM_ALIAS_LINE%"${NVM_ALIAS_LINE##*[![:space:]]}"}" + nvm_echo "${NVM_ALIAS_LINE}" + done < "$1" +} + nvm_alias() { local ALIAS ALIAS="${1-}" @@ -1457,16 +1474,7 @@ nvm_alias() { return 0 fi - local NVM_ALIAS_LINE - while IFS= read -r NVM_ALIAS_LINE || [ -n "${NVM_ALIAS_LINE}" ]; do - NVM_ALIAS_LINE="${NVM_ALIAS_LINE%%#*}" - case "${NVM_ALIAS_LINE}" in - *[![:space:]]*) ;; - *) continue ;; - esac - NVM_ALIAS_LINE="${NVM_ALIAS_LINE%"${NVM_ALIAS_LINE##*[![:space:]]}"}" - nvm_echo "${NVM_ALIAS_LINE}" - done < "${NVM_ALIAS_PATH}" + nvm_print_alias_file "${NVM_ALIAS_PATH}" } nvm_ls_current() { @@ -4961,7 +4969,7 @@ nvm() { nvm_has_solaris_binary nvm_is_merged_node_version \ nvm_is_natural_num nvm_is_version_installed nvm_validate_install \ nvm_install_lock_name nvm_acquire_install_lock nvm_release_install_lock \ - nvm_list_aliases nvm_make_alias nvm_print_alias_path \ + nvm_list_aliases nvm_make_alias nvm_print_alias_file nvm_print_alias_path \ nvm_print_default_alias nvm_print_formatted_alias nvm_resolve_local_alias \ nvm_sanitize_path nvm_has_colors nvm_process_parameters \ nvm_node_version_has_solaris_binary nvm_iojs_version_has_solaris_binary \ diff --git a/test/fast/Unit tests/nvm_print_alias_file b/test/fast/Unit tests/nvm_print_alias_file new file mode 100755 index 00000000..3ce1b29f --- /dev/null +++ b/test/fast/Unit tests/nvm_print_alias_file @@ -0,0 +1,53 @@ +#!/bin/sh + +die () { echo "$@" ; cleanup ; exit 1; } + +cleanup () { + rm -rf "${TEST_DIR-}" + unset TEST_DIR OUTPUT EXPECTED +} + +: nvm.sh +\. ../../../nvm.sh + +TEST_DIR="${PWD}/nvm_print_alias_file_tmp" +mkdir -p "${TEST_DIR}" || die 'failed to create test dir' + +printf 'v0.10 # inline comment\n' > "${TEST_DIR}/inline" +OUTPUT="$(nvm_print_alias_file "${TEST_DIR}/inline")" +[ "_${OUTPUT}" = '_v0.10' ] || die "inline comment not stripped; got '${OUTPUT}'" + +printf '# leading comment\nv0.11\n' > "${TEST_DIR}/comment-first" +OUTPUT="$(nvm_print_alias_file "${TEST_DIR}/comment-first")" +[ "_${OUTPUT}" = '_v0.11' ] || die "comment-only line not skipped; got '${OUTPUT}'" + +printf 'v0.12\t \nv0.13\n' > "${TEST_DIR}/trailing-space" +OUTPUT="$(nvm_print_alias_file "${TEST_DIR}/trailing-space")" +EXPECTED="$(printf 'v0.12\nv0.13')" +[ "_${OUTPUT}" = "_${EXPECTED}" ] || die "trailing whitespace not stripped, or a line was dropped; got '${OUTPUT}'" + +printf '\n \n\t\n# only a comment\n' > "${TEST_DIR}/nothing-usable" +OUTPUT="$(nvm_print_alias_file "${TEST_DIR}/nothing-usable")" +[ "_${OUTPUT}" = '_' ] || die "blank and comment-only lines should produce no output; got '${OUTPUT}'" + +nvm_print_alias_file "${TEST_DIR}/nothing-usable" >/dev/null 2>&1 \ + || die 'a file with no usable lines should still exit zero' + +printf 'v0.14' > "${TEST_DIR}/no-trailing-newline" +OUTPUT="$(nvm_print_alias_file "${TEST_DIR}/no-trailing-newline")" +[ "_${OUTPUT}" = '_v0.14' ] || die "a final line with no trailing newline should be read; got '${OUTPUT}'" + +# a `#` pattern is a repetition operator under zsh's `extendedglob`, which is what +# made `nvm_alias` print nothing at all: https://github.com/nvm-sh/nvm/issues/3885 +if [ -n "${ZSH_VERSION-}" ]; then + setopt extendedglob || die 'enabling extendedglob failed' + OUTPUT="$(nvm_print_alias_file "${TEST_DIR}/inline")" + [ "_${OUTPUT}" = '_v0.10' ] || die "with extendedglob set, expected 'v0.10', got '${OUTPUT}'" + [[ -o extendedglob ]] || die 'nvm_print_alias_file left extendedglob unset' + + unsetopt extendedglob + nvm_print_alias_file "${TEST_DIR}/inline" > /dev/null + [[ -o extendedglob ]] && die 'nvm_print_alias_file left extendedglob set' +fi + +cleanup diff --git a/test/sourcing/Sourcing nvm.sh with zsh extendedglob should use the default b/test/sourcing/Sourcing nvm.sh with zsh extendedglob should use the default new file mode 100755 index 00000000..f30f51b8 --- /dev/null +++ b/test/sourcing/Sourcing nvm.sh with zsh extendedglob should use the default @@ -0,0 +1,33 @@ +#!/bin/sh + +die () { echo "$@" ; exit 1; } + +\. ../common.sh + +# `#` is a pattern operator only under zsh's `extendedglob` +[ -n "${ZSH_VERSION-}" ] || exit 0 + +# We need to unload nvm again first, as by the time this test is run, +# despite being unloaded in setup, the inherited PATH still contains +# an nvm-installed node version. `nvm unload` unsets NVM_DIR, so cache it. +\. ../../nvm.sh || die 'sourcing returned nonzero exit code' +NVM_DIR_CACHED="${NVM_DIR}" +nvm unload || die 'unloading returned nonzero exit code' +NVM_DIR="${NVM_DIR_CACHED}" + +# the comment is the point: stripping it is what needs a `#` pattern +echo '0.10.1 # comment' > "${NVM_DIR}/alias/default" || die 'creation of default alias failed' + +setopt extendedglob || die 'enabling extendedglob failed' + +\. ../../nvm.sh || die 'sourcing returned nonzero exit code' + +[[ -o extendedglob ]] || die 'sourcing nvm.sh disabled extendedglob' + +NVM_LS_CURRENT_NOT_GREPPED="$(nvm ls current | strip_colors)" +NVM_LS_CURRENT="$(nvm ls current | strip_colors | \grep -o 'v0\.10\.1')" +[ "_${NVM_LS_CURRENT}" = '_v0.10.1' ] || die "'nvm ls current' did not return '-> v0.10.1', got '${NVM_LS_CURRENT_NOT_GREPPED}'" + +NVM_ALIAS_DEFAULT="$(nvm alias default | strip_colors)" +[ "_${NVM_ALIAS_DEFAULT}" = '_default -> 0.10.1 (-> v0.10.1 *)' ] \ + || die "'nvm alias default' did not return 'default -> 0.10.1 (-> v0.10.1 *)', got '${NVM_ALIAS_DEFAULT}'"