I’ve used bash for a long time and zsh with a plugin manager for a few years. My .zshrc had slowly turned into something I was afraid to touch, so I gave fish a proper month instead of the usual afternoon.

Short version: I’m keeping it for interactive use. Scripts stay in bash.

What I like

Good defaults. Autosuggestions from history, syntax highlighting, and completions generated from man pages all work out of the box. That replaced four zsh plugins.

Abbreviations instead of aliases. An abbreviation expands in place when you hit space, so your history contains the real command:

abbr -a gs git status
abbr -a gco git checkout
abbr -a k kubectl

Functions are just files. Anything in ~/.config/fish/functions/name.fish is autoloaded the first time you call it. funced and funcsave make quick edits painless:

function mkcd
    mkdir -p $argv[1]; and cd $argv[1]
end
funcsave mkcd

PATH handling. fish_add_path deduplicates and persists, so no more PATH growing on every nested shell:

fish_add_path ~/.local/bin

What takes getting used to

fish is not POSIX. Variables use set (there’s an export wrapper, but set -gx is the native way), and $(...) only arrived in fish 3.4, so I just use (...):

set -gx EDITOR nvim
set files (ls *.md)

Sourcing a bash script that sets environment variables doesn’t work directly. For the two tools that need it, I either use the fish integration they ship (most do now) or run them through bash.

My setup

My whole config.fish is now about 30 lines. Most of it is abbreviations and a couple of status is-interactive guards. Compared to the old zsh setup, startup is faster and I actually know what every line does.

If you’ve bounced off fish before because a copy-pasted command from Stack Overflow didn’t work, the trick is simple: keep #!/usr/bin/env bash for scripts, and type bash for the rare one-off.