From d55a55c387a5c6b3240fad26e20c95a150d6f601 Mon Sep 17 00:00:00 2001 From: Cassidy Marble Date: Wed, 7 Oct 2026 15:30:01 -0300 Subject: [PATCH] [Perf] `nvm_version_greater`, `nvm_version_greater_than_or_equal_to`: avoid `awk` where possible --- nvm.sh | 76 +++++++++++++++++++----- test/fast/Unit tests/nvm_version_greater | 39 ++++++++++++ 2 files changed, 101 insertions(+), 14 deletions(-) diff --git a/nvm.sh b/nvm.sh index a1df0506..16b5cbe9 100755 --- a/nvm.sh +++ b/nvm.sh @@ -796,33 +796,81 @@ nvm_curl_version() { command curl -V | command awk '{ if ($1 == "curl") print $2 }' | command sed 's/-.*$//g' } +# Try to do some shell magic to do semver greater-than maths. +# Fall through to the expensive test by calling `awk` if we can't be sure. nvm_version_greater() { - command awk 'BEGIN { + local NVM_LEFT + NVM_LEFT="${1#v}" + local NVM_RIGHT + NVM_RIGHT="${2#v}" + local NVM_PLAIN + # a third, more complex, argument of `1` (a flag) compares equal versions as true; `0` or + # no argument compares them as false. + local NVM_OR_EQUAL + NVM_OR_EQUAL="${3:-0}" + NVM_PLAIN='true' + + # `x.y.z` operands are compared in shell arithmetic, which avoids a fork, + # except when a group is long enough to overflow it; anything else - partial + # versions, prereleases, and malformed operands - keeps the comparison this + # function has always made + case "${NVM_LEFT}" in + '' | *[!0-9.]* | *..* | .* | *. | *.*.*.*) NVM_PLAIN='false' ;; + *.*.*) ;; + *) NVM_PLAIN='false' ;; + esac + case "${NVM_RIGHT}" in + '' | *[!0-9.]* | *..* | .* | *. | *.*.*.*) NVM_PLAIN='false' ;; + *.*.*) ;; + *) NVM_PLAIN='false' ;; + esac + + if [ "${NVM_PLAIN}" = 'true' ] && [ "${#NVM_LEFT}" -lt 19 ] && [ "${#NVM_RIGHT}" -lt 19 ]; then + local NVM_GROUP + local NVM_LEFT_GROUP + local NVM_RIGHT_GROUP + NVM_GROUP=1 + while [ "${NVM_GROUP}" -le 3 ]; do + NVM_LEFT_GROUP="${NVM_LEFT%%.*}" + NVM_LEFT="${NVM_LEFT#*.}" + NVM_RIGHT_GROUP="${NVM_RIGHT%%.*}" + NVM_RIGHT="${NVM_RIGHT#*.}" + if [ "${NVM_LEFT_GROUP}" -lt "${NVM_RIGHT_GROUP}" ]; then + return 1 + elif [ "${NVM_LEFT_GROUP}" -gt "${NVM_RIGHT_GROUP}" ]; then + return 0 + fi + NVM_GROUP=$((NVM_GROUP + 1)) + done + if [ "${NVM_OR_EQUAL}" = 1 ]; then + return 0 + fi + return 1 + fi + command awk -v or_equal="${NVM_OR_EQUAL}" 'BEGIN { if (ARGV[1] == "" || ARGV[2] == "") exit(1) split(ARGV[1], a, /\./); split(ARGV[2], b, /\./); for (i=1; i<=3; i++) { if (a[i] && a[i] !~ /^[0-9]+$/) exit(2); - if (b[i] && b[i] !~ /^[0-9]+$/) { exit(0); } + if (b[i] && b[i] !~ /^[0-9]+$/) { + # a non-numeric right-hand group (a prerelease): the left is greater, + # unless equality counts, in which case the comparison below decides + if (or_equal != 1) { exit(0); } + } if (a[i] < b[i]) exit(3); else if (a[i] > b[i]) exit(0); } + if (or_equal == 1) { exit(0); } exit(4) - }' "${1#v}" "${2#v}" + }' "${NVM_LEFT}" "${NVM_RIGHT}" } +# Try to do some shell magic to do semver greater-than or equal-to maths. +# Fall through to the expensive test by calling `awk` if we can't be sure. +# see `nvm_version_greater` nvm_version_greater_than_or_equal_to() { - command awk 'BEGIN { - if (ARGV[1] == "" || ARGV[2] == "") exit(1) - split(ARGV[1], a, /\./); - split(ARGV[2], b, /\./); - for (i=1; i<=3; i++) { - if (a[i] && a[i] !~ /^[0-9]+$/) exit(2); - if (a[i] < b[i]) exit(3); - else if (a[i] > b[i]) exit(0); - } - exit(0) - }' "${1#v}" "${2#v}" + nvm_version_greater "${1-}" "${2-}" 1 } nvm_version_dir() { diff --git a/test/fast/Unit tests/nvm_version_greater b/test/fast/Unit tests/nvm_version_greater index e88c28d6..3b2cfb36 100755 --- a/test/fast/Unit tests/nvm_version_greater +++ b/test/fast/Unit tests/nvm_version_greater @@ -6,6 +6,8 @@ die () { echo "$@" ; exit 1; } \. ../../../nvm.sh nvm_version_greater 0.10.0 0.2.12 || die '"nvm_version_greater 0.10.0 0.2.12" did not return true' +nvm_version_greater 0.20.12 0.10.0 || die '"nvm_version_greater 0.20.12 0.10.0" did not return true' +nvm_version_greater v18.20.0 v18.19.9 || die '"nvm_version_greater v18.20.0 v18.19.9" did not return true' if nvm_version_greater 0.10.0 0.20.12; then die '"nvm_version_greater 0.10.0 0.20.12" returned true' @@ -14,3 +16,40 @@ fi if nvm_version_greater 0.10.0 0.10.0; then die '"nvm_version_greater" returned false for the same two versions' fi + +if nvm_version_greater v18.20.0 v18.20.0; then + die '"nvm_version_greater v18.20.0 v18.20.0" returned true for the same two versions' +fi + +if nvm_version_greater v18.19.9 v18.20.0; then + die '"nvm_version_greater v18.19.9 v18.20.0" returned true' +fi + +# a partial version is not a plain `x.y.z`, so it keeps the awk comparison +nvm_version_greater 0.10 0.2.12 || die '"nvm_version_greater 0.10 0.2.12" did not return true' + +# the comparison must not write anything to stdout (zsh's `typeset` prints a +# local variable when it is redeclared) +OUTPUT="$(nvm_version_greater 0.10.0 0.2.12 2>&1)" +[ -z "${OUTPUT}" ] || die "nvm_version_greater 0.10.0 0.2.12 wrote output: '${OUTPUT}'" + +# `nvm_version_greater` must not fork a command for a plain `x.y.z` pair: +# mock `awk`, which the comparison used, and fail if it runs +TEST_DIR="$(pwd)/nvm_version_greater_tmp" +[ ! -e "${TEST_DIR}" ] && mkdir "${TEST_DIR}" +MOCK_BIN="${TEST_DIR}/bin" +mkdir -p "${MOCK_BIN}" +{ + echo '#!/bin/sh' + echo 'echo "awk was called" >&2' + echo 'exit 1' +} > "${MOCK_BIN}/awk" +chmod +x "${MOCK_BIN}/awk" + +PATH="${MOCK_BIN}:${PATH}" nvm_version_greater 0.10.0 0.2.12 \ + || die 'nvm_version_greater 0.10.0 0.2.12 forked a command' + +PATH="${MOCK_BIN}:${PATH}" nvm_version_greater v18.20.0 v18.19.9 \ + || die 'nvm_version_greater v18.20.0 v18.19.9 forked a command' + +rm -rf "${TEST_DIR}"