Fish shell
A command line shell that does not think that the problem is you
2019-03-30 — 2025-11-18
Wherein the author’s migration to fish is described, its opinionated design and PATH peculiarities are noted, and SSH‑agent setup and backgrounding limitations are documented.
Not the aquatic creatures, but rather the command-line shell doohickey, which I like because it’s less annoying than bash. I’m gradually switching to fish after I accidentally lost a lot of precious data because of a quirk in bash syntax. Long, boring story. It’s time for new, exciting, different stupid errors.
fish has a strong fanbase and an opinionated design. If we dislike those design opinions, at least we might appreciate that its documentation expresses said opinions with healthy sarcasm. Such self-awareness is sorely absent from the drearily earnest nerdview of a typical gnu.org project.
I don’t agree with every design choice in fish, but I also think that having any kind of principled opinion is better than the design-by-accumulation-of-tradition-cruft that structures command-line shells.
1 Installing
- Ubuntu users can install an updated fish PPA.
- or, on macOS/Linux: install using homebrew
gararine reports how to make fish the default shell with homebrew:
# check the fish path with `which fish`. In the examples below it was located at: `/opt/homebrew/bin/fish`.
# Add fish to the known shells
sudo sh -c `which fish` >> /etc/shells
# Set fish as the default shell
chsh -s `which fish`
# Add brew binaries in fish path
fish_add_path /opt/homebrew/bin
#To collect command completions for all commands run:
fish_update_completions2 Configuration
2.1 Modifying the PATH
This is the most confusing part of fish for me. It’s worth reading some tutorials, e.g.
tl;dr:
To add a path, use the utility command fish_add_path.
There’s no fish_remove_path. Weird.
There’s an alternative method: we update a path variable like any other variable, which is confusingly related to the method above. Adding a path? Say it’s /usr/local/bin. Put
In ~/.config/fish/config.fish, or (“universal” style)
Removing a path?
🚧TODO🚧 Clarify the difference between $PATH and $fish_user_path, which depends on my understanding of how the content of $PATH magically replenishes itself, and on the difference between “universal” and “global” variables.
2.2 Modifying any settings with GUI
2.3 Traditional config
Put commands in ~/.config/fish/config.fish.
2.4 Extremely traditional config
Aelius notes a neat hack to unify the config:
one of the things I like about fish is how there are sane defaults and I don’t need to have any config. Which works for me, because I have no interest in learning fish syntax. I just want a helpful shell, I don’t want to have to know yet another language, and I deeply resent fish every time it doesn’t process the line of posix sh I paste into it from a wiki…
After jumping between several different shells and rewriting my
.profilea number of different times for a number of different shells, I came up with a way to decouple my environment config from the shell I use. My environment always works, I don’t have to learn fish or any other syntax.I set
/bin/dashas my login shell. the first line of my~/.profileisENV=$HOME/.shinit; export ENV.In any interactive shell, dash executes~/.shinit, which contains one line:exec /usr/bin/fish.Every config item I need from my shell goes into
~/.profile, written in easy, conventional posix sh— and I still get to use fish as my interactive shell, without having to go through the trouble of adopting its config to my system.
I don’t do this myself. I’m all in on fish.
2.5 ssh-agent
Optiligence notes that this minor alteration should work.
Alternatively, check out danhper/fish-ssh-agent or ivakyb/fish_ssh_agent.
Installation:
Append the next line to ~/.config/fish/config.fish
We really need to verify that https://gitlab.com/kyb/fish_ssh_agent/raw/master/functions/fish_ssh_agent.fish isn’t malicious; this is high-security code.
To be safer, we can get a known-good (IMO) version like this:
3 Plugins
We can hack fish. There are also popular plugin managers. As far as I can tell, a passable default is oh my fish
Is fisher around too?
It also handles omf plugins, apparently. [TODO clarify]
The omf manual is brusque. See a helpful blog post. I’m currently running omf, but since it intrusively changed my prompt I’m grumpy about it. I switched to a better prompt, spacefish.
We do need the wacky powerline fonts.
There are various useful plugins that aren’t purely cosmetic. For example, fzf adds fuzzy history search. z does recency/frequency-based directory navigation.
4 Homebrew compatibility on Ubuntu
I use fish as my default shell, but Ubuntu automatically executes the bash startup script .profile on login. When I used homebrew, I ran into the following errors because it tried to run the fish init in a bash process:
This may be related to an intermittently reported bug in homebrew. The fix that worked for me was to change the automatically added line in .profile to:
and to add [TODO clarify]
to ~/.config/fish/config.fish. [TODO clarify]
5 Python environments
5.1 virtualenv
If I used virtualenv on Python I would need virtualfish to replace Python’s virtualenvwrapper.sh. Or I could switch to native Python 3 venv, which is more or less the same thing but works better and doesn’t support Python 2. But if we need to support Python 2 at this stage it’s probably because we’re in some weird enterprise environment with horrid legacy software, so hopefully we can farm this problem out to the tech support team. Either that or we’re barred from using fish by policy and this isn’t a problem.
5.2 Using Anaconda Python
We need to do some extra setup to use conda with fish.
Or, for older versions,
into ~/.config/fish/config.fish.
(Replace ~/miniconda3/ with the output of conda info --root if you used a non-standard install location)
6 Vars, expansions, extensions, suffixes
Wildcards are minimal: just *, **, brace expansion and mv a.{txt,html.
For more sophisticated string processing, we define custom functions (which I never actually do) or use classic subcommands, which I do all the time. Usually, I want to rename files:
or expansion via string, which is harder to remember.
Or use another fancy utility like rename.
7 For loops
8 Test exit status
To simply execute the second command if the first succeeds the command we want is and, which is hard to google for:
9 Temporary variable setting: uses env
10 VS Code
VS Code requires launching it from the command line if we use fish on macOS.
11 Aliases, custom commands
12 Writing functions
13 Gotchas
“Sophisticated” backgrounding, i.e. anything other than single shell commands, isn’t supported in fish. Background jobs aren’t in the background. Specifically, functions and piped commands don’t work.
14 Python environments
In lazy mode, python environments manipulates paths and environment variables. We need extra configuration in the fish shell. pyenv needs
See also How to use nvm, rbenv, pyenv, goenv… with the fish shell.
Or ditch pyenv and use uv.
15 Incoming
Deleting history interactively:
NB: it only allocates five jobs at a time. Nifty.
