Skip to content

Commit 29dce5e

Browse files
ljharbYash-Singh1
andcommitted
[New] allow .nvmrc files to support comments
In theory, `npx nvmrc` can now be used to validate an `.nvmrc` file that `nvm` will support. Allowances have been made for future extensibility, and aliases may no longer contain a `#`. Fixes #3336. Closes #2288. Co-authored-by: Jordan Harband <[email protected]> Co-authored-by: Yash Singh <[email protected]>
1 parent 95081f0 commit 29dce5e

File tree

8 files changed

+278
-3
lines changed

8 files changed

+278
-3
lines changed

.editorconfig

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,3 +26,10 @@ insert_final_newline = off
2626

2727
[Makefile]
2828
indent_style = tab
29+
30+
[test/fixtures/nvmrc/**]
31+
indent_style = off
32+
insert_final_newline = off
33+
34+
[test/fixtures/actual/alias/empty]
35+
insert_final_newline = off

.gitmodules

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
[submodule "test/fixtures/nvmrc"]
2+
path = test/fixtures/nvmrc
3+
url = [email protected]:nvm-sh/nvmrc.git

README.md

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -298,6 +298,13 @@ To install a specific version of node:
298298
nvm install 14.7.0 # or 16.3.0, 12.22.1, etc
299299
```
300300

301+
To set an alias:
302+
303+
```sh
304+
nvm alias my_alias v14.4.0
305+
```
306+
Make sure that your alias does not contain any spaces or slashes.
307+
301308
The first version installed becomes the default. New shells will start with the default version of node (e.g., `nvm alias default`).
302309

303310
You can list available versions using `ls-remote`:
@@ -563,7 +570,11 @@ Now using node v5.9.1 (npm v3.7.3)
563570

564571
`nvm use` et. al. will traverse directory structure upwards from the current directory looking for the `.nvmrc` file. In other words, running `nvm use` et. al. in any subdirectory of a directory with an `.nvmrc` will result in that `.nvmrc` being utilized.
565572

566-
The contents of a `.nvmrc` file **must** be the `<version>` (as described by `nvm --help`) followed by a newline. No trailing spaces are allowed, and the trailing newline is required.
573+
The contents of a `.nvmrc` file **must** contain precisely one `<version>` (as described by `nvm --help`) followed by a newline. `.nvmrc` files may also have comments. The comment delimiter is `#`, and it and any text after it, as well as blank lines, and leading and trailing white space, will be ignored when parsing.
574+
575+
Key/value pairs using `=` are also allowed and ignored, but are reserved for future use, and may cause validation errors in the future.
576+
577+
Run [`npx nvmrc`](https://npmjs.com/nvmrc) to validate an `.nvmrc` file. If that tool’s results do not agree with nvm, one or the other has a bug - please file an issue.
567578

568579
### Deeper Shell Integration
569580

nvm.sh

Lines changed: 93 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -467,7 +467,89 @@ nvm_find_nvmrc() {
467467
fi
468468
}
469469

470-
# Obtain nvm version from rc file
470+
nvm_nvmrc_invalid_msg() {
471+
local error_text
472+
error_text="invalid .nvmrc!
473+
all non-commented content (anything after # is a comment) must be either:
474+
- a single bare nvm-recognized version-ish
475+
- or, multiple distinct key-value pairs, each key/value separated by a single equals sign (=)
476+
477+
additionally, a single bare nvm-recognized version-ish must be present (after stripping comments)."
478+
479+
local warn_text
480+
warn_text="non-commented content parsed:
481+
${1}"
482+
483+
nvm_err "$(nvm_wrap_with_color_code r "${error_text}")
484+
485+
$(nvm_wrap_with_color_code y "${warn_text}")"
486+
}
487+
488+
nvm_process_nvmrc() {
489+
local NVMRC_PATH="$1"
490+
local lines
491+
local unpaired_line
492+
493+
lines=$(command sed 's/#.*//' "$NVMRC_PATH" | command sed 's/^[[:space:]]*//;s/[[:space:]]*$//' | nvm_grep -v '^$')
494+
495+
if [ -z "$lines" ]; then
496+
nvm_nvmrc_invalid_msg "${lines}"
497+
return 1
498+
fi
499+
500+
# Initialize key-value storage
501+
local keys=''
502+
local values=''
503+
504+
while IFS= read -r line; do
505+
if [ -z "${line}" ]; then
506+
continue
507+
elif [ -z "${line%%=*}" ]; then
508+
if [ -n "${unpaired_line}" ]; then
509+
nvm_nvmrc_invalid_msg "${lines}"
510+
return 1
511+
fi
512+
unpaired_line="${line}"
513+
elif case "$line" in *'='*) true;; *) false;; esac; then
514+
key="${line%%=*}"
515+
value="${line#*=}"
516+
517+
# Trim whitespace around key and value
518+
key=$(nvm_echo "${key}" | command sed 's/^[[:space:]]*//;s/[[:space:]]*$//')
519+
value=$(nvm_echo "${value}" | command sed 's/^[[:space:]]*//;s/[[:space:]]*$//')
520+
521+
# Check for invalid key "node"
522+
if [ "${key}" = 'node' ]; then
523+
nvm_nvmrc_invalid_msg "${lines}"
524+
return 1
525+
fi
526+
527+
# Check for duplicate keys
528+
if nvm_echo "${keys}" | nvm_grep -q -E "(^| )${key}( |$)"; then
529+
nvm_nvmrc_invalid_msg "${lines}"
530+
return 1
531+
fi
532+
keys="${keys} ${key}"
533+
values="${values} ${value}"
534+
else
535+
if [ -n "${unpaired_line}" ]; then
536+
nvm_nvmrc_invalid_msg "${lines}"
537+
return 1
538+
fi
539+
unpaired_line="${line}"
540+
fi
541+
done <<EOF
542+
$lines
543+
EOF
544+
545+
if [ -z "${unpaired_line}" ]; then
546+
nvm_nvmrc_invalid_msg "${lines}"
547+
return 1
548+
fi
549+
550+
nvm_echo "${unpaired_line}"
551+
}
552+
471553
nvm_rc_version() {
472554
export NVM_RC_VERSION=''
473555
local NVMRC_PATH
@@ -478,7 +560,12 @@ nvm_rc_version() {
478560
fi
479561
return 1
480562
fi
481-
NVM_RC_VERSION="$(command head -n 1 "${NVMRC_PATH}" | command tr -d '\r')" || command printf ''
563+
564+
565+
if ! NVM_RC_VERSION="$(nvm_process_nvmrc "${NVMRC_PATH}")"; then
566+
return 1
567+
fi
568+
482569
if [ -z "${NVM_RC_VERSION}" ]; then
483570
if [ "${NVM_SILENT:-0}" -ne 1 ]; then
484571
nvm_err "Warning: empty .nvmrc file found at \"${NVMRC_PATH}\""
@@ -4058,6 +4145,9 @@ nvm() {
40584145
# so, unalias it.
40594146
nvm unalias "${ALIAS}"
40604147
return $?
4148+
elif echo "${ALIAS}" | grep -q "#"; then
4149+
nvm_err 'Aliases with a comment delimiter (#) are not supported.'
4150+
return 1
40614151
elif [ "${TARGET}" != '--' ]; then
40624152
# a target was passed: create an alias
40634153
if [ "${ALIAS#*\/}" != "${ALIAS}" ]; then
@@ -4271,6 +4361,7 @@ nvm() {
42714361
nvm_get_colors nvm_set_colors nvm_print_color_code nvm_wrap_with_color_code nvm_format_help_message_colors \
42724362
nvm_echo_with_colors nvm_err_with_colors \
42734363
nvm_get_artifact_compression nvm_install_binary_extract nvm_extract_tarball \
4364+
nvm_process_nvmrc nvm_nvmrc_invalid_msg \
42744365
>/dev/null 2>&1
42754366
unset NVM_RC_VERSION NVM_NODEJS_ORG_MIRROR NVM_IOJS_ORG_MIRROR NVM_DIR \
42764367
NVM_CD_FLAGS NVM_BIN NVM_INC NVM_MAKE_JOBS \

test/common.sh

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,3 +101,105 @@ watch() {
101101
kill %2;
102102
return $EXIT_CODE
103103
}
104+
105+
parse_json() {
106+
local json
107+
json="$1"
108+
local key
109+
key=""
110+
local value
111+
value=""
112+
local output
113+
output=""
114+
local in_key
115+
in_key=0
116+
local in_value
117+
in_value=0
118+
local in_string
119+
in_string=0
120+
local escaped
121+
escaped=0
122+
local buffer
123+
buffer=""
124+
local char
125+
local len
126+
len=${#json}
127+
local arr_index
128+
arr_index=0
129+
local in_array
130+
in_array=0
131+
132+
for ((i = 0; i < len; i++)); do
133+
char="${json:i:1}"
134+
135+
if [ "$in_string" -eq 1 ]; then
136+
if [ "$escaped" -eq 1 ]; then
137+
buffer="$buffer$char"
138+
escaped=0
139+
elif [ "$char" = "\\" ]; then
140+
escaped=1
141+
elif [ "$char" = "\"" ]; then
142+
in_string=0
143+
if [ "$in_key" -eq 1 ]; then
144+
key="$buffer"
145+
buffer=""
146+
in_key=0
147+
elif [ "$in_value" -eq 1 ]; then
148+
value="$buffer"
149+
buffer=""
150+
output="$output$key=\"$value\"\n"
151+
in_value=0
152+
elif [ "$in_array" -eq 1 ]; then
153+
value="$buffer"
154+
buffer=""
155+
output="$output$arr_index=\"$value\"\n"
156+
arr_index=$((arr_index + 1))
157+
fi
158+
else
159+
buffer="$buffer$char"
160+
fi
161+
continue
162+
fi
163+
164+
case "$char" in
165+
"\"")
166+
in_string=1
167+
buffer=""
168+
if [ "$in_value" -eq 0 ] && [ "$in_array" -eq 0 ]; then
169+
in_key=1
170+
fi
171+
;;
172+
":")
173+
in_value=1
174+
;;
175+
",")
176+
if [ "$in_value" -eq 1 ]; then
177+
in_value=0
178+
fi
179+
;;
180+
"[")
181+
in_array=1
182+
;;
183+
"]")
184+
in_array=0
185+
;;
186+
"{" | "}")
187+
;;
188+
*)
189+
if [ "$in_value" -eq 1 ] && [ "$char" != " " ] && [ "$char" != "\n" ] && [ "$char" != "\t" ]; then
190+
buffer="$buffer$char"
191+
fi
192+
;;
193+
esac
194+
done
195+
196+
printf "%b" "$output"
197+
}
198+
199+
extract_value() {
200+
local key
201+
key="$1"
202+
local parsed
203+
parsed="$2"
204+
echo "$parsed" | grep "^$key=" | cut -d'=' -f2 | tr -d '"'
205+
}
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
#!/bin/sh
2+
3+
\. ../../../nvm.sh
4+
5+
die () { echo "$@" ; exit 1; }
6+
7+
OUTPUT="$(nvm alias foo#bar baz 2>&1)"
8+
EXPECTED_OUTPUT="Aliases with a comment delimiter (#) are not supported."
9+
[ "$OUTPUT" = "$EXPECTED_OUTPUT" ] || die "trying to create an alias with a hash should fail with '$EXPECTED_OUTPUT', got '$OUTPUT'"
10+
11+
EXIT_CODE="$(nvm alias foo#bar baz >/dev/null 2>&1 ; echo $?)"
12+
[ "$EXIT_CODE" = "1" ] || die "trying to create an alias with a hash should fail with code 1, got '$EXIT_CODE'"
13+
14+
OUTPUT="$(nvm alias foo# baz 2>&1)"
15+
EXPECTED_OUTPUT="Aliases with a comment delimiter (#) are not supported."
16+
[ "$OUTPUT" = "$EXPECTED_OUTPUT" ] || die "trying to create an alias ending with a hash should fail with '$EXPECTED_OUTPUT', got '$OUTPUT'"
17+
18+
EXIT_CODE="$(nvm alias foo# baz >/dev/null 2>&1 ; echo $?)"
19+
[ "$EXIT_CODE" = "1" ] || die "trying to create an alias ending with a hash should fail with code 1, got '$EXIT_CODE'"
20+
21+
OUTPUT="$(nvm alias \#bar baz 2>&1)"
22+
EXPECTED_OUTPUT="Aliases with a comment delimiter (#) are not supported."
23+
[ "$OUTPUT" = "$EXPECTED_OUTPUT" ] || die "trying to create an alias starting with a hash should fail with '$EXPECTED_OUTPUT', got '$OUTPUT'"
24+
25+
EXIT_CODE="$(nvm alias \#bar baz >/dev/null 2>&1 ; echo $?)"
26+
[ "$EXIT_CODE" = "1" ] || die "trying to create an alias starting with a hash should fail with code 1, got '$EXIT_CODE'"
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
#!/bin/sh
2+
3+
die () { echo "$@" ; cleanup ; exit 1; }
4+
5+
cleanup() {
6+
echo 'cleaned up'
7+
}
8+
9+
\. ../../../nvm.sh
10+
11+
\. ../../common.sh
12+
13+
for f in ../../../test/fixtures/nvmrc/test/fixtures/valid/*; do
14+
STDOUT="$(nvm_process_nvmrc $f/.nvmrc 2>/dev/null)"
15+
EXIT_CODE="$(nvm_process_nvmrc $f/.nvmrc >/dev/null 2>/dev/null; echo $?)"
16+
17+
EXPECTED="$(extract_value node "$(parse_json "$(cat "$f/expected.json")")")"
18+
19+
[ "${EXIT_CODE}" = "0" ] || die "$(basename "${f}"): expected exit code of 0 but got ${EXIT_CODE}"
20+
21+
[ "${STDOUT}" = "${EXPECTED}" ] || die "$(basename "${f}"): expected STDOUT of \`${EXPECTED}\` but got \`${STDOUT}\`"
22+
done
23+
24+
for f in ../../../test/fixtures/nvmrc/test/fixtures/invalid/*; do
25+
STDOUT="$(nvm_process_nvmrc $f/.nvmrc 2>/dev/null)"
26+
STDERR="$(nvm_process_nvmrc $f/.nvmrc 2>&1 >/dev/null | awk '{if(NR > 8) print $0}' | strip_colors)"
27+
EXIT_CODE="$(nvm_process_nvmrc $f/.nvmrc >/dev/null 2>/dev/null; echo $?)"
28+
29+
EXPECTED="$(parse_json "$(cat "$f/expected.json")" | sed 's/^[0-9]*="//;s/"$//')"
30+
31+
[ "${EXIT_CODE}" != "0" ] || die "$(basename "${f}"): expected exit code of 'not 0' but got ${EXIT_CODE}"
32+
33+
[ "${STDERR}" = "${EXPECTED}" ] || die "$(basename "${f}"): expected STDERR of \`${EXPECTED}\` but got \`${STDERR}\`"
34+
done

test/fixtures/nvmrc

Submodule nvmrc added at 0d325aa

0 commit comments

Comments
 (0)