May 21, 2018
As someone who starting computing as a teenager in 2000 with the “public beta” of Mac OS X, my first shell was Bash. Seventeen years later, I still use Bash every day.
When I started grad school at Davis, my lab mate Steve showed me
zsh, a Bash-like shell which provides some colorful and predictive features on top of the basic
bash functionality. The main thing that I liked was that when you tab-completed a directory name and then pressed tab again, it would present a list of all the possible choices immediately, and you could cycle through the list with your arrow keys and select the item you wanted.
This was a great feature, but there were some things about
zsh that I didn’t like, that I probably could have configured away, for example slowdowns when changing to large git repos, that caused me to switch back to Bash as my default shell.
I recently came across
fish, the Friendly Interactive Shell. The tagline: “finally, a shell for the 90s”. And the point is well taken: shells and command line interfaces can seem arcane, especially to beginners used to the highly developed touchscreen interfaces of 2017.
It’s certainly true that the shell doesn’t provide a flashy GUI with many elements, yet the designers of
fish have made many different design decisions aimed at providing a flexible, powerful, and expressive set of tools for their users.
fish makes many opinionated design decisions that just work. For example, in
fish, stderr is redirected with a caret
^. Since the standard way of redirecting
>, this design choice seems coherent and is easy to remember, make the
bash answer to the common question of how to redirect both
stderr to a file (
command 2>&1 filename) look silly in comparison. In
fish, this would be
command >^ filename.
When I was first using Fish, I found myself asking “Where are my config files? Why is there no
It seems that the Fish designers wish that everyone would store user files in
~/.config/appname/ rather than
~/.appname. You can find Fish configuration files in
~/.config/fish/config.fish but there is no file called
In Fish, you can create simple functions and store them in
~/.config/fish/config.fish. There are no aliases in Fish, but you can use simple functions instead.
function ll # simple function to alias ls -l ls -l $argv end
Fish functions automatically know the values in
argv, which is a list of all the variables that were passed as arguments to the function.
function la # list detailed by last access ls -ltuhs $argv end
or complex ones
One nice aspect of Fish is that many common shell commands are implemented as
.fish scripts, so you can easily modify the behavior of built-in commands. This can be a blessing or a curse: best to keep modifications minimal. One simple modification to the
cd command allows you to pass the flag
cd instead of the name of a directory so that
cd -r takes you to the most-recently—accessed directory.
Here is the source code of the
cd command in full, with the
-r option enabled. Put in your
~/.config/fish/config.fish to test it out in your shell.
function cd --description 'Change directory with -r (recent) flag' set -l MAX_DIR_HIST 25 if test (count $argv) -gt 1 printf "%s\n" (_ "Too many args for cd command") return 1 end # Skip history in subshells. if status --is-command-substitution builtin cd $argv return $status end # Avoid set completions set -l previous $PWD if test "$argv" = "-" if test "$__fish_cd_direction" = "next" nextd else prevd end return $status end if test "$argv" = "-r" # will determine the value of argv for us # using the most-recently modified file set argv (ls -tu1p | grep '/$' | head -1) end builtin cd $argv set -l cd_status $status if test $cd_status -eq 0 -a "$PWD" != "$previous" set -q dirprev[$MAX_DIR_HIST]; and set -e dirprev set -g dirprev $dirprev $previous set -e dirnext set -g __fish_cd_direction prev end return $cd_status end
After about two years of use on and off, I got frustrated with some downsides of
fish that affected my particular setup, and I learned a few things about
bash that allowed me to switch back and not give up any of the great aspects (for me) of
The major thing that did not work correctly was debugging Flask apps locally. In recent versions of Flask, you run an app locally by
exporting a variable
FLASK_APP that points to the Python file containing your Flask app. However, in Fish this did not work for me, even using the Fish command
set instead of the Bash
Also, I learned that you can easily make Bash behave like
fish in listing choices for tab completion by adding a single line to your
The last piece, for me, was
z. This efficient command allows you to navigate to frequently-accessed directories by fuzzy matching the name. For example, I can jump to my
Desktop directory from anywhere using
but typing tilde slash requires the use of a modifier key (Shift), and, on my keyboard anyway, the tilde is above the Tab on the left hand side. With
z, this can easily be
Your fingers almost even stay on the home row. Not having to type the tilde and the slashes is great, and even works with things like
z long path to my project files # now in /Users/alex/Projects/path/to_my/project/files
menu-complete to Tab and installing
brew install z), I got 95% of the additional features from
zsh that I liked from those shells, with all the comfort of Bash. I’ve been using Bash every day since then.