A while ago, I switched from bash to fish. This time, I stuck with it. After reading this, you should have a better idea of the hassles involved with switching to a new shell.

(I’m going to assume that you know what a shell is. While I don’t expect you to have used anything other than bash, I assume you’ve heard of other shells like zsh and tcsh.)


Even an old version of bash is good enough

Let’s get this out of the way: bash ain’t half bad. command.com, used for both DOS and Windows, didn’t even do command tab completion. cmd.exe, used in Windows NT up through Windows 10, only started doing command completion by manually changing a setting. bash does this out of the box. Furthermore, even an old version of bash, like the 3.2 that ships with macOS 10.14 Mojave, does all this. (Every so often I check bash’s NEWS file; none of the additions seem memorable enough to justify switching to a new version.)

bash also has nifty-keen substitution parameters like !! and !$. They’re handy.

Initially everything is just a little bit wrong

As you might expect, a fresh fish shell is way different from my moderately-customized bash settings. fish color settings aren’t too important, but I’m used to my style of prompt and I didn’t want much, if anything, to change about it. The biggest annoyance? fish automatically shortens paths in prompts. That is, ~/Pictures/Funny/Autocorrect Failures would get shortened to ~/P/F/Autocorrect Failures and I’d have to type pwd to reorient myself in the file system hierarchy. Piecing together different prompts and their writing styles, I eventually settled on this functions/fish_prompt.fish:

function fish_prompt --description 'Write out the prompt'

    set -l last_status $status

    # `set -U ARROW_ONLY yes` and `set -eU ARROW_ONLY` to toggle
    # good for presentations
    if test -n "$ARROW_ONLY"
        echo '> '
        return
    end

    if test $last_status -ne 0
        printf "%s(%d)%s " (set_color red --bold) $last_status (set_color normal)
    end

    set -l color_cwd
    set -l suffix

    switch "$USER"
        case root toor
            if set -q fish_color_cwd_root
                set color_cwd $fish_color_cwd_root
            else
                set color_cwd $fish_color_cwd
            end
            set suffix '#'
        case '*'
            set color_cwd $fish_color_cwd
            set suffix '>'
            if set -q fish_private_mode
                set suffix '»'
            end
    end

    # set $fish_prompt_pwd_dir_length to 0 to disable shortening in prompt_pwd
    echo -n -s \
        (set_color --bold $fish_color_user) \
        "$USER" @ (prompt_hostname) \
        ' ' \
        (set_color --bold $color_cwd) \
        (prompt_pwd) \
        (set_color normal) \
        " $suffix "
end

It’s a bit fancier than my old $PS1, but it’s still reasonably clear to read for a novice like me. While the documentation for $fish_prompt_pwd_dir_length wasn’t easy to find, everything else was. This was a refreshing change compared to what I remember of bash documentation. With bash’s web-based info pages, I never seemed to be able to find how to do anything I wanted and had to rely on other sites to explain bash’s feature set to me.

Settling in

The fish documentation talks about universal variables. At first blush, variables that are shared between all instances of a shell sounded like a useful idea. Every so often I’ve wanted to change something about bash and had to paste-and-run source ~/.bashrc in all my running bash instances. This is an annoyance, if an infrequent one. Unfortunately, I found it’s much more useful to be able to put my variable settings — and especially additions — in config.fish, the .bashrc/.bash_profile equivalent. To see why, have a look at a snippet of my fish_variables:

# This file contains fish universal variable definitions.
# VERSION: 3.0
SETUVAR __fish_init_2_39_8:\x1d
SETUVAR __fish_init_2_3_0:\x1d
SETUVAR __fish_init_3_x:\x1d
SETUVAR fish_color_autosuggestion:BD93F9
SETUVAR fish_color_cancel:\x2dr
SETUVAR fish_color_command:normal
# …
SETUVAR fish_pager_color_prefix:white\x1e\x2d\x2dbold\x1e\x2d\x2dunderline
SETUVAR fish_pager_color_progress:brwhite\x1e\x2d\x2dbackground\x3dcyan
SETUVAR fish_prompt_pwd_dir_length:0

Now imagine trying to edit your $PATH if it were separated by not just colons, but backslash-escaped UTF-8 byte sequences of Private Use Area code points. Ick. Meanwhile, the old-fashioned way preserves the ability to logically group similar path adjustments:

set -x PATH $HOME/Library/Python/2.7/bin $PATH
set -x PATH /Library/Frameworks/Python.framework/Versions/2.7/bin $PATH
set -x PATH /Library/Frameworks/Python.framework/Versions/3.5/bin $PATH

set -x GOPATH $HOME/Projects/Go
set -x PATH $HOME/Projects/Go/bin $PATH

set -x PATH $HOME/bin $PATH

This is doubly important for anything that needs to be documented on a per-chunk basis:

# for xterm-256color:
#
# - 38;5 — next number sets text color
# - 48;5 — next number sets background color
#
# (colors at, say, <https://i.stack.imgur.com/UQVe5.png>)
set -x EXA_COLORS
set -a EXA_COLORS "di=38;5;69"      # directories
set -a EXA_COLORS "da=38;5;195"     # times

set    EXA_COLORS (string join ":" $EXA_COLORS)

While it’d be neat to have exa colors propagate instantly, editing its fish_variables entry would be as fun as editing a 120-column, one-line regular expression.

Yes, this is a lot of busywork

If all this sounds like I’m doing lots of work to merely get my fish setup to be as good as my bash setup, you’re right. What’s more, I hadn’t really seen any of the benefits of fish yet.

Eventually, I found a lot of things to like about fish that made me glad I switched.