The fish shell does not support the bash syntax and manage its PATH differently. Here are some tips to use your version managers with fish!

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 what pyenv init actually does:

  1. 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.

  2. 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.

  3. 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.

  4. 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 override cd or hack your shell prompt, but if for some reason you need pyenv 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.

Here is a good explanation:

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!