Repository: nvm
Issue: `nvm uninstall` slowed down by (sometimes infinite) recursion into symlinked directories
Labels: pull request wanted,performance

## The issue

`nvm uninstall`, specifically the `nvm_check_file_permissions` helper, is unnecessarily slowed down by recursing into symlinked directories inside the global node_modules. In rare cases, it can get into infinite recursion.

## Context

`npm link ../related-module` can be used to test changes in another module, without needing to repeatedly publish or pack on each change. For some reason, instead of a direct symlink, NPM creates 2 layers of symlinks - from the original repo's `node_modules/related-module` to the global node_modules, then from the global node_modules to the destination directory.

`nvm_check_file_permissions` follows the symlink from the global node_modules directory and recurses into the destination tree, which can take a long time if there are a lot of symlinks or large symlinked directories.

Also, if the symlink destination happens to itself have a circular symlink somewhere, `nvm_check_file_permissions` goes into infinite recursion, which is how I discovered this issue. 😓

## Proposal

I believe `nvm_check_file_permissions` shouldn't need to follow symlinks, as all that matters is whether the current user has permission remove the symlinks in the global node_modules directory.

Changing [the directory check](https://github.com/nvm-sh/nvm/blob/d1a22a63bd38c0392393044354297e5ca02bc0cf/nvm.sh#L2757) to skip symlinks with `&& [ ! -L "$FILE" ]` worked for me to resolve the infinite recursion and let the uninstall finish. With the change, a symlink will go to the `else` case, which checks that the current user can delete it.

Anything I'm missing?

I could create a PR, but it'd likely just be the source code change for now, since I'm not sure I'll have time to look at writing a good test for it this week or next.

## Minimal repro
Pick an unused Node version for the fresh install. Here, I picked a non-LTS non-latest version, as it's less likely to be in use.

Setup
```
nvm install v19.8.1
cd $(npm root -g)
ln -s . im-a-circular-symlink
```

Run `ls -al` to see the symlink in the global node_modules.

To attempt the deletion:
```
nvm use default
NVM_DEBUG=1 nvm uninstall v19.8.1
```
Watch the infinite recursion for a while, Ctrl+C to stop.


## More practical repro

This demonstrates it's possible to get into the situation under a more normal scenario.

First, cd to a directory you clone Git repos to.
```
nvm install v19.8.1
git clone https://github.com/share/sharedb.git
cd sharedb/examples/rich-text
npm link ../..
nvm use default
NVM_DEBUG=1 nvm uninstall v19.8.1
```
Someone would run `npm link ../..` to try the example with the local code, instead of with the package installed off NPM.

Run `ls -al $(npm root -g)` to see the symlink in global node_modules that was created by `npm link`.

# Issue template

#### Operating system and version:
MacOS 13.3.1 (22E261)

#### `nvm debug` output:
<details>
<!-- do not delete the following blank line -->

```sh
nvm --version: v0.39.3
$TERM_PROGRAM: Apple_Terminal
$SHELL: /bin/bash
$SHLVL: 1
whoami: 'ehwang'
${HOME}: /Users/ehwang
${NVM_DIR}: '${HOME}/.nvm'
${PATH}: ${NVM_DIR}/versions/node/v18.16.0/bin:/usr/local/bin:/System/Cryptexes/App/usr/bin:/usr/bin:/bin:/usr/sbin:/sbin:/var/run/com.apple.security.cryptexd/codex.system/bootstrap/usr/local/bin:/var/run/com.apple.security.cryptexd/codex.system/bootstrap/usr/bin:/var/run/com.apple.security.cryptexd/codex.system/bootstrap/usr/appleinternal/bin:${HOME}/.npm/bin:${HOME}/go/bin:${HOME}/Library/Python/3.10/bin
$PREFIX: ''
${NPM_CONFIG_PREFIX}: ''
$NVM_NODEJS_ORG_MIRROR: ''
$NVM_IOJS_ORG_MIRROR: ''
shell version: 'GNU bash, version 3.2.57(1)-release (x86_64-apple-darwin22)'
uname -a: 'Darwin 22.4.0 Darwin Kernel Version 22.4.0: Mon Mar 6 21:00:17 PST 2023; root:xnu-8796.101.5~3/RELEASE_X86_64 x86_64'
checksum binary: 'shasum'
OS version: macOS 13.3.1 22E261
awk: /usr/bin/awk, awk version 20200816
curl: /usr/bin/curl, curl 7.87.0 (x86_64-apple-darwin22.0) libcurl/7.87.0 (SecureTransport) LibreSSL/3.3.6 zlib/1.2.11 nghttp2/1.51.0
wget: not found
sed: /usr/bin/sed
cut: /usr/bin/cut
basename: /usr/bin/basename
rm: /bin/rm
mkdir: /bin/mkdir
xargs: /usr/bin/xargs
git: /usr/bin/git, git version 2.39.2 (Apple Git-143)
grep: /usr/bin/grep, grep (BSD grep, GNU compatible) 2.6.0-FreeBSD
nvm current: v18.16.0
which node: ${NVM_DIR}/versions/node/v18.16.0/bin/node
which iojs: 
which npm: ${NVM_DIR}/versions/node/v18.16.0/bin/npm
npm config get prefix: ${NVM_DIR}/versions/node/v18.16.0
npm root -g: ${NVM_DIR}/versions/node/v18.16.0/lib/node_modules
```
</details>

#### `nvm ls` output:
<details>
<!-- do not delete the following blank line -->

```sh
       v14.19.3
       v16.20.0
       v18.14.0
->     v18.16.0
default -> v18 (-> v18.16.0)
iojs -> N/A (default)
unstable -> N/A (default)
node -> stable (-> v18.16.0) (default)
stable -> 18.16 (-> v18.16.0) (default)
lts/* -> lts/hydrogen (-> v18.16.0)
lts/argon -> v4.9.1 (-> N/A)
lts/boron -> v6.17.1 (-> N/A)
lts/carbon -> v8.17.0 (-> N/A)
lts/dubnium -> v10.24.1 (-> N/A)
lts/erbium -> v12.22.12 (-> N/A)
lts/fermium -> v14.21.3 (-> N/A)
lts/gallium -> v16.20.0
lts/hydrogen -> v18.16.0
```
</details>

#### How did you install `nvm`?
Install script in README

#### What steps did you perform?
Ran `nvm uninstall v14.19.3`

#### What happened?
Command hangs for over an hour before I stopped it to investigate

#### What did you expect to happen?
The Node version to get uninstalled in a reasonable amount of time

#### Is there anything in any of your profile files that modifies the `PATH`?
Yes. Just one for Python and Go binaries, irrelevant to this.