Skip to content

Commit

Permalink
[New] allow .nvmrc files to support comments
Browse files Browse the repository at this point in the history
In theory, `npx nvmrc` can now be used to validate an `.nvmrc` file that `nvm` will support. Allowances have been made for future extensibility.
  • Loading branch information
ljharb committed Jun 7, 2024
1 parent e4272a8 commit 5a080de
Show file tree
Hide file tree
Showing 5 changed files with 228 additions and 2 deletions.
3 changes: 3 additions & 0 deletions .gitmodules
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
[submodule "test/fixtures/nvmrc"]
path = test/fixtures/nvmrc
url = [email protected]:nvm-sh/nvmrc.git
90 changes: 88 additions & 2 deletions nvm.sh
Original file line number Diff line number Diff line change
Expand Up @@ -467,7 +467,87 @@ nvm_find_nvmrc() {
fi
}

# Obtain nvm version from rc file
nvm_nvmrc_invalid_msg() {
local error_text
error_text="invalid .nvmrc!
all non-commented content (anything after # is a comment) must be either:
- a single bare nvm-recognized version-ish
- or, multiple distinct key-value pairs, each key/value separated by a single equals sign (=)
additionally, a single bare nvm-recognized version-ish must be present (after stripping comments)."

local warn_text
warn_text="non-commented content parsed:
${1}"

nvm_err "$(nvm_wrap_with_color_code r "${error_text}")
$(nvm_wrap_with_color_code y "${warn_text}")"
}

nvm_process_nvmrc() {
local NVMRC_PATH="$1"
local lines
local unpaired_line

lines=$(command sed 's/#.*//' "$NVMRC_PATH" | command sed 's/^[[:space:]]*//;s/[[:space:]]*$//' | nvm_grep -v '^$')

if [ -z "$lines" ]; then
nvm_nvmrc_invalid_msg "${lines}"
return 1
fi

# Initialize key-value storage
local keys=''
local values=''

while IFS= read -r line; do
if [ -z "${line}" ]; then
continue
elif [ -z "${line%%=*}" ]; then
if [ -n "${unpaired_line}" ]; then
nvm_nvmrc_invalid_msg "${lines}"
return 1
fi
unpaired_line="${line}"
elif [[ "${line}" == *'='* ]]; then
key="${line%%=*}"
value="${line#*=}"

# Trim whitespace around key and value
key=$(nvm_echo "${key}" | command sed 's/^[[:space:]]*//;s/[[:space:]]*$//')
value=$(nvm_echo "${value}" | command sed 's/^[[:space:]]*//;s/[[:space:]]*$//')

# Check for invalid key "node"
if [ "${key}" = 'node' ]; then
nvm_nvmrc_invalid_msg "${lines}"
return 1
fi

# Check for duplicate keys
if nvm_echo "${keys}" | nvm_grep -q -E "(^| )${key}( |$)"; then
nvm_nvmrc_invalid_msg "${lines}"
return 1
fi
keys="${keys} ${key}"
values="${values} ${value}"
else
if [ -n "${unpaired_line}" ]; then
nvm_nvmrc_invalid_msg "${lines}"
return 1
fi
unpaired_line="${line}"
fi
done <<< "${lines}"

if [ -z "${unpaired_line}" ]; then
nvm_nvmrc_invalid_msg "${lines}"
return 1
fi

nvm_echo "${unpaired_line}"
}

nvm_rc_version() {
export NVM_RC_VERSION=''
local NVMRC_PATH
Expand All @@ -478,7 +558,12 @@ nvm_rc_version() {
fi
return 1
fi
NVM_RC_VERSION="$(command head -n 1 "${NVMRC_PATH}" | command tr -d '\r')" || command printf ''

NVM_RC_VERSION="$(nvm_process_nvmrc "${NVMRC_PATH}")"
if [ $? -ne 0 ]; then
return 1
fi

if [ -z "${NVM_RC_VERSION}" ]; then
if [ "${NVM_SILENT:-0}" -ne 1 ]; then
nvm_err "Warning: empty .nvmrc file found at \"${NVMRC_PATH}\""
Expand Down Expand Up @@ -4271,6 +4356,7 @@ nvm() {
nvm_get_colors nvm_set_colors nvm_print_color_code nvm_wrap_with_color_code nvm_format_help_message_colors \
nvm_echo_with_colors nvm_err_with_colors \
nvm_get_artifact_compression nvm_install_binary_extract nvm_extract_tarball \
nvm_process_nvmrc nvm_nvmrc_invalid_msg \
>/dev/null 2>&1
unset NVM_RC_VERSION NVM_NODEJS_ORG_MIRROR NVM_IOJS_ORG_MIRROR NVM_DIR \
NVM_CD_FLAGS NVM_BIN NVM_INC NVM_MAKE_JOBS \
Expand Down
102 changes: 102 additions & 0 deletions test/common.sh
Original file line number Diff line number Diff line change
Expand Up @@ -101,3 +101,105 @@ watch() {
kill %2;
return $EXIT_CODE
}

parse_json() {
local json
json="$1"
local key
key=""
local value
value=""
local output
output=""
local in_key
in_key=0
local in_value
in_value=0
local in_string
in_string=0
local escaped
escaped=0
local buffer
buffer=""
local char
local len
len=${#json}
local arr_index
arr_index=0
local in_array
in_array=0

for ((i = 0; i < len; i++)); do
char="${json:i:1}"

if [ "$in_string" -eq 1 ]; then
if [ "$escaped" -eq 1 ]; then
buffer="$buffer$char"
escaped=0
elif [ "$char" = "\\" ]; then
escaped=1
elif [ "$char" = "\"" ]; then
in_string=0
if [ "$in_key" -eq 1 ]; then
key="$buffer"
buffer=""
in_key=0
elif [ "$in_value" -eq 1 ]; then
value="$buffer"
buffer=""
output="$output$key=\"$value\"\n"
in_value=0
elif [ "$in_array" -eq 1 ]; then
value="$buffer"
buffer=""
output="$output$arr_index=\"$value\"\n"
arr_index=$((arr_index + 1))
fi
else
buffer="$buffer$char"
fi
continue
fi

case "$char" in
"\"")
in_string=1
buffer=""
if [ "$in_value" -eq 0 ] && [ "$in_array" -eq 0 ]; then
in_key=1
fi
;;
":")
in_value=1
;;
",")
if [ "$in_value" -eq 1 ]; then
in_value=0
fi
;;
"[")
in_array=1
;;
"]")
in_array=0
;;
"{" | "}")
;;
*)
if [ "$in_value" -eq 1 ] && [ "$char" != " " ] && [ "$char" != "\n" ] && [ "$char" != "\t" ]; then
buffer="$buffer$char"
fi
;;
esac
done

printf "%b" "$output"
}

extract_value() {
local key
key="$1"
local parsed
parsed="$2"
echo "$parsed" | grep "^$key=" | cut -d'=' -f2 | tr -d '"'
}
34 changes: 34 additions & 0 deletions test/fast/Unit tests/nvm_process_nvmrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
#!/bin/sh

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

cleanup() {
echo 'cleaned up'
}

\. ../../../nvm.sh

\. ../../common.sh

for f in ../../../test/fixtures/nvmrc/test/fixtures/valid/*; do
STDOUT="$(nvm_process_nvmrc $f/.nvmrc 2>/dev/null)"
EXIT_CODE="$(nvm_process_nvmrc $f/.nvmrc >/dev/null 2>/dev/null; echo $?)"

EXPECTED="$(extract_value node "$(parse_json "$(cat "$f/expected.json")")")"

[ "${EXIT_CODE}" = "0" ] || die "$(basename "${f}"): expected exit code of 0 but got ${EXIT_CODE}"

[ "${STDOUT}" = "${EXPECTED}" ] || die "$(basename "${f}"): expected STDOUT of \`${EXPECTED}\` but got \`${STDOUT}\`"
done

for f in ../../../test/fixtures/nvmrc/test/fixtures/invalid/*; do
STDOUT="$(nvm_process_nvmrc $f/.nvmrc 2>/dev/null)"
STDERR="$(nvm_process_nvmrc $f/.nvmrc 2>&1 >/dev/null | awk '{if(NR > 8) print $0}' | strip_colors)"
EXIT_CODE="$(nvm_process_nvmrc $f/.nvmrc >/dev/null 2>/dev/null; echo $?)"

EXPECTED="$(parse_json "$(cat "$f/expected.json")" | sed 's/^[0-9]*="//;s/"$//')"

[ "${EXIT_CODE}" != "0" ] || die "$(basename "${f}"): expected exit code of 'not 0' but got ${EXIT_CODE}"

[ "${STDERR}" = "${EXPECTED}" ] || die "$(basename "${f}"): expected STDERR of \`${EXPECTED}\` but got \`${STDERR}\`"
done
1 change: 1 addition & 0 deletions test/fixtures/nvmrc
Submodule nvmrc added at 0d325a

0 comments on commit 5a080de

Please sign in to comment.