diff --git a/nvm.sh b/nvm.sh index 16b5cbe9..ce84b0ea 100755 --- a/nvm.sh +++ b/nvm.sh @@ -1254,24 +1254,33 @@ nvm_change_path() { # if there’s no initial path, just return the supplementary path if [ -z "${1-}" ]; then nvm_echo "${3-}${2-}" + return + fi + # `${NVM_DIR}` is matched literally, so escape characters that are special in regexes + local NVM_DIR_RE + NVM_DIR_RE="$(nvm_echo "${NVM_DIR}" | command sed 's/[][\.*^$+?(){}|]/\\&/g')" # if the initial path doesn’t contain an nvm path, prepend the supplementary # path - elif ! nvm_echo "${1-}" | nvm_grep -q "${NVM_DIR}/[^/]*${2-}" \ - && ! nvm_echo "${1-}" | nvm_grep -q "${NVM_DIR}/versions/[^/]*/[^/]*${2-}"; then + if ! nvm_echo "${1-}" | nvm_grep -Eq "${NVM_DIR_RE}/[^/]*${2-}" \ + && ! nvm_echo "${1-}" | nvm_grep -Eq "${NVM_DIR_RE}/versions/[^/]*/[^/]*${2-}"; then nvm_echo "${3-}${2-}:${1-}" # if the initial path contains BOTH an nvm path (checked for above) and # that nvm path is preceded by a system binary path, just prepend the # supplementary path instead of replacing it. # https://github.com/nvm-sh/nvm/issues/1652#issuecomment-342571223 - elif nvm_echo "${1-}" | nvm_grep -Eq "(^|:)(/usr(/local)?)?${2-}:.*${NVM_DIR}/[^/]*${2-}" \ - || nvm_echo "${1-}" | nvm_grep -Eq "(^|:)(/usr(/local)?)?${2-}:.*${NVM_DIR}/versions/[^/]*/[^/]*${2-}"; then + elif nvm_echo "${1-}" | nvm_grep -Eq "(^|:)(/usr(/local)?)?${2-}:.*${NVM_DIR_RE}/[^/]*${2-}" \ + || nvm_echo "${1-}" | nvm_grep -Eq "(^|:)(/usr(/local)?)?${2-}:.*${NVM_DIR_RE}/versions/[^/]*/[^/]*${2-}"; then nvm_echo "${3-}${2-}:${1-}" # use sed to replace the existing nvm path with the supplementary path. This # preserves the order of the path. else - nvm_echo "${1-}" | command sed \ - -e "s#${NVM_DIR}/[^/]*${2-}[^:]*#${3-}${2-}#" \ - -e "s#${NVM_DIR}/versions/[^/]*/[^/]*${2-}[^:]*#${3-}${2-}#" + local NVM_DIR_SED + NVM_DIR_SED="$(nvm_echo "${NVM_DIR_RE}" | command sed 's/#/\\#/g')" + local NEW_DIR_SED + NEW_DIR_SED="$(nvm_echo "${3-}" | command sed 's/[\&#]/\\&/g')" + nvm_echo "${1-}" | command sed -E \ + -e "s#${NVM_DIR_SED}/[^/]*${2-}[^:]*#${NEW_DIR_SED}${2-}#" \ + -e "s#${NVM_DIR_SED}/versions/[^/]*/[^/]*${2-}[^:]*#${NEW_DIR_SED}${2-}#" fi } @@ -3638,14 +3647,29 @@ nvm_has_solaris_binary() { fi } +# replaces every literal occurrence of $2 in $1 with $3; values go through the +# environment so awk does not interpret backslash escapes or regex characters +nvm_replace_literal() { + nvm_echo "${1-}" | NVM_FROM="${2-}" NVM_TO="${3-}" command awk ' + BEGIN { from = ENVIRON["NVM_FROM"]; to = ENVIRON["NVM_TO"]; n = length(from) } + { + out = ""; s = $0 + while (n > 0 && (i = index(s, from)) > 0) { + out = out substr(s, 1, i - 1) to + s = substr(s, i + n) + } + print out s + }' +} + nvm_sanitize_path() { local SANITIZED_PATH SANITIZED_PATH="${1-}" if [ "_${SANITIZED_PATH}" != "_${NVM_DIR}" ]; then - SANITIZED_PATH="$(nvm_echo "${SANITIZED_PATH}" | command sed -e "s#${NVM_DIR}#\${NVM_DIR}#g")" + SANITIZED_PATH="$(nvm_replace_literal "${SANITIZED_PATH}" "${NVM_DIR}" '${NVM_DIR}')" fi if [ "_${SANITIZED_PATH}" != "_${HOME}" ]; then - SANITIZED_PATH="$(nvm_echo "${SANITIZED_PATH}" | command sed -e "s#${HOME}#\${HOME}#g")" + SANITIZED_PATH="$(nvm_replace_literal "${SANITIZED_PATH}" "${HOME}" '${HOME}')" fi nvm_echo "${SANITIZED_PATH}" } @@ -5426,7 +5450,7 @@ nvm() { nvm_install_lock_name nvm_acquire_install_lock nvm_release_install_lock \ nvm_list_aliases nvm_make_alias nvm_print_alias_file nvm_print_alias_path \ nvm_print_default_alias nvm_print_formatted_alias nvm_resolve_local_alias \ - nvm_sanitize_path nvm_has_colors nvm_has_italics nvm_process_parameters \ + nvm_replace_literal nvm_sanitize_path nvm_has_colors nvm_has_italics nvm_process_parameters \ nvm_node_version_has_solaris_binary nvm_iojs_version_has_solaris_binary \ nvm_curl_libz_support nvm_command_info nvm_is_zsh nvm_stdout_is_terminal \ nvm_npmrc_bad_news_bears nvm_sanitize_auth_header \ diff --git a/test/fast/Unit tests/nvm_change_path special characters b/test/fast/Unit tests/nvm_change_path special characters new file mode 100755 index 00000000..ec85be9e --- /dev/null +++ b/test/fast/Unit tests/nvm_change_path special characters @@ -0,0 +1,37 @@ +#!/bin/sh + +die () { echo "$@" ; exit 1; } + +: nvm.sh +\. ../../../nvm.sh + +for DIR in '/tmp/a&b/.nvm' '/tmp/a#b/.nvm' '/tmp/a[1]/.nvm' '/tmp/a*b/.nvm' '/tmp/a+(b)/.nvm' '/tmp/a.b/.nvm' '/tmp/a\b/.nvm' '/tmp/a$b/.nvm' '/tmp/a|b/.nvm'; do + NVM_DIR="${DIR}" + TEST_PATH="/opt/bin:${NVM_DIR}/versions/node/v4.5.0/bin:/usr/bin" + + # replace branch + NEW_PATH="$(nvm_change_path "${TEST_PATH}" "/bin" "${NVM_DIR}/versions/node/v7.1.0")" + EXPECTED="/opt/bin:${NVM_DIR}/versions/node/v7.1.0/bin:/usr/bin" + [ "${NEW_PATH}" = "${EXPECTED}" ] || die "NVM_DIR=${DIR}: expected >${EXPECTED}<, got >${NEW_PATH}<" + + # system path precedes the nvm path: prepend instead of replacing + SYSTEM_FIRST_PATH="/usr/bin:${NVM_DIR}/versions/node/v4.5.0/bin" + NEW_PATH="$(nvm_change_path "${SYSTEM_FIRST_PATH}" "/bin" "${NVM_DIR}/versions/node/v7.1.0")" + EXPECTED="${NVM_DIR}/versions/node/v7.1.0/bin:${SYSTEM_FIRST_PATH}" + [ "${NEW_PATH}" = "${EXPECTED}" ] || die "NVM_DIR=${DIR} (system path first): expected >${EXPECTED}<, got >${NEW_PATH}<" + + # MANPATH call + TEST_MANPATH="/opt/share/man:${NVM_DIR}/versions/node/v4.5.0/share/man:/usr/share/man" + NEW_PATH="$(nvm_change_path "${TEST_MANPATH}" "/share/man" "${NVM_DIR}/versions/node/v7.1.0")" + EXPECTED="/opt/share/man:${NVM_DIR}/versions/node/v7.1.0/share/man:/usr/share/man" + [ "${NEW_PATH}" = "${EXPECTED}" ] || die "NVM_DIR=${DIR} (MANPATH): expected >${EXPECTED}<, got >${NEW_PATH}<" +done + +# `.` in NVM_DIR must be escaped so it matches itself only, not "any character": +# an unrelated path entry that merely looks similar (some other char where the +# `.` is) must be left alone, not mistaken for an nvm path and rewritten. +NVM_DIR="/tmp/a.b/.nvm" +UNRELATED_PATH="/opt/bin:/tmp/aXb/.nvm/versions/node/v4.5.0/bin:/usr/bin" +NEW_PATH="$(nvm_change_path "${UNRELATED_PATH}" "/bin" "${NVM_DIR}/versions/node/v7.1.0")" +EXPECTED="${NVM_DIR}/versions/node/v7.1.0/bin:${UNRELATED_PATH}" +[ "${NEW_PATH}" = "${EXPECTED}" ] || die "unrelated PATH entry /tmp/aXb/.nvm/... should not be touched by NVM_DIR=${NVM_DIR}: expected >${EXPECTED}<, got >${NEW_PATH}<" diff --git a/test/fast/Unit tests/nvm_sanitize_path special characters b/test/fast/Unit tests/nvm_sanitize_path special characters new file mode 100755 index 00000000..186c1f13 --- /dev/null +++ b/test/fast/Unit tests/nvm_sanitize_path special characters @@ -0,0 +1,29 @@ +#!/bin/sh + +die () { echo "$@" ; exit 1; } + +: nvm.sh +\. ../../../nvm.sh + +HOME='/home/user' + +for DIR in '/tmp/a&b/.nvm' '/tmp/a#b/.nvm' '/tmp/a[1]/.nvm' '/tmp/a*b/.nvm' '/tmp/a+(b)/.nvm' '/tmp/a.b/.nvm' '/tmp/a\b/.nvm' '/tmp/a$b/.nvm' '/tmp/a|b/.nvm'; do + NVM_DIR="${DIR}" + + OUTPUT="$(nvm_sanitize_path "${NVM_DIR}/versions/node/v4.5.0")" + EXPECTED='${NVM_DIR}/versions/node/v4.5.0' + [ "${OUTPUT}" = "${EXPECTED}" ] || die "NVM_DIR=${DIR}: expected >${EXPECTED}<, got >${OUTPUT}<" + + OUTPUT="$(nvm_sanitize_path "${NVM_DIR}")" + [ "${OUTPUT}" = "${NVM_DIR}" ] || die "NVM_DIR=${DIR}: path equal to NVM_DIR should be left alone, got >${OUTPUT}<" +done + +# `.` in NVM_DIR must match itself only, not any character +NVM_DIR='/tmp/a.b/.nvm' +OUTPUT="$(nvm_sanitize_path '/tmp/aXb/.nvm/foo')" +[ "${OUTPUT}" = '/tmp/aXb/.nvm/foo' ] || die "unrelated path was rewritten: >${OUTPUT}<" + +# HOME is replaced too +NVM_DIR='/tmp/a#b/.nvm' +OUTPUT="$(nvm_sanitize_path "${HOME}/.npmrc")" +[ "${OUTPUT}" = '${HOME}/.npmrc' ] || die "expected >\${HOME}/.npmrc<, got >${OUTPUT}<"