Snacker News

Switching to fish from bash

May 21, 2017

As someone who starting computing as a teenager in 2000 with the “public beta” of Mac OS X, my first shell was GNU Bash. Seventeen years later, I still use Bash every day.

A trip to the Z-shell

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.

A shell for the 90s

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.

Notes on configuring Fish

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.

Setting aliases in Fish

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

Example of extending Fish

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

Back to Bash

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