Fish shell

A command line shell that does not think that the problem is you

March 30, 2019 — June 2, 2023

computers are awful
faster pussycat
plain text

Not the aquatic creatures, but rather the command-line doohickey, which is not as shit as the other ones. I’m gradually transitioning to fish, after accidentally losing a lot of precious data due to a quirk in bash syntax. Long boring story. It’s time for new, exciting, different stupid errors.

Figure 1

fish has a strong fanbase and an opinionated design. If you dislike those design opinions, at least you might appreciate it has a healthy degree of sarcasm with which said opinions are expressed, which sarcasm is sorely absent from the drearily earnest nerdview of your typical project. You might also hold that having any kind of principled opinion is better than the design-by-accumulation-of-tradition-cruft which structures command-line shells.

1 Installing

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:

2 Configuration

2.1 Modifying the PATH

This is the most confusing thing in fish for me. It is worth reading tutorials, e.g.


To add a path, use the utility command fish_add_path

fish_add_path /usr/local/bin

To remove a path

set PATH (string match -v /usr/local/bin $PATH)

Adding a path? Say it’s /usr/local/bin. Put

set -gx PATH /usr/local/bin $PATH

in ~/.config/fish/, OR

set -U fish_user_paths /usr/local/bin $fish_user_paths

Removing a path?

set -gx PATH (string match -v /usr/local/bin $PATH)

🏗 explain the difference between $PATH and $fish_user_path which will depend upon me understanding how the content of $PATH magically replenishes itself and the difference between “universal” and ”global” variables.

2.2 Modifying any settings with GUI


2.3 Traditional config

Put commands in ~/.config/fish/

2.4 Extremely traditional config

Aelius notes a hack to unify 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 .profile a 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/dash as my login shell. the first line of my ~/.profile is ENV=$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.

2.5 ssh-agent

Optiligence notes that this minor alteration should work.

eval (ssh-agent -c)

Alternatively, see danhper/fish-ssh-agent or ivakyb/fish_ssh_agent.


wget -P ~/.config/fish/functions/

Append next line to ~/.config/fish/


You really need to verify that is not anything malicious; this is high security code.

To be more secure, we can get a known-good (IMO) version thusly:

wget -P ~/.config/fish/functions/

3 Plugins

You can hack fish. Popular plugin management systems exist also. AFAICT a passable default is oh my fish

curl -L | fish

fisher seems to be around too?

curl -sL | source && fisher install jorgebucaran/fisher

It also handles omf plugins, apparently.

The omf manual is brusque. See a helpful blogpost. I am currently running omf, but since it intrusively changed my prompt I am grumpy at it. However, I got a better prompt, spacefish.

omf install spacefish

It does need the wacky powerline fonts.

sudo apt-get install fonts-powerline

There are various useful plugins that are not purely cosmetic; For example fzf adds fuzzy history search. z does recency/frequency-based directory navigation.

4 homebrew compatibility on Ubuntu

Since I use fish shell as my default but ubuntu automatically executes the bash startup script .profile on login, I ran into the following errors when it tried to run the fish init in a bash process when I used homebrew:

bash: set: -g: invalid option
set: usage: set [-abefhkmnptuvxBCHP] [-o option-name] [--] [arg ...]
bash: set: -g: invalid option

This is maybe related to an intermittently reported bug in homebrew. The fix that worked for me was to change the automatically-added line in .profile to be

eval $(SHELL=bash /bin/brew shellenv)

and to add

eval (env SHELL=fish /bin/brew shellenv)

to ~/.config/fish/

5 Python environments

5.1 virtualenv

If I used virtualenv on python I would need virtualfish to replace python’s Or switch to native python3 venv, which is more or less the same thing but works better and doesn’t support python 2. But if you need to support python 2 at this stage it’s because you are in some weird enterprise environment with horrid legacy software, so hopefully you can farm this problem out to the tech support team? Either that or you are barred from using fish by policy and this is not a problem.

5.2 Using anaconda python

You need to do some extra setup to use conda with fish.

~/miniconda3/bin/conda init fish

Or, for older versions,

source ~/miniconda3/etc/fish/conf.d/

into ~/.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 *, **, and brace expansion, mv a.{txt,html.

For more sophisticated string processing, one defines custom functions (which I never actually do) or use classic subcommands which I do all the time. Usually I want to rename files:

for file in (ls *.html)
  mv $file (basename $file .html).txt

or expansion via string, which is harder to remember

for file in (ls *.html)
  mv $file (string replace -r "\.html\$" .txt $file)

Or use another fancy utility like rename.

7 For loops

while true
  echo "Loop forever"

8 Test exit status

Similar to bash.

if test $status -eq 0
  echo yeah

To simply execute the second command if the first succeeded the command you want is and, which is hard to google for:

../bin/ foo; and cp foo ~/Dropbox/

9 Temporary variable setting uses env


10 VS code

VS code requires you launch it from the command line if you use fish on macos.

11 Aliases, custom commands

Command, alias, or abbr?

  • Command allows overwriting a command.
  • Alias makes an alias, but is lacking autocomplete.
  • Abbr makes a sort of alias that will expand out and support autocomplete.

12 Writing functions

13 Incoming

How to use nvm, rbenv, pyenv, goenv… with the fish shell

Deleting history interactively:

history delete

Native parallelism?:

while true
    if test (count (jobs)) -lt 5
        dostuff &
        wait # to wait for _any_ job

NB that only allocates 5 jobs at a time. Nifty.