Skip to content
Open
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 21 additions & 6 deletions nvm.sh
Original file line number Diff line number Diff line change
Expand Up @@ -1323,7 +1323,21 @@ nvm_alias() {
return 2
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
*[![:space:]]*) ;;
*) continue ;;
esac
while : ; do
case "${NVM_ALIAS_LINE}" in
*[[:space:]]) NVM_ALIAS_LINE="${NVM_ALIAS_LINE%[[:space:]]}" ;;
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Stripping one whitespace char per iteration is O(n²) for trailing whitespace. Not a real concern for alias files in practice, but a single-pass equivalent (e.g. NVM_ALIAS_LINE="${NVM_ALIAS_LINE%"${NVM_ALIAS_LINE##*[![:space:]]}"}") avoids the loop entirely.

*) break ;;
esac
done
nvm_echo "${NVM_ALIAS_LINE}"
done < "${NVM_ALIAS_PATH}"
}

nvm_ls_current() {
Expand Down Expand Up @@ -1356,13 +1370,14 @@ 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
Expand All @@ -1373,7 +1388,7 @@ nvm_resolve_alias() {
break
fi

SEEN_ALIASES="${SEEN_ALIASES}\\n${ALIAS_TEMP}"
SEEN_ALIASES="${SEEN_ALIASES}${ALIAS_TEMP} "
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Critical: cycle detection is broken by this format change.

SEEN_ALIASES is now a single space-separated string (e.g. " one two three "), but the cycle check at line 1386 above (not shown in this hunk) is unchanged:

if command printf '%b' "${SEEN_ALIASES}" | nvm_grep -q -e "^${ALIAS_TEMP}$"; then

printf '%b' " one two three " emits a single line; grep -q "^${ALIAS_TEMP}$" will never match it. Result: any cycle becomes an infinite loop.

Reproduces against the existing test/fast/Aliases/circular/ fixtures — hangs.

Suggested fix (matches what the PR description claims was done):

case "${SEEN_ALIASES}" in
  *" ${ALIAS_TEMP} "*) ALIAS=""; break ;;
esac

The surrounding spaces in SEEN_ALIASES are already in place to make this work — this last step just needs to actually be made.

ALIAS="${ALIAS_TEMP}"
done

Expand Down
17 changes: 17 additions & 0 deletions test/fast/Aliases/nvm_alias handles alias with spaces in name
Original file line number Diff line number Diff line change
@@ -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 whose name contains spaces
mkdir -p ../../../alias
printf 'v22.1.0\n' > "../../../alias/test edge spaces"
ACTUAL="$(nvm_alias "test edge spaces")"
EXPECTED='v22.1.0'
[ "${ACTUAL}" = "${EXPECTED}" ] || die "expected >${EXPECTED}<, got >${ACTUAL}<"

rm -f "../../../alias/test edge spaces"
20 changes: 20 additions & 0 deletions test/fast/Aliases/nvm_alias handles binary data after version
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
#!/bin/sh

die () { echo "$@" ; exit 1; }

export NVM_DIR="$(cd ../../.. && pwd)"

: nvm.sh
\. "${NVM_DIR}/nvm.sh"

# Create an alias file where the first line has a valid version
# followed by a second line of binary-like data
printf 'v22.1.0\n' > ../../../alias/test-edge-binary
printf '\001\002\003\377\n' >> ../../../alias/test-edge-binary
ACTUAL="$(nvm_alias test-edge-binary)"
# nvm_alias emits every non-blank, non-comment line — first line should be the version
FIRST_LINE="$(nvm_echo "${ACTUAL}" | command head -n 1)"
EXPECTED='v22.1.0'
[ "${FIRST_LINE}" = "${EXPECTED}" ] || die "expected first line >${EXPECTED}<, got >${FIRST_LINE}<"

rm -f ../../../alias/test-edge-binary
22 changes: 22 additions & 0 deletions test/fast/Aliases/nvm_alias handles carriage return
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
#!/bin/sh

die () { echo "$@" ; exit 1; }

export NVM_DIR="$(cd ../../.. && pwd)"

: nvm.sh
\. "${NVM_DIR}/nvm.sh"

# Create an alias file with Windows-style line endings (CRLF)
printf 'v22.1.0\r\n' > ../../../alias/test-edge-cr
ACTUAL="$(nvm_alias test-edge-cr)"
EXPECTED='v22.1.0'
[ "${ACTUAL}" = "${EXPECTED}" ] || die "expected >${EXPECTED}<, got >${ACTUAL}<"

# Create an alias file with bare carriage return (no newline)
printf 'v22.2.0\r' > ../../../alias/test-edge-cr-bare
ACTUAL="$(nvm_alias test-edge-cr-bare)"
EXPECTED='v22.2.0'
[ "${ACTUAL}" = "${EXPECTED}" ] || die "expected >${EXPECTED}< for bare CR, got >${ACTUAL}<"

rm -f ../../../alias/test-edge-cr ../../../alias/test-edge-cr-bare
17 changes: 17 additions & 0 deletions test/fast/Aliases/nvm_alias handles comment-only alias file
Original file line number Diff line number Diff line change
@@ -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
22 changes: 22 additions & 0 deletions test/fast/Aliases/nvm_alias handles embedded NUL
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
#!/bin/sh

die () { echo "$@" ; exit 1; }

export NVM_DIR="$(cd ../../.. && pwd)"

: nvm.sh
\. "${NVM_DIR}/nvm.sh"

# Create an alias file with an embedded NUL byte after the version.
# NUL handling varies by shell: some stop at NUL, others read through it.
# The function must not crash, and must emit output starting with the version.
printf 'v22.1.0\000garbage\n' > ../../../alias/test-edge-nul
ACTUAL="$(nvm_alias test-edge-nul)"
EXIT_CODE=$?
[ "${EXIT_CODE}" = '0' ] || die "expected exit code 0, got ${EXIT_CODE}"
case "${ACTUAL}" in
v22.1.0*) ;; # OK — starts with the version regardless of NUL handling
*) die "expected output starting with >v22.1.0<, got >${ACTUAL}<" ;;
esac

rm -f ../../../alias/test-edge-nul
17 changes: 17 additions & 0 deletions test/fast/Aliases/nvm_alias handles empty alias file
Original file line number Diff line number Diff line change
@@ -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
22 changes: 22 additions & 0 deletions test/fast/Aliases/nvm_alias handles form feed and vertical tab
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
#!/bin/sh

die () { echo "$@" ; exit 1; }

export NVM_DIR="$(cd ../../.. && pwd)"

: nvm.sh
\. "${NVM_DIR}/nvm.sh"

# Create an alias file with trailing form feed
printf 'v22.1.0\f\n' > ../../../alias/test-edge-ff
ACTUAL="$(nvm_alias test-edge-ff)"
EXPECTED='v22.1.0'
[ "${ACTUAL}" = "${EXPECTED}" ] || die "expected >${EXPECTED}< for form feed, got >${ACTUAL}<"

# Create an alias file with trailing vertical tab
printf 'v22.2.0\v\n' > ../../../alias/test-edge-vt
ACTUAL="$(nvm_alias test-edge-vt)"
EXPECTED='v22.2.0'
[ "${ACTUAL}" = "${EXPECTED}" ] || die "expected >${EXPECTED}< for vertical tab, got >${ACTUAL}<"

rm -f ../../../alias/test-edge-ff ../../../alias/test-edge-vt
16 changes: 16 additions & 0 deletions test/fast/Aliases/nvm_alias handles no trailing newline
Original file line number Diff line number Diff line change
@@ -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 no trailing newline
printf 'v22.1.0' > ../../../alias/test-edge-no-newline
ACTUAL="$(nvm_alias test-edge-no-newline)"
EXPECTED='v22.1.0'
[ "${ACTUAL}" = "${EXPECTED}" ] || die "expected >${EXPECTED}<, got >${ACTUAL}<"

rm -f ../../../alias/test-edge-no-newline
17 changes: 17 additions & 0 deletions test/fast/Aliases/nvm_alias handles very long line
Original file line number Diff line number Diff line change
@@ -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 with a very long line (version followed by a long comment)
LONG_COMMENT="$(printf '%0*d' 10000 0 | command tr '0' 'x')"
printf 'v22.1.0 #%s\n' "${LONG_COMMENT}" > ../../../alias/test-edge-long
ACTUAL="$(nvm_alias test-edge-long)"
EXPECTED='v22.1.0'
[ "${ACTUAL}" = "${EXPECTED}" ] || die "expected >${EXPECTED}<, got >${ACTUAL}<"

rm -f ../../../alias/test-edge-long
16 changes: 16 additions & 0 deletions test/fast/Aliases/nvm_alias strips trailing whitespace
Original file line number Diff line number Diff line change
@@ -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
20 changes: 20 additions & 0 deletions test/fast/Aliases/nvm_resolve_alias handles deep chain
Original file line number Diff line number Diff line change
@@ -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)"
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Worth adding a sibling test for cycles (1-hop self-reference and a multi-hop loop). The existing test/fast/Aliases/circular/ fixtures already cover this - making sure they keep passing would have flagged the cycle-detection regression on the nvm.sh side.

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
19 changes: 19 additions & 0 deletions test/fast/Aliases/nvm_resolve_alias with nonexistent target
Original file line number Diff line number Diff line change
@@ -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
Loading