When I am coding, I use the terminal a lot. Over the time, I have tried out different popular shells1: bash, zsh, and fish. For around one year now, I have settled for the fish shell. In this post, I want to explain how I got there, why I am very happy with fish, and how I have configured it.
This is how my shell looks right now:
If you are not sure which shell is the default on your system, look at the output of this command2:
If you are using Linux, this most likely will be bash. On macOS, the default is zsh3.
The beginning: Bash
When I started using the terminal on Linux, I just used what was there: Bash. Basically all tutorials in the internet assume that you are using it, so copy-pasting commands from StackOverflow just worked.
Over time, I realized I was typing too much. I learned about the shortcut
Ctrl+R to do a reverse/backwards search in history (I could not live without this shortcut anymore…). But looking at the shells of other students, I saw that their shells (a) looked nicer, and (b) automatically suggested command completions as they were typing. I wanted this, too! This is how it looks like:
More customization & auto-completion: zsh (oh-my-zsh)
I learned that auto-suggestions were a plugin for oh-my-zsh, a customization framework for zsh. So I switched to zsh with oh-my-zsh and installed the zsh-autosuggestions plugin.
Bonus: oh-my-zsh is bundled with a lot of other plugins & themes, and you can simply enable them. For example, there is the z plugin: With the new command
z, you can quickly jump to frequently-used directories by just typing parts of the directory path:
Also, its default theme, “robbyrussell”, is pretty great. It not just looks very nice, it is also useful: For example, it displays whether the current directory is tracked by git, and shows the name of the branch you are currently at.
⚠️ Caution: Syntax differences between bash and zsh
By switching from bash to zsh, you need to be aware that the command syntax is not 100% the same. The most important difference I observed is related to filename expansion, also called “globbing”.
Take this command:
If you execute this command in a directory with two files or folders called
blub, the output for bash and zsh will be the same:
But if you execute this command in a directory without any matching file (e.g. an empty directory), the output will be different:
If no files match the expression, bash will simply pass the glob expression as text to the command. zsh instead will fail the whole execution without executing the command at all.
I think zsh’s behaviour is pretty reasonable. But some commands found in the internet will expect the glob expression to get passed to the command. So the command won’t work in zsh.
The fastest solution: Enclosing the expression with quotes, so no expansion will take place, but the expression gets passed as a string:
This works in both bash and zsh. Here are some StackOverflow discussions where this was the issue: One related to
rsync, and one related to
Works Out of the Box: Switching to fish
My setup with zsh & oh-my-zsh worked well for me for a couple of years and I still recommend it without hesitation. But then I got to know the fish shell. Fish’s main advantage for me is: It comes with much more features out of the box, and does many things smarter, without any extra package or configuration.
Take the autosuggest feature for example. For zsh, we had to install oh-my-zsh and the the zsh-autosuggest plugin. For fish, the feature is already built-in! In fact, the description of the zsh-autosuggest plugin is “Fish-like autosuggestions for zsh”. So that’s that. Moreover, the autosuggest is better than zsh’s: Fish’s autosuggest will only suggest commands that actually succeeded, zsh’s autosuggest does not take that into account.
Two more features sold Fish for me: Smart tab completions & syntax highlight to detect potential errors.
Smart tab completions
Fish’s tab completions are much smarter and helpful than the ones in bash or zsh. See this screenshot - in all shells, I have typed
git com, followed by a
As you can see, bash did not complete my command at all. I likely could install a package for that (maybe bash-completion?), but there was no completion out of the box. Zsh did complete the word
commit, but did not offer any help besides that. Fish did complete
commit as well, and in addition, it mentions that there are multiple commands that start with
commits-since, including a short description of each one. Especially interesting: The
commits-since command is not part of the normal git installation, but is available because I have git-extras installed, a collection of useful utilities for git. So fish is aware of these extra utility commands, and suggests them as well. How cool is that!? 😀
Fish goes one more step further. It can complete command arguments, too, including descriptions of what an argument does:
I am pretty sure I could somehow configure bash or zsh to provide me with argument completions, too - but with fish, it just works out of the box. As far as I understand it, fish does that by parsing a commands man pages, and automatically generating the tab completions from that - without the need for extra packages that provide completions for specific commands.
Syntax highlight to detect potential errors
Another neat feature in fish is its syntax highlighting. If a command has an error (e.g. because you have a typo in the tool you want to call, or because a file path does not exist), it marks the command in red:
My additional configuration for fish
Fish works really well out of the box. Over the time, I still have configured it a bit to fit my needs:
- I have installed the oh-my-fish framework - I simply did that to get the “robbyrussell” theme that I knew & liked from oh-my-zsh. I haven’t used any other feature of oh-my-fish yet.
- I have installed fisher, a plugin manager for fish (oh-my-fish can be used as a plugin manager, too, but fisher seems to be more popular). With fisher, I have installed these two plugins:
- Backwards-search: While I like fish’s autosuggestions a lot, I do not like their approach to backwards-searching commands.
In fish, you are supposed to start typing a command, and then use the up & down arrow keys to navigate between suggestions from your history that start the same way. And this is the issue: If you don’t remember the beginning of a command, but just some part in-between, this search does not work - the backwards-searches in bash & zsh doTherefore, I have installed fzf. Amongst other features, this adds backwards-search with
Ctrl+R4. The search is even better than the ones built-in into bash & zsh: You’ll see a list of results while you type, not just the top-match. And the search matching algorithm is fuzzy, so e.g.
gmiupwill match the command
git commit -m "Update".
- UPDATE 2022-11-18: I was wrong - fish’s backwards-search does not just match the start of a command, but will look for the search string everywhere - so just like bash & zsh. The fzf extension still makes backwards-search much better than the default, so I still recommend it.
- UPDATE 2023-01-12: With version 3.6.0, fish added support for
Ctrl+Rbackwards-search - and this search is pretty awesome. Like with fzf, it shows a list of results. An advantage: The entries in the list are syntax-highlighted. The only downside: It doesn’t do fuzzy matching (yet?) - this is the only reason I’ll stay with fzf’s backwards-search.
⚠️ Caution: Even larger syntax differences
One thing to keep in mind with fish: While zsh is pretty close to bash regarding syntax and how to configure it (
.zshrc), fish does more things differently. One thing that I have to look up regularly: Settings environment variables.
Most often, you will want to add directories to the
PATH variable. For that, there is a special command, fish_add_path, that does that & persists the change accross sessions:
For other cases of setting environment variables, it is very helpful to read into fish’s different “variables scopes”, and then closely read the documentation of the
One more thing to keep in mind: Fish’s config files are located in
~/.config/fish, not directly in
~. Most important is the
config.fish file, that gets executed for every new session. So, this file is roughly similar to bash’s
.bashrc and zsh’s
Regarding resources on learning to use fish, I can recommend the Fish shell playground (includes an in-browser terminal to play around with fish!), and the fish documentation and help pages. Good to know: By executing the command
help in your fish shell, it will open the fish manual in a browser window. This also works with specific fish commands, e.g.
help set. To see the documentation in the terminal, use
So, this is the story on how I ended up with my current shell setup. I am very happy with it - by using fish, most of the features come out of the box, and features like autosuggest & smart tab-completion really make it feel that the shell is helping me throughout my day. I hope that this post helps you in improving your shell setup, and helps you become more productive in the terminal! 🐠
Difference between shell & terminal, via superuser.com: A shell is a program that processes commands and returns output. A terminal is a wrapper program which runs a shell. Terminals used to be devices, but nowadays a terminal is purely abstracted in software. ↩︎
zsh is the default on macOS since macOS 10.15 (Catalina), around ~2019. ↩︎
After installing fzf, remember to activate the keybindings. E.g. when installing with Homebrew:
$(brew --prefix)/opt/fzf/install. I skipped this part in the docs when I first installed fzf, and it took me a lot of time until I got keybindings to work…To remove the keybindings again, remove the call to
- My Favorite Shortcuts for VS Code
- JSON5: JSON with comments (and more!)
- jq: Analyzing JSON data on the command line
- Get Total Directory Size via Command Line
- Changing DNS server for Huawei LTE router
- Notes on Job & Career Development
- Adding full-text search to a static site (= no backend needed)
- Generating a random string on Linux & macOS
- Caddy web server: Why use it? How to use it?
- Tailwind CSS: A Primer