I’ve been a user of the fish shell for years and I absolutely love it, mainly because it has so many features out-of-the-box.
The thing is, fish is also a language, so you can’t run bash-specific commands and syntax in fish.
It’s not an issue most of the time, but it is when you want bash programs to integrate with your shell. In the case of this article: programming languages version managers.
I am not familiar with every language that exist out there and their version manager, especially since there are multiple per language. But here are some tips if you want to use the same as me. 😁
pyenv, rbenv, goenv…
I’m putting these 3 together because they are nearly the same. In fact, goenv
is a fork of pyenv
which is a fork of rbenv
and ruby-build
. There are probably other forks that I don’t know, but this article covers them too.
They use the PATH
and shims to integrate with you system in a seamless way. Please read this if you want to know more.
Installing them is easy, I did it with brew on my Mac. But You won’t be able to use the versions managed by them until they mess with your PATH
like I described above. For instance, you can install and setup a specific version of python all you want, the python
command will still call the native python binary or your machine.
To be more precise, here’s what each of these version manager has to do.
To quote the pyenv
README:
pyenv init
is the only command that crosses the line of loading extra commands into your shell. Coming from rvm, some of you might be opposed to this idea. Here’s whatpyenv init
actually does:
Sets up your shims path. This is the only requirement for pyenv to function properly. You can do this by hand by prepending
$(pyenv root)/shims
to your$PATH
.Installs autocompletion. This is entirely optional but pretty useful. Sourcing
$(pyenv root)/completions/pyenv.bash
will set that up. There is also a$(pyenv root)/completions/pyenv.zsh
for Zsh users.Rehashes shims. From time to time you’ll need to rebuild your shim files. Doing this on init makes sure everything is up to date. You can always run
pyenv rehash
manually.Installs the sh dispatcher. This bit is also optional, but allows pyenv and plugins to change variables in your current shell, making commands like
pyenv shell
possible. The sh dispatcher doesn’t do anything crazy like overridecd
or hack your shell prompt, but if for some reason you needpyenv
to be a real script rather than a shell function, you can safely skip it.To see exactly what happens under the hood for yourself, run
pyenv init -
.
pyenv init -
will output what commands are used to do all of this.
Here’s what it looks like:
$ pyenv init -
export PATH="/Users/stanislas/.pyenv/shims:${PATH}"
export PYENV_SHELL=bash
source '/usr/local/Cellar/pyenv/1.2.5/libexec/../completions/pyenv.bash'
command pyenv rehash 2>/dev/null
pyenv() {
local command
command="${1:-}"
if ["$#" -gt 0]; then
shift
fi
case "$command" in
rehash|shell)
eval "$(pyenv "sh-$command" "$@")";;
*)
command pyenv "$command" "$@";;
esac
}
Note: it’s just an output, nothing is actually modified.
Then the docs tell you to add eval "$(pyenv init -)"
to your shell’s profile so that it’s executed every time your start your shell. FYI we pass the output of init -
to eval so that it executes the commands that are output.
There are two issues with this:
eval "$(pyenv init -)"
will not work with fish- the commands of
init -
are using bash syntax, not fish
However here’s what’s good with these scripts: they’re fish compatible and the output of init -
will depend on the shell.
Here’s how it looks like under fish:
~> pyenv init -
set -gx PATH '/Users/stanislas/.pyenv/shims' $PATH
set -gx PYENV_SHELL fish
source '/usr/local/Cellar/pyenv/1.2.5/libexec/../completions/pyenv.fish'
command pyenv rehash 2>/dev/null
function pyenv
set command $argv[1]
set -e argv[1]
switch "$command"
case rehash shell
source (pyenv "sh-$command" $argv|psub)
case '*'
command pyenv "$command" $argv
end
end
You can see the use of set
instead for export
, the function’s syntax, etc, are fish and not bash!
Now, instead of adding eval "$(pyenv init -)"
to our fish config, we can add source (pyenv init - | psub)
.
This is just how fish works. I was curious about psub
tough, because it is required. It turns out it’s a fish command using what’s called process substitution.
Pipes and input redirects shove content onto the STDIN stream. Process substitution runs the commands, saves their output to a special temporary file and then passes that file name in place of the command. Whatever command you are using treats it as a file name. Note that the file created is not a regular file but a named pipe that gets removed automatically once it is no longer needed.
To get back to our example, my ~/.config/fish/config.fish
now has the following :
source (rbenv init - | psub)
source (pyenv init - | psub)
source (goenv init - | psub)
And… that’s all!
It works perfectly:
stanislas@mbp ~> pyenv -v
pyenv 1.2.5
stanislas@mbp ~> pyenv versions
system
2.7
* 3.7.0 (set by /Users/stanislas/.pyenv/version)
stanislas@mbp ~> python -V
Python 3.7.0
It’s more straightforward under bash or zsh because you just have to add a line in your profile, and this line is on the README, but in the end, once you know how to do it with fish, it’s as easy.
nvm
Now, nvm
is not as easy. Whereas rbenv
and its forks just mess with your PATH
, nvm needs to be executed by your shell.
Quoting the README:
The script clones the nvm repository to
~/.nvm
and adds the source line to your profile (~/.bash_profile
,~/.zshrc
,~/.profile
, or~/.bashrc
).export NVM_DIR="$HOME/.nvm" [-s "$NVM_DIR/nvm.sh"] && \. "$NVM_DIR/nvm.sh" # This loads nvm
However it’s a bash script that won’t work with fish at all.
The README officially states this, and redirect to alternatives:
Note:
nvm
does not support [Fish] either (see #303). Alternatives exist, which are neither supported nor developed by us:
- bass allows you to use utilities written for Bash in fish shell
- fast-nvm-fish only works with version numbers (not aliases) but doesn’t significantly slow your shell startup
- plugin-nvm plugin for Oh My Fish, which makes nvm and its completions available in fish shell
- fnm - fisherman-based version manager for fish
There are a lot of of other solutions in the linked issue, even rewrites of nvm in fish.
In the end I chose to use this solution that combines bass and a function. bass is a fish function that allows to run bash script in fish.
To install bass
, follow the README, same for nvm.
Then create the nvm
function in ~/.config/fish/functions/nvm.fish
:
function nvm
bass source ~/.nvm/nvm.sh --no-use ';' nvm $argv;
end
This function will actually run nvm.sh
is bass before every nvm
command, thus updating our pass and allowing the use of the nvm
command.
Now, you can call the nvm function that will source + execute nvm.
stanislas@mbp ~> nvm --version
0.33.11
stanislas@mbp ~> nvm install node
v10.6.0 is already installed.
Now using node v10.6.0 (npm v6.1.0)
stanislas@mbp ~> nvm use node
Now using node v10.6.0 (npm v6.1.0)
stanislas@mbp ~> node -v
v10.6.0
stanislas@mbp ~> npm -v
6.1.0
The drawback is, you will have to use nvm use node
every time you want to use node in a new shell, otherwise it will not be in your path. This is fine if you’re a casual node user, but in the case you’re not, add this to ~/.config/fish/config.fish
:
nvm use node > /dev/null 2>&1
This will call our nvm
function and add node to the PATH
. I added > /dev/null 2>&1
in order to not have the Now using node...
message at the start of every shell. The drawback of nvm is that it’s slow, so executing it at the start of every shell will add about 2s to the startup.
Anyway, that’s it. It was a bit more tricky for nvm, but we managed to have a functioning dev environment without doing too dirty things. Enjoy!