Created
January 19, 2026 20:17
-
-
Save pjeby/3dd65992fae3d2973fc34a6fb7f82e4e to your computer and use it in GitHub Desktop.
Fix for fnm (Fast Node Manager) issues with repath/hash -r and symlink accumulation (bash only, sorry)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/usr/bin/env bash | |
| # | |
| # This script fixes some outstanding issues with FNM (fast node manager's) | |
| # "multishell" symlink system. Specifically: | |
| # | |
| # 1) that FNM generates a symlink for every shell it's ever run in, and *never | |
| # cleans them up*, eventually leading to inode exhaustion. | |
| # | |
| # 2) that many shells (especially bash) do not change their cached PATH lookups | |
| # when a symlink on PATH is changed, rather than PATH itself being changed. | |
| # | |
| # So, this script fixes these issues (at least for bash) by | |
| # | |
| # 1) wrapping the `fnm` command to detect when it has been run, and update the | |
| # PATH if needed | |
| # | |
| # 2) reading the FNM-generated symlink to get the node version and update the | |
| # PATH with it | |
| # | |
| # 3) deleting the symlink after it's read | |
| # | |
| # In this manner, it prevents symlink accumulation because the symlink never | |
| # exists for more than a fraction of a second after fnm creates it. And the | |
| # rehashing problem doesn't happen because the PATH is changed, which allows the | |
| # shell to notice its cache is out of date. | |
| # | |
| # To use the script, add a line like this to your .bashrc: | |
| # | |
| # source /wherever/you/put/this/script | |
| # | |
| # right after your `eval "$(fnm env)"` or equivalent, then restart your shell. | |
| # (You can also just source the script in your current shell instead of | |
| # restarting.) | |
| # | |
| # NOTE: Because this workaround deletes the fnm-generated symlink as soon as | |
| # it's created, the `fnm current` command will NOT work properly in | |
| # non-interactive shells (i.e., in scripts), unless you source this file there | |
| # too. (We recommend using `node --version` or similar instead of using `fnm | |
| # current` in your scripts anyway.) | |
| # | |
| # Also, because the symlink isn't used, it's impossible for a script to change | |
| # the active node version of a parent shell or script. (This is actually a | |
| # *feature*, for the most part, but if you currently have scripts that rely on | |
| # being able to change a parent shell's node version, they won't work if you're | |
| # using this.) | |
| # | |
| #EOF | |
| if [[ $0 = $BASH_SOURCE ]]; then | |
| # Script must be sourced, not executed; output docs to stderr | |
| sed -n -e '2, /^#EOF/ { /^#EOF/d; s/^# \?//; p }' "$0" >&2 | |
| exit 64 | |
| fi | |
| # Wrap fnm as a function that does repathing after successful `fnm use` | |
| # and supports `fnm current` | |
| fnm() { | |
| case "${1-}" in | |
| current) | |
| # We replace the built-in `fnm current` because it relies on the symlink | |
| # we're deleting | |
| fnm_repath | |
| case "$PATH" in | |
| *"$FNM_BASE/node-versions/"*) | |
| : "${PATH#*"$FNM_BASE/node-versions/"}"; echo "${_%%/*}" | |
| ;; | |
| *"$FNM_BASE/aliases/"*) | |
| local alias="${PATH/*"$FNM_BASE/aliases/"/"$FNM_BASE/aliases/"}" | |
| alias=${alias%%:*} | |
| linkpath="$(readlink "$alias")" | |
| if [[ $linkpath ]]; then | |
| : "${linkpath#*"$FNM_BASE/node-versions/"}"; echo "${_%%/*}" | |
| else | |
| echo "none" | |
| fi | |
| ;; | |
| *) echo "none" | |
| ;; | |
| esac | |
| ;; | |
| *) | |
| PATH="$PATH:$FNM_MULTISHELL_PATH" command fnm "$@" && fnm_repath | |
| ;; | |
| esac | |
| } | |
| # If FNM_MULTISHELL_PATH exists, read where it points to and put that on the | |
| # PATH in the first spot where a previous entry pointing into FNM_BASE is found, | |
| # or at the head of the PATH if no such entry exists. This lets you put things | |
| # in front of FNM on the PATH and keep them there even when swapping versions. | |
| fnm_repath() { | |
| # Don't repath if symlink already removed | |
| [[ ! -d "$FNM_MULTISHELL_PATH" ]] || { | |
| # Get the new path | |
| local linkpath="$(readlink "$FNM_MULTISHELL_PATH")" | |
| [[ ! "$linkpath" ]] || { | |
| # Got a path; move to head of PATH and remove the symlink | |
| local f p="" parts | |
| IFS=: read -ra parts <<<"$PATH" # split on ':' | |
| # Remove any path pointing into the FNM directory, replacing it | |
| # with the link path if found | |
| for f in "${parts[@]}"; do | |
| if [[ "$f" = "$FNM_BASE"/* ]]; then | |
| # Replace old path w/new if this is the first one found | |
| [[ ! $linkpath ]] || { p+="$linkpath:"; linkpath=; } | |
| else | |
| p+="$f:" | |
| fi | |
| done | |
| # Put the link path at the start of PATH if there was no old path | |
| # to replace | |
| PATH="${linkpath:+"$linkpath":}${p%:}" | |
| # remove the symlink so there's no symlink buildup | |
| rm "$FNM_MULTISHELL_PATH" | |
| } | |
| } | |
| } | |
| # Remove the multishell path and set up the base | |
| case ${OSTYPE-} in | |
| cygwin|msys) | |
| # Remove the multishell path from PATH | |
| PATH=${PATH#"$(cygpath "$FNM_MULTISHELL_PATH")":} | |
| # Prefix for PATH matching/updating | |
| FNM_BASE="$(cygpath "$FNM_DIR")" | |
| ;; | |
| *) | |
| # Remove the multishell path from PATH | |
| PATH=${PATH#"$FNM_MULTISHELL_PATH":} | |
| # Prefix for PATH matching/updating | |
| FNM_BASE="$FNM_DIR" | |
| ;; | |
| esac | |
| # Now initialize the PATH and get rid of the symlink | |
| fnm_repath |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment