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 bash
, zsh
, and 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 stdout
is >
, this design choice seems coherent and is easy to remember, make the bash
answer to the common question of how to redirect both stdout
and 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 .fishrc
?”
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 .fishrc
.
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 -r
to 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[1]
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 fish
.
The major thing that did not work correctly was debugging Flask apps locally. In recent versions of Flask, you run an app locally by export
ing 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 export
.
Also, I learned that you can easily make Bash behave like zsh
and fish
in listing choices for tab completion by adding a single line to your .bashrc
.
bind 'TAB:menu-complete'
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
cd ~/Desktop
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
z desk
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
Between binding menu-complete
to Tab and installing z
(with brew install z
), I got 95% of the additional features from fish
and zsh
that I liked from those shells, with all the comfort of Bash. I’ve been using Bash every day since then.