From d22bf7f5384b80377e6baaa005a66019624c1bd3 Mon Sep 17 00:00:00 2001 From: Jake Lodwick <25925+jakelodwick@users.noreply.github.com> Date: Tue, 17 Feb 2026 13:44:31 -0700 Subject: [PATCH] [Refactor] `nvm_alias`, `nvm_resolve_alias`: use builtins MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [Tests] `nvm_alias`, `nvm_resolve_alias`: add edge-case tests nvm_alias() used a sed/awk pipeline to strip comments and blank lines from alias files that almost always contain a single word. A while-read loop with parameter expansion does the same filtering more directly. nvm_resolve_alias() piped nvm_alias through head and tail to extract one line, and used printf/grep for cycle detection. Parameter expansion and a case statement replace both without the extra plumbing. All replacements are POSIX (read -r, case, IFS=, parameter expansion). As a side effect, this also removes 4 external process invocations during shell init. [Fix] `nvm_resolve_alias`: detect cycles via newline-anchored `case` The original commit referenced above changed SEEN_ALIASES from `\n`-delimited storage (interpreted by `printf '%b' | nvm_grep -e "^${name}$"`) to space- delimited but left the line-anchored grep in place — without newlines in the haystack the anchored pattern can never match, so cycles never break. Switch to literal-newline storage and a `case` pattern anchored on those newlines. Newline anchoring also handles alias names containing spaces, which token-based patterns false-positive on (e.g. lookup of `bar` matches substring " bar " inside " foo bar midway " when the chain visits the multi-token alias `foo bar`). New test file covers self-loop, multi-hop loop, cycle through a space-bearing alias name, and a non-cycle through a space-bearing alias name. Existing `test/fast/Aliases/circular/` fixtures continue to pass. --- nvm.sh | 43 +++++++++++---- .../nvm_alias handles comment-only alias file | 17 ++++++ .../nvm_alias handles empty alias file | 17 ++++++ .../nvm_alias strips trailing whitespace | 16 ++++++ .../Aliases/nvm_resolve_alias handles cycles | 52 +++++++++++++++++++ .../nvm_resolve_alias handles deep chain | 20 +++++++ .../nvm_resolve_alias with nonexistent target | 19 +++++++ 7 files changed, 174 insertions(+), 10 deletions(-) create mode 100755 test/fast/Aliases/nvm_alias handles comment-only alias file create mode 100755 test/fast/Aliases/nvm_alias handles empty alias file create mode 100755 test/fast/Aliases/nvm_alias strips trailing whitespace create mode 100755 test/fast/Aliases/nvm_resolve_alias handles cycles create mode 100755 test/fast/Aliases/nvm_resolve_alias handles deep chain create mode 100755 test/fast/Aliases/nvm_resolve_alias with nonexistent target diff --git a/nvm.sh b/nvm.sh index ff35373e..37e39668 100755 --- a/nvm.sh +++ b/nvm.sh @@ -1370,7 +1370,22 @@ nvm_alias() { return 0 fi - command sed 's/#.*//; s/[[:space:]]*$//' "${NVM_ALIAS_PATH}" | command awk 'NF' + 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 + *[!\ \ ]*) ;; + *) continue ;; + esac + while : ; do + case "${NVM_ALIAS_LINE}" in + *' ') NVM_ALIAS_LINE="${NVM_ALIAS_LINE% }" ;; + *' ') NVM_ALIAS_LINE="${NVM_ALIAS_LINE% }" ;; + *) break ;; + esac + done + nvm_echo "${NVM_ALIAS_LINE}" + done < "${NVM_ALIAS_PATH}" } nvm_ls_current() { @@ -1403,24 +1418,32 @@ nvm_resolve_alias() { local ALIAS ALIAS="${PATTERN}" local ALIAS_TEMP + local ALIAS_OUTPUT local SEEN_ALIASES - SEEN_ALIASES="${ALIAS}" - local NVM_ALIAS_INDEX - NVM_ALIAS_INDEX=1 + SEEN_ALIASES=" +${ALIAS} +" while true; do - ALIAS_TEMP="$( (nvm_alias "${ALIAS}" 2>/dev/null | command head -n "${NVM_ALIAS_INDEX}" | command tail -n 1) || nvm_echo)" + ALIAS_OUTPUT="$(nvm_alias "${ALIAS}" 2>/dev/null)" || ALIAS_OUTPUT='' + ALIAS_TEMP="${ALIAS_OUTPUT%% +*}" if [ -z "${ALIAS_TEMP}" ]; then break fi - if command printf '%b' "${SEEN_ALIASES}" | nvm_grep -q -e "^${ALIAS_TEMP}$"; then - ALIAS="∞" - break - fi + case "${SEEN_ALIASES}" in + *" +${ALIAS_TEMP} +"*) + ALIAS="∞" + break + ;; + esac - SEEN_ALIASES="${SEEN_ALIASES}\\n${ALIAS_TEMP}" + SEEN_ALIASES="${SEEN_ALIASES}${ALIAS_TEMP} +" ALIAS="${ALIAS_TEMP}" done diff --git a/test/fast/Aliases/nvm_alias handles comment-only alias file b/test/fast/Aliases/nvm_alias handles comment-only alias file new file mode 100755 index 00000000..cb8fa077 --- /dev/null +++ b/test/fast/Aliases/nvm_alias handles comment-only alias file @@ -0,0 +1,17 @@ +#!/bin/sh + +die () { echo "$@" ; exit 1; } + +export NVM_DIR="$(cd ../../.. && pwd)" + +: nvm.sh +\. "${NVM_DIR}/nvm.sh" + +# Create an alias file containing only comments +printf '# this is a comment\n# another comment\n' > ../../../alias/test-edge-comments +ACTUAL="$(nvm_alias test-edge-comments 2>/dev/null)" +EXIT_CODE="$(nvm_alias test-edge-comments 2>/dev/null; echo $?)" +[ -z "${ACTUAL}" ] || die "expected empty output for comment-only alias file, got >${ACTUAL}<" +[ "${EXIT_CODE}" = '0' ] || die "expected exit code 0, got ${EXIT_CODE}" + +rm -f ../../../alias/test-edge-comments diff --git a/test/fast/Aliases/nvm_alias handles empty alias file b/test/fast/Aliases/nvm_alias handles empty alias file new file mode 100755 index 00000000..3f80c73a --- /dev/null +++ b/test/fast/Aliases/nvm_alias handles empty alias file @@ -0,0 +1,17 @@ +#!/bin/sh + +die () { echo "$@" ; exit 1; } + +export NVM_DIR="$(cd ../../.. && pwd)" + +: nvm.sh +\. "${NVM_DIR}/nvm.sh" + +# Create an empty alias file +printf '' > ../../../alias/test-edge-empty +ACTUAL="$(nvm_alias test-edge-empty 2>/dev/null)" +EXIT_CODE="$(nvm_alias test-edge-empty 2>/dev/null; echo $?)" +[ -z "${ACTUAL}" ] || die "expected empty output for empty alias file, got >${ACTUAL}<" +[ "${EXIT_CODE}" = '0' ] || die "expected exit code 0, got ${EXIT_CODE}" + +rm -f ../../../alias/test-edge-empty diff --git a/test/fast/Aliases/nvm_alias strips trailing whitespace b/test/fast/Aliases/nvm_alias strips trailing whitespace new file mode 100755 index 00000000..caf53bb4 --- /dev/null +++ b/test/fast/Aliases/nvm_alias strips trailing whitespace @@ -0,0 +1,16 @@ +#!/bin/sh + +die () { echo "$@" ; exit 1; } + +export NVM_DIR="$(cd ../../.. && pwd)" + +: nvm.sh +\. "${NVM_DIR}/nvm.sh" + +# Create an alias file with trailing spaces and tabs +printf '22.1.0 \t \n' > ../../../alias/test-edge-trailing-ws +ACTUAL="$(nvm_alias test-edge-trailing-ws)" +EXPECTED='22.1.0' +[ "${ACTUAL}" = "${EXPECTED}" ] || die "expected >${EXPECTED}<, got >${ACTUAL}<" + +rm -f ../../../alias/test-edge-trailing-ws diff --git a/test/fast/Aliases/nvm_resolve_alias handles cycles b/test/fast/Aliases/nvm_resolve_alias handles cycles new file mode 100755 index 00000000..38567426 --- /dev/null +++ b/test/fast/Aliases/nvm_resolve_alias handles cycles @@ -0,0 +1,52 @@ +#!/bin/sh + +die () { echo "$@" ; exit 1; } + +export NVM_DIR="$(cd ../../.. && pwd)" + +: nvm.sh +\. "${NVM_DIR}/nvm.sh" + +# 1-hop self-reference: alias points to itself +echo 'self' > '../../../alias/self' + +ACTUAL="$(nvm_resolve_alias self)" +EXPECTED='∞' +[ "${ACTUAL}" = "${EXPECTED}" ] || die "expected >${EXPECTED}< for self-reference, got >${ACTUAL}<" + +rm -f '../../../alias/self' + +# Multi-hop loop: link1 -> link2 -> link3 -> link1 +echo 'link2' > '../../../alias/link1' +echo 'link3' > '../../../alias/link2' +echo 'link1' > '../../../alias/link3' + +ACTUAL="$(nvm_resolve_alias link1)" +EXPECTED='∞' +[ "${ACTUAL}" = "${EXPECTED}" ] || die "expected >${EXPECTED}< for 3-hop cycle, got >${ACTUAL}<" + +rm -f '../../../alias/link1' '../../../alias/link2' '../../../alias/link3' + +# Cycle through an alias name containing a space +echo 'midway' > '../../../alias/foo bar' +echo 'foo bar' > '../../../alias/midway' + +ACTUAL="$(nvm_resolve_alias 'foo bar')" +EXPECTED='∞' +[ "${ACTUAL}" = "${EXPECTED}" ] || die "expected >${EXPECTED}< for space-name cycle, got >${ACTUAL}<" + +rm -f '../../../alias/foo bar' '../../../alias/midway' + +# Non-cycle through an alias name containing a space. +# Token-based cycle detection would false-positive on this chain because +# 'bar' appears as a substring of the multi-token alias name 'foo bar'. +# Resolves: 'foo bar' -> 'midway' -> 'bar' -> 0.0.99 +echo 'midway' > '../../../alias/foo bar' +echo 'bar' > '../../../alias/midway' +echo '0.0.99' > '../../../alias/bar' + +ACTUAL="$(nvm_resolve_alias 'foo bar')" +EXPECTED='v0.0.99' +[ "${ACTUAL}" = "${EXPECTED}" ] || die "expected >${EXPECTED}< for space-name chain, got >${ACTUAL}<" + +rm -f '../../../alias/foo bar' '../../../alias/midway' '../../../alias/bar' diff --git a/test/fast/Aliases/nvm_resolve_alias handles deep chain b/test/fast/Aliases/nvm_resolve_alias handles deep chain new file mode 100755 index 00000000..435d35ed --- /dev/null +++ b/test/fast/Aliases/nvm_resolve_alias handles deep chain @@ -0,0 +1,20 @@ +#!/bin/sh + +die () { echo "$@" ; exit 1; } + +export NVM_DIR="$(cd ../../.. && pwd)" + +: nvm.sh +\. "${NVM_DIR}/nvm.sh" + +# Create a 4-deep alias chain: hop1 -> hop2 -> hop3 -> hop4 -> 0.0.99 +echo 'hop2' > ../../../alias/hop1 +echo 'hop3' > ../../../alias/hop2 +echo 'hop4' > ../../../alias/hop3 +echo '0.0.99' > ../../../alias/hop4 + +ACTUAL="$(nvm_resolve_alias hop1)" +EXPECTED='v0.0.99' +[ "${ACTUAL}" = "${EXPECTED}" ] || die "expected >${EXPECTED}< for 4-deep chain, got >${ACTUAL}<" + +rm -f ../../../alias/hop1 ../../../alias/hop2 ../../../alias/hop3 ../../../alias/hop4 diff --git a/test/fast/Aliases/nvm_resolve_alias with nonexistent target b/test/fast/Aliases/nvm_resolve_alias with nonexistent target new file mode 100755 index 00000000..48eb9596 --- /dev/null +++ b/test/fast/Aliases/nvm_resolve_alias with nonexistent target @@ -0,0 +1,19 @@ +#!/bin/sh + +die () { echo "$@" ; exit 1; } + +export NVM_DIR="$(cd ../../.. && pwd)" + +: nvm.sh +\. "${NVM_DIR}/nvm.sh" + +# Create an alias pointing to a version that does not exist as an alias +echo '99.99.99' > ../../../alias/test-edge-noexist + +ACTUAL="$(nvm_resolve_alias test-edge-noexist)" +EXPECTED='v99.99.99' +EXIT_CODE="$(nvm_resolve_alias test-edge-noexist >/dev/null 2>&1; echo $?)" +[ "${ACTUAL}" = "${EXPECTED}" ] || die "expected >${EXPECTED}<, got >${ACTUAL}<" +[ "${EXIT_CODE}" = '0' ] || die "expected exit code 0, got ${EXIT_CODE}" + +rm -f ../../../alias/test-edge-noexist