Skip to content

refactor: read alias files with shell builtins instead of sed/awk/head/tail pipeline#3787

Open
jakelodwick wants to merge 2 commits intonvm-sh:masterfrom
jakelodwick:perf/alias-resolve-builtins
Open

refactor: read alias files with shell builtins instead of sed/awk/head/tail pipeline#3787
jakelodwick wants to merge 2 commits intonvm-sh:masterfrom
jakelodwick:perf/alias-resolve-builtins

Conversation

@jakelodwick
Copy link

@jakelodwick jakelodwick commented Feb 17, 2026

Responding to @ljharb's note in #1261"If you can think of ways to speed up alias resolution, I'm all for it" — this simplifies how nvm_alias() and nvm_resolve_alias() read alias files.

nvm_alias() currently pipes through sed | awk to strip comments and blank lines. Alias files almost always contain a single line with one version string, so the pipeline is more machinery than the job needs. A while IFS= read -r loop with inline filtering does the same work more directly.

nvm_resolve_alias() wraps that in head -n N | tail -n 1 to extract a single line. Since we only ever need the first non-empty line, parameter expansion (${var%%newline*}) does the same thing without the pipeline. The cycle-detection check (printf | nvm_grep -q) is similarly replaced with a case pattern match — same logic, reads more clearly.

All replacements are POSIX (read -r, case, IFS=, parameter expansion). local declarations are separated from assignments for ksh compatibility. Tested in bash, zsh, dash, ksh, and sh.

As a side effect, this removes several subprocess forks per alias lookup — 5 for a single-hop alias, up to 12 for a two-hop with cycle check.

Edge-case tests added: empty alias file, comment-only file, trailing whitespace, 4-deep chain, nonexistent target.

[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.
@ljharb
Copy link
Member

ljharb commented Feb 17, 2026

Alias files almost always contain a single line with one version string

however, they could contain anything, and we must be robust against that. (i haven't reviewed yet, to be clear)

Copy link
Member

@ljharb ljharb left a comment

Choose a reason for hiding this comment

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

It would be good to add more test cases for edge cases, like binary data, very long lines, embedded NUL characters, no trailing newline, other whitespace characters (carriage return \r, form feed, etc.) that [[:space:]] would catch etc, an alias that has spaces (nvm alias 'foo bar' node, eg)?

nvm.sh Outdated
while IFS= read -r NVM_ALIAS_LINE || [ -n "${NVM_ALIAS_LINE}" ]; do
NVM_ALIAS_LINE="${NVM_ALIAS_LINE%%#*}"
case "${NVM_ALIAS_LINE}" in
*[!\ \ ]*) ;;
Copy link
Member

Choose a reason for hiding this comment

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

is there another way to represent the literal tab here, that's less likely to be accidentally converted to spaces by some random tool?

Copy link
Author

Choose a reason for hiding this comment

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

Yes — [[:space:]] POSIX class. Can't be mangled by editors or formatters, and it's already used in nvm.sh (lines 540, 569, 570). The trailing-strip uses %[[:space:]] so the removal is explicit too.

[Tests] `nvm_alias`: add edge-case tests for hostile file content
@jakelodwick
Copy link
Author

Added seven test files: binary data after version, CRLF + bare CR, embedded NUL, form feed + vertical tab, no trailing newline, 10k-char long line, alias name with spaces. All passing in sh/bash/zsh/dash.

@jakelodwick
Copy link
Author

Agreed. The new edge-case tests exercise exactly that.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants