From ZSH to Fish
Tags: fish, zsh, shell, terminal
Published: 2022-09-26 16:50:46

ZSH was my default shell for many years. I have modified and moved `.zshrc` across many different distributions of Linux. Recently I started working on a MacBook Pro, which has ZSH as the default shell. Using it for few months now, I noticed a few things about my ZSH configuration:

1. The startup of the new shell had minor delays before it could be used. These delays weren't huge, but I started noticing it more and more on MacOS. We tend to interact faster with systems that gives us [faster feedbacks](https://bootcamp.uxdesign.cc/importance-of-doherty-threshold-ebe65f0b9fab).
2. My `.zshrc` configuration had some plugins and configurations from `oh-my-zsh` that Fish shell mostly provides out-of-the-box.

With this in mind, I wanted to know how fast can I migrate to Fish shell while converting my current `.zshrc`.


## Fish Shell 🐠

### Getting ready

My `.zshrc` configuration looks like this:

```bash
# ---- ZSH ---- #
export ADOTDIR="$HOME/.antigen"
# Source antigen zsh plugin manager
source $HOME/.antigenrc
antigen init
# Autosuggestion color highlight
export ZSH_AUTOSUGGEST_HIGHLIGHT_STYLE="fg=#F2F0F6,underline"
export ZSH_AUTOSUGGEST_BUFFER_MAX_SIZE=30
# History settings.
export HISTSIZE=1000000
export HISTCONTROL='ignoreboth'
export HISTORY_IGNORE="(clear|history)"
SAVEHIST=$HISTSIZE
# Add wisely, as too many plugins slow down shell startup.
plugins=(copyfile
         extract
         git-extras 
         git-flow-avh 
         zsh-autosuggestions 
         zsh-completions
         zsh-syntax-highlighting 
         colored-man-pages
         z)

# ------------- #

# ---- User Configuration ---- #
## Nix specific
export NIX_PATH=darwin-config=$HOME/.nixpkgs/darwin-configuration.nix:$HOME/.nix-defexpr/channels${NIX_PATH:+:}$NIX_PATH
source $HOME/.nix-profile/etc/profile.d/hm-session-vars.sh
source /nix/var/nix/profiles/default/etc/profile.d/nix-daemon.sh

eval "$(starship init zsh)"
# Leave less preview on screen when quit
export LESS="-RX"
export EDITOR='nvim'
export LESS_TERMCAP_md="${yellow}"
# ---------------------------- #


# ---- Alias ---- #
alias vim="nvim"
alias l="exa --long --header --all"
alias ls="exa --long --header"
alias tree="exa --tree --level=2"
alias cat="bat"
alias ping="ping -c 3"
# Add verbose output on these commands
alias rm="rm -v"
alias mkdir="mkdir -v"
alias mv="mv -v"
alias cp="cp -v"
# --------------- #

#THIS MUST BE AT THE END OF THE FILE FOR SDKMAN TO WORK!!!
export SDKMAN_DIR="$HOME/.sdkman"
[[ -s "$HOME/.sdkman/bin/sdkman-init.sh" ]] && source "$HOME/.sdkman/bin/sdkman-init.sh"
```

### Setup

Install Fish and Fisher (*plugin manager for Fish*):

```bash
brew install fish fisher
```

Fish configurations are located at `~/.config/fish`. Now we can activate fish shell using `fish` command to get started.

#### Alias

To convert aliases, simply paste them and use `funcsave` to save them at `~/.config/fish/functions`:

```bash
alias cp="cp -v"
alias mv="mv -v"
...
funcsave cp mv ...
```

### ENV variables

Use [Universal Variables](https://stackoverflow.com/a/30187924/9871463):

```bash
set -Ux EDITOR "nvim"
...
```

### Plugins

Use Fisher to install several helpful plugins.:

```bash
fisher install jethrokuan/z             # Jump around the folders
fisher install decors/fish-colored-man
fisher install oh-my-fish/plugin-peco    # Ctrl + R for searching history
fisher install oh-my-fish/plugin-extract
```

Note that some of Fish plugins are enabled by default and there is no need to set the up like in ZSH.

I also found below plugins useful while converting my `.zshrc` (*though you may won't need them*)

```bash
fisher install reitzig/sdkman-for-fish
fisher install lilyball/nix-env.fish
fisher install jorgebucaran/nvm.fish
```

### Prompt

I use Starship which is "Cross-Shell Prompt" so my configurations for Starship are automatically applied. To enable it, just use:

```bash
echo "starship init fish | source" >> $HOME/.config/fish/config.fish
```

## Results

### Benchmarks

Using hyperfine at first run, I've noticed Fish is faster than ZSH to some extent:


![Benchmark results using Hyperfine on both ZSH and Fish](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/h7kl9a6td56izvugtowl.png)

I got curious why both shells take so much time to start. Profiling both shells, I figured out that both of them have performance degradation due to SDKMan enabled (*manages multiple Java Distributions similar to NVM*). 

Once SDKMan is removed for both shells, I re-ran benchmarks again:


![Benchmark results using Hyperfine on both ZSH and Fish](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/8j5m4pg9jqpl5kdqsdb1.png)

With both shells being almost similar in my setup, Fish shell ran **3** times faster than ZSH.

### Benefits

I find my shell configuration more organized now using Fish. Syntax of Fish scripts feel lot more readable compared to ZSH/Bash scripts. Other noticeable features that I found useful:

- Auto suggest command completions (`fish_update_completions`)
- `CTRL` + `R` with Peco for searching history easily
- Great plugins
- Faster compared to my ZSH

### Gotchas

Although converting configurations was extremly fast, I spent a lot of time on following:

- I keep backups of ZSH history dating back several years. There is a tool to convert [ZSH history to Fish](https://github.com/rsalmei/zsh-history-to-fish), but it didn't kept timestamp of execution. Also, some of multiline commands
(*such as `curl`*) broke `fish_history` file. The tool works, but may have to manually correct errors.
- Poetry completions produce [errors](https://github.com/python-poetry/cleo/issues/84#issuecomment-1255627939). I though it was error something due to Fish, but found out later that this will be fixed.
- Some of the builds scripts (Makefiles, builds tasks...) exported certain ENV variables using `export` so running it with Fish produced errors.


## Conclusion

In this state, I have migrated from ZSH to Fish really fast and set it as default shell, keeping the same configurations. Without gotchas that I ran in to, time of converting was less than 10 minutes. I find my shell much faster now, using  new plugins and some other that Fish provides by default.

At the end, I suggest you try it. If it doesn't work, you can always go back to your previous configuration.