Recreating NVM in Bash
Like most other Node developers, I’ve been using NVM for years. It’s a great tool that allows you to easily switch between different versions of Node. But have you ever wondered how it works under the hood? Or how to go without it if you’re feeling adventurous?
In this article, I’ll show you how to create a modest version of NVM in Bash. We’ll go through the core functionality of NVM and replicate it in a few lines of Bash. You’ll learn how to install and switch between different versions of Node without any external tools. And you’ll get a better understanding of how your system works.
Why I decided to do this
One day I decided to run nvm install
to get the current LTS version of Node. My machine at the time was an ARM-based MacBook Pro, for which a precompiled binary was already available. But to my surprise, NVM started downloading the source code and building it from scratch. “That’s weird”, I thought “And more than a little inefficient”.
Confused, I decided to investigate the source code of NVM. I soon found out that this seemingly simple CLI tool was a lot more complex than I expected. I wasn’t going to find my answer without going really deep into it.
“Screw it”, I decided “I’ll just do it myself”. I opened https://nodejs.org/, downloaded the precompiled binary, unzipped it, put it into a folder inside my home directory and updated my PATH
variable to include the new binary. “Wow, that was easy”, I thought “I wonder what else you can do with a few lines of Bash”.
What NVM really does
In a nutshell, all NVM actually does is:
-
It maintains a hidden folder
$HOME/.nvm
where it stores all the versions of Node that you’ve installed. -
When you need a specific version you can use
nvm install
to download it. NVM will then extract the binary to a folder inside$HOME/.nvm
. -
After that you can switch between the installed versions with
nvm use
. This command updates thePATH
variable to include thebin
folder of the specified version. -
You can also
nvm uninstall
a version to remove it from your system (and make more space on for cat memes or whatever).
That’s about it. There are a few more commands and options like nvm ls
to list all installed versions, but the core functionality is described above. I rarely use anything else in my day-to-day work.
Do it yourself in Bash
As the core functionality of NVM is quite simple, we can replicate it in a few lines of Bash. We’ll create a folder in our home directory where we’ll store all the Node versions. And we’ll write a few functions to install, use, and uninstall the versions.
Step 0: Create a folder for your Node versions
First, you need to create a folder for all your Node versions. Let’s call it .node_versions
and put it inside your home directory:
mkdir -p ~/.node_versions
The -p
flag will prevent an error if the folder already exists. Let’s cd
into our new folder:
cd ~/.node_versions
Step 1: Download and unzip the Node binary
First, you need to know the URL of the Node binary you want to install. You can find it on the official Node website. For example, the URL for the latest version of Node for macOS (Apple Silicon) is https://nodejs.org/dist/v22.5.1/node-v22.5.1-darwin-x64.tar.gz
There is a nice-looking download page at https://nodejs.org/en/download/prebuilt-binaries
. Or you can just open the root URL https://nodejs.org/dist/
and see all the available versions in a more raw form.
When we have the URL, we can download the binary with curl
and untar it with tar
. For example, to download the latest version of Node for macOS (Apple Silicon) you can run:
curl -o node.tar.gz https://nodejs.org/dist/v22.5.1/node-v22.5.1-darwin-x64.tar.gz
tar -xzf node.tar.gz
After running these commands you should see two new files in your ~/.node_versions
folder:
drwxr-xr-x 9 user group 288 Jul 19 05:32 node-v22.5.1-darwin-x64
-rw-r--r-- 1 user group 47832563 Jul 29 12:44 node.tar.gz
node.tar.gz
is the archive we downloaded, it can now be safely removed:
rm node.tar.gz
The other folder is the extracted Node binary. You can rename it to something more readable:
mv node-v22.5.1-darwin-x64 22.5.1
If you are following along, I suggest you download a couple more versions to test switching between them later.
Step 2: Making the version available in your shell
Now that we have the Node binary extracted, we need to make it available in our shell. We can do this by updating the PATH
variable.
export PATH=$HOME/.node_versions/22.5.1/bin:$PATH
By exporting the PATH
variable we are telling the shell to look for executables in the bin
folder of the specified Node version first. This way we can have multiple versions of Node installed and switch between them easily.
If you ls
the bin
folder you will see all the Node executables (like node
, npm
, corepack
, etc.):
ls $HOME/.node_versions/22.5.1/bin
You can test if the installation was successful by running:
which node
node --version
The first command should output the path to the node
executable (the one we just created) and the second command should output the version of Node we just installed.
Now technically you have a working Node version manager. You can install multiple versions of Node and switch between them by updating the PATH
variable. But we can make it a bit more user-friendly.
Step 3: Default version
If you exit the shell right now and open a new one, the PATH
variable will be reset to its default value. The Node version we just installed will be unavailable again. To make it persist between shell sessions we need to update our shell configuration file.
For example, if you are using bash
you can add the following line to your .zshrc
or .bashrc
file:
# ~/.bashrc or ~/.zshrc
export PATH=$HOME/.node_versions/22.5.1/bin:$PATH
Yes, exactly the same line we used before. That is what I love about using shell scripts. You can first do something manually to see if it works, and then just copy-paste it into a script to automate it.
To make this even more robust we can create an alias for the default
version. This way we can easily switch between versions without having to update the PATH
variable manually.
ln -s ~/.node_versions/22.5.1 ~/.node_versions/default
This command creates a symbolic link that points to the version we just installed. Now we can update the PATH
variable to include the default
version (replace 22.5.1
with default
):
export PATH=$HOME/.node_versions/default/bin:$PATH
Step 4: Switching between versions
We can now easily implement nvm use
and nvm alias default
functionality. First, let’s do nvm use
. It should be as simple as updating the PATH
variable for the current shell session (it won’t persist after you close the shell).
Add the following function to your .zshrc
or .bashrc
file to make it available in every shell session from now on:
nvm_use() {
export PATH=$HOME/.node_versions/$1/bin:$PATH
}
If you’re not familiar with shell scripting, the $1
variable is a placeholder for the first argument passed to the function. So if you run nvm_use 22.5.1
the function will update the PATH
variable to include the 22.5.1
version.
The nvm alias default
functionality is also simple. We just need a function that updates the default
symbolic link to point to a different version. Here is how you can implement it:
nvm_default() {
ln -sf ~/.node_versions/$1 ~/.node_versions/default
}
Now you can run nvm_default 22.5.1
to set the default
version to 22.5.1
. And you can run nvm_use default
to switch to the default
version.
Step 5: Listing installed versions
At this point, we’re almost done. We can install, switch, and set the default version. The only thing missing is a way to list all installed versions. We can do this by listing the contents of the ~/.node_versions
folder:
nvm_ls() {
ls -1 ~/.node_versions
}
The -1
flag tells ls
to list one file per line. Almost like -l
but without the extra information.
Step 6: Removing versions
Finally let’s create an inverse function to nvm install
. It should remove the specified version from the ~/.node_versions
folder. Here is the simplest way to do it:
nvm_remove() {
rm -rf ~/.node_versions/$1
}
Of course, there are a bunch of edge cases we are not handling here. For example, what if you try to remove the current default
version? Or what if you try to remove a version that is not installed? But this is a good starting point.
Conclusion
I tried to make it as simple as possible. In a real-world scenario you would want to handle possible errors, missing arguments, and make it more user-friendly in general. And probably move all these functions to a separate file so you don’t have to clutter your .zshrc
or .bashrc
file.
But I hope you can see how easy it can be to create your own Node version manager. It doesn’t have all the features of nvm
but it’s extremely simple and because of that, it’s easy to understand and modify. Which is the thing I love most about such projects.
I actually use a similar script to manage my Node versions. It’s a bit more complex and has a few more features but the core idea is the same. You can find it here.