[Fix] nvm_process_nvmrc: don't show the content of a symlinked .nvmrc

`.nvmrc` is found and read through symlinks,
so a repository that commits `.nvmrc` as a symlink to a path outside itself
(eg, `../../.npmrc`)
made a plain `nvm use`, `nvm install`, or `nvm which`
(or `nvm-exec`, or the README's auto-switching shell hook)
print that file:
the invalid `.nvmrc` message shows everything that was parsed,
even with `--silent`,
and a one-line file was taken as the version,
which is printed in the "Found" message and in later errors.

A symlinked `.nvmrc` is still followed,
so linking to another version file
(eg, `.node-version`, or a parent directory's `.nvmrc`)
keeps working,
but its content is never shown,
and it is valid only if it names a version or an existing alias,
via a new `nvm_is_version_or_alias` helper.
This commit is contained in:
Jordan Harband committed 2026-11-02 11:34:39 -06:00
1 parent 3f8411e58d
commit 67268ebcb6
2 files changed
+105 -2

No files matched your search

+34 -2
View File
@@ -942,11 +942,43 @@ EOF
nvm_echo "${unpaired_line}" nvm_echo "${unpaired_line}"
} }
nvm_is_version_or_alias() {
case "${1-}" in
system | 'lts/*') return 0 ;;
lts/-*)
case "${1#lts/-}" in
'' | *[!0-9]*) return 1 ;;
esac
return 0
;;
lts/*)
case "${1#lts/}" in
'' | *[!a-z]*) return 1 ;;
esac
return 0
;;
esac
nvm_is_valid_version "${1-}" || nvm_alias "${1-}" > /dev/null 2>&1
}
nvm_process_nvmrc() { nvm_process_nvmrc() {
local NVMRC_PATH local NVMRC_PATH
NVMRC_PATH="$1" NVMRC_PATH="$1"
nvm_process_nvmrc_content "$(command cat "${NVMRC_PATH}")" if [ ! -L "${NVMRC_PATH}" ]; then
nvm_process_nvmrc_content "$(command cat "${NVMRC_PATH}")"
return
fi
# a symlink can point at any file, so never print its content,
# and only accept a version or an existing alias, which would be printed later
local NVMRC_VERSION
if ! NVMRC_VERSION="$(nvm_process_nvmrc_content "$(command cat "${NVMRC_PATH}")" 2>/dev/null)" \
|| ! nvm_is_version_or_alias "${NVMRC_VERSION}"; then
nvm_err "invalid .nvmrc! '${NVMRC_PATH}' is a symbolic link, so it must contain a version or an alias name; its content is not shown."
return 1
fi
nvm_echo "${NVMRC_VERSION}"
} }
nvm_rc_version() { nvm_rc_version() {
@@ -6533,7 +6565,7 @@ nvm() {
nvm_print_color_legend \ nvm_print_color_legend \
nvm_echo_with_colors nvm_err_with_colors \ nvm_echo_with_colors nvm_err_with_colors \
nvm_get_artifact_compression nvm_install_binary_extract nvm_extract_tarball \ nvm_get_artifact_compression nvm_install_binary_extract nvm_extract_tarball \
nvm_process_nvmrc nvm_process_nvmrc_content nvm_nvmrc_invalid_msg \ nvm_process_nvmrc nvm_process_nvmrc_content nvm_nvmrc_invalid_msg nvm_is_version_or_alias \
nvm_write_nvmrc nvm_hash_reset \ nvm_write_nvmrc nvm_hash_reset \
>/dev/null 2>&1 >/dev/null 2>&1
unset NVM_NODEJS_ORG_MIRROR NVM_IOJS_ORG_MIRROR NVM_DIR \ unset NVM_NODEJS_ORG_MIRROR NVM_IOJS_ORG_MIRROR NVM_DIR \
@@ -0,0 +1,71 @@
#!/bin/sh
die () { echo "$@" ; cleanup ; exit 1; }
cleanup() {
rm -rf "${TEST_DIR-}"
nvm unalias nvmrc_symlink_alias > /dev/null 2>&1
unset TEST_DIR
}
: nvm.sh
\. ../../../nvm.sh
\. ../../common.sh
TEST_DIR="${PWD}/nvm_process_nvmrc_symlink_tmp"
mkdir -p "${TEST_DIR}/home" "${TEST_DIR}/project/packages/a" || die 'failed to create test dirs'
printf '//registry.npmjs.org/:_authToken=SECRET\n' > "${TEST_DIR}/home/.npmrc"
printf 'https://user:SECRET@github.com\n' > "${TEST_DIR}/home/.git-credentials"
NVMRC="${TEST_DIR}/project/.nvmrc"
for TARGET in .npmrc .git-credentials; do
rm -f "${NVMRC}"
ln -s "../home/${TARGET}" "${NVMRC}" || die 'failed to create the symlink'
try_err nvm_process_nvmrc "${NVMRC}"
[ "${CAPTURED_EXIT_CODE}" != 0 ] || die "expected a symlink to ${TARGET} to be invalid; got exit code ${CAPTURED_EXIT_CODE}"
case "${CAPTURED_STDERR}" in
*SECRET*) die "expected a symlink to ${TARGET} to not show its content; got \`${CAPTURED_STDERR}\`" ;;
esac
[ "${CAPTURED_STDERR}" = "invalid .nvmrc! '${NVMRC}' is a symbolic link, so it must contain a version or an alias name; its content is not shown." ] || die "expected the symlink error for ${TARGET}; got \`${CAPTURED_STDERR}\`"
try nvm_process_nvmrc "${NVMRC}"
[ -z "${CAPTURED_STDOUT}" ] || die "expected no version from a symlink to ${TARGET}; got \`${CAPTURED_STDOUT}\`"
done
nvm alias nvmrc_symlink_alias v0.0.1 > /dev/null 2>&1 || die 'failed to create an alias'
# a deliberate symlink to another version file, in or above the project, still works
for VERSION in v20.1.0 20 'v20.0.0-rc.1' node system 'lts/*' lts/iron lts/-1 nvmrc_symlink_alias; do
rm -f "${NVMRC}" "${TEST_DIR}/project/packages/a/.nvmrc"
printf '%s # comment\n' "${VERSION}" > "${NVMRC}"
ln -s ../../.nvmrc "${TEST_DIR}/project/packages/a/.nvmrc" || die 'failed to create the symlink'
try nvm_process_nvmrc "${TEST_DIR}/project/packages/a/.nvmrc"
[ "${CAPTURED_EXIT_CODE}" = 0 ] || die "expected a symlink to an .nvmrc with \`${VERSION}\` to be valid; got exit code ${CAPTURED_EXIT_CODE}"
[ "${CAPTURED_STDOUT}" = "${VERSION}" ] || die "expected \`${VERSION}\` from a symlinked .nvmrc; got \`${CAPTURED_STDOUT}\`"
done
for VERSION in not_an_alias lts/Iron 'lts/' 'lts/-x'; do
rm -f "${TEST_DIR}/project/.node-version"
printf '%s\n' "${VERSION}" > "${TEST_DIR}/project/.node-version"
rm -f "${NVMRC}"
ln -s .node-version "${NVMRC}" || die 'failed to create the symlink'
try_err nvm_process_nvmrc "${NVMRC}"
[ "${CAPTURED_EXIT_CODE}" != 0 ] || die "expected a symlink to a file with \`${VERSION}\` to be invalid; got exit code ${CAPTURED_EXIT_CODE}"
case "${CAPTURED_STDERR}" in
*"${VERSION}"*) die "expected a symlink to a file with \`${VERSION}\` to not show its content; got \`${CAPTURED_STDERR}\`" ;;
esac
done
# a regular file still shows what was parsed
rm -f "${NVMRC}"
printf 'foo=bar\n' > "${NVMRC}"
try_err nvm_process_nvmrc "${NVMRC}"
[ "${CAPTURED_EXIT_CODE}" != 0 ] || die "expected an .nvmrc without a version to be invalid; got exit code ${CAPTURED_EXIT_CODE}"
nvm_echo "${CAPTURED_STDERR}" | strip_colors | nvm_grep -q '^foo=bar$' || die "expected a regular .nvmrc's parsed content to be shown; got \`${CAPTURED_STDERR}\`"
cleanup