The Fish Shell: A Complete Guide

By

Learn the Fish shell from installation and interactive features to configuration, variables, functions, scripts, plugins, and Bash differences.

~~~

Fish is a command-line shell designed to be friendly from the moment you install it.

You get syntax highlighting, useful suggestions, rich tab completions, and a good prompt without spending an afternoon editing configuration files.

That is what makes Fish special.

Bash and Zsh can also provide an excellent command-line experience. But Fish makes more choices for you, and its defaults are genuinely useful.

I think it is one of the best shells for someone who wants a pleasant interactive terminal without maintaining a large configuration.

There is one important detail to know from the start: Fish is not a POSIX shell. Its scripting language is different from Bash and sh.

This is not usually a problem. You can use Fish interactively and still run Bash scripts. You just need to know which language a command or script was written for.

In this guide, you will learn how to:

  • install and try Fish safely
  • use its interactive features
  • configure its prompt and colors
  • manage variables and your PATH
  • write conditions, loops, functions, and scripts
  • create abbreviations and completions
  • install plugins with Fisher
  • run Bash scripts and understand the important syntax differences
  • troubleshoot a broken configuration

What is a shell?

A shell is a program that reads commands and asks the operating system to run them.

When you type:

ls -la

the shell reads the command name, ls, and passes -la as an argument.

The shell also gives you features around the commands you run:

  • command history
  • variables
  • pipes and redirections
  • wildcards
  • aliases and functions
  • scripting
  • job control

The terminal and the shell are not the same thing.

The terminal is the application that displays text and receives your keystrokes. Terminal.app, iTerm2, Ghostty, Windows Terminal, and the terminal inside VS Code are terminal applications.

Fish, Bash, and Zsh are shells that run inside a terminal.

If this distinction is new to you, read my guide to shells and my macOS terminal guide.

Why use Fish?

Fish calls itself the friendly interactive shell. The name is accurate.

Its main strength is not one spectacular feature. It is the way many small features work together without much configuration.

Right after installation you get:

  • syntax highlighting while you type
  • suggestions based on history and completions
  • tab completion for commands, options, paths, and Git branches
  • descriptions for many command options
  • searchable command history
  • a configurable prompt
  • a web-based configuration tool
  • a consistent scripting language

The shell feels helpful before you install a theme or a plugin manager.

This matters because shell configuration can become a hobby of its own. It is easy to collect plugins, copy a large dotfiles repository, and end up with a shell you do not understand.

Fish gives you a strong starting point. You can keep it almost unchanged and still have a great command-line environment.

Fish, Bash, and Zsh solve slightly different problems

Bash is installed on a huge number of systems. It is a common choice for scripts because it is widely available and many developers already know it. You can learn it in my Bash guide and Bash scripting guide.

Zsh is highly configurable and is the default login shell on modern macOS releases. With the right configuration, it can provide many features that feel similar to Fish.

Fish focuses on a friendly interactive experience and a cleaner scripting language. It deliberately does not follow the POSIX shell language.

This leads to a practical setup that works very well:

  • use Fish for the commands you type every day
  • use Bash or POSIX sh when a script must run on many machines
  • use Fish scripts for personal tools that only need to run where Fish is installed

You do not have to pick one shell for every job.

Install Fish

The Fish homepage lists the current installation options for macOS, Linux, BSD, and Windows environments.

Install Fish on macOS

With Homebrew, run:

brew install fish

Homebrew usually installs Fish at /opt/homebrew/bin/fish on Apple silicon Macs and /usr/local/bin/fish on Intel Macs.

Do not depend on a guessed path. Ask your current shell where Fish was installed:

command -v fish

Install Fish on Linux

Fish is available through the package managers used by the main Linux distributions.

On Fedora:

sudo dnf install fish

On Arch Linux:

sudo pacman -S fish

Ubuntu and Debian also provide Fish packages. Distribution repositories can contain an older release, so check the installation instructions linked from the Fish homepage if you need the latest version.

Install Fish on Windows

The simplest option is to use Fish inside the Windows Subsystem for Linux, then install it with the package manager for your Linux distribution.

Fish is also available for Cygwin and MSYS2. It is primarily a Unix-style shell, so WSL is usually the most familiar environment for following Unix command-line tutorials.

Try Fish without changing your default shell

You do not need to commit to Fish immediately.

Start it from your current shell:

fish

Check the installed version:

fish --version

Now use the terminal normally. Try cd, ls, git, or any other command you already use.

When you want to return to the previous shell, run:

exit

This is the best way to evaluate Fish. Use it for a few normal work sessions before making it your default.

Syntax highlighting catches small mistakes

Fish highlights a command as you type it.

Valid commands, quoted strings, options, paths, and errors get different colors. If you mistype a command name, Fish can show it as invalid before you press Enter.

This does not replace understanding the command. It gives you an early signal that something is wrong.

It is especially helpful with long paths, nested quotes, and commands copied from notes.

Autosuggestions learn from your work

Start typing a command you used before. Fish displays a suggestion after the cursor in a muted color.

Press the right arrow to accept the complete suggestion.

Press Alt and the right arrow to accept one word at a time. This is useful when the beginning is correct but you want to change the final argument.

Suggestions can come from:

  • your command history
  • known command completions
  • valid file and directory paths

Imagine that you previously ran:

git commit -m "Fix checkout validation"

The next time you type git com, Fish can offer the rest of the command. You can accept it and edit only the message.

Suggestions are not commands being executed. Fish only inserts the text after you accept it, so you still have a chance to review it.

Search history by typing the beginning

Fish makes command history feel natural.

Type part of a command, then press the up arrow. Fish moves through previous commands that contain what you typed.

For example, type:

docker

Then press the up arrow to move through your earlier Docker commands instead of every command in your history.

Press Ctrl-R when you want to search history more broadly.

Fish stores history between sessions. If another open session has newer commands you want to import immediately, run history merge.

Remember that shell history can contain sensitive arguments. Avoid putting passwords, tokens, and other secrets directly on the command line.

Tab completion explains your choices

Press Tab after part of a command, option, or path. Fish either completes it or displays the available choices.

Here I typed c and pressed Tab:

Fish completing command names

Fish ships with completions for many popular commands. Those completions can understand more than filenames.

For example, Git completions can suggest branches and tags. Package-manager completions can suggest subcommands. Other completions can show the description of each option.

Type:

ls -

Then press Tab:

Fish showing options and their descriptions

Many of these descriptions come from manual pages. If a command you installed has a useful man page, Fish can generate completions from it with:

fish_update_completions

You can always open the full manual yourself:

man ls

For Fish’s own commands, use help:

help set

This opens the relevant Fish documentation in a browser. Use man set if you prefer to stay in the terminal.

Configure Fish in the browser

Run:

fish_config

Fish starts a local web interface and opens it in your browser.

The Fish web configuration interface

From this interface you can:

  • choose a color scheme
  • preview and select a prompt
  • inspect functions
  • inspect variables
  • browse command history
  • see keyboard bindings

This is an optional configuration tool. Fish does not need a web server running during normal use. The command starts the local interface when you need it, and you can stop it when you are done.

You can also work with prompts directly from the terminal:

fish_config prompt show

To try one of the displayed prompts in the current session, run:

fish_config prompt choose disco

Replace disco with the name shown by the previous command. Save the current prompt permanently with:

fish_config prompt save

Where Fish stores its configuration

Your Fish configuration lives in:

~/.config/fish/

The main startup file is:

~/.config/fish/config.fish

Fish runs this file when a new shell starts.

You can also split configuration into small files inside:

~/.config/fish/conf.d/

Fish reads the .fish files in that directory during startup. This is useful when you want separate files for a language runtime, a work environment, or a tool.

Fish also uses these directories:

~/.config/fish/functions/
~/.config/fish/completions/

The first contains autoloaded functions. The second contains your custom completions.

A small starter configuration

Create ~/.config/fish/config.fish if it does not exist, then start with something small:

set -gx EDITOR code
set -gx VISUAL code

fish_add_path $HOME/.local/bin

if status is-interactive
    set -g fish_greeting

    abbr --add gs 'git status'
    abbr --add gc 'git commit'
    abbr --add gp 'git push'
end

This configuration does four things:

  1. It tells command-line programs to use VS Code as the editor.
  2. It adds ~/.local/bin to PATH if that directory exists.
  3. It hides the greeting printed when Fish starts.
  4. It creates three interactive Git abbreviations.

The status is-interactive condition prevents interactive-only setup from running when Fish executes a script.

After changing the file, either start a new Fish session or reload it:

source ~/.config/fish/config.fish

Keep this file understandable. Add a setting only when it solves a real problem for you.

Variables use the set command

Fish does not use the familiar shell assignment syntax:

name=Flavio

It uses the set command:

set name Flavio
echo $name

Remove a variable with --erase, or its short form -e:

set --erase name

Fish variables can have several values. In other words, lists are a normal part of the language:

set editors code vim nano

echo $editors[1]
echo $editors[-1]

Fish list indexes start at 1. A negative index counts from the end, so -1 selects the last item.

Loop through the list like this:

for editor in $editors
    echo $editor
end

Unlike traditional shells, Fish does not split an unquoted variable on spaces. If one list item contains a space, it remains one item.

You still need quotes when you want literal text containing spaces:

set project_name "My Fish Project"
mkdir "$project_name"

Variable scopes

Fish has explicit options for variable scope:

  • -l or --local creates a local variable
  • -g or --global creates a variable for the current Fish process
  • -U or --universal stores a variable across Fish sessions
  • -x or --export makes the variable available to child processes

You can combine options.

This exports EDITOR globally in the current Fish process:

set -gx EDITOR code

This stores an exported variable across current and future Fish sessions:

set -Ux MY_API_URL https://api.example.com

Universal variables are convenient, but do not use them for secrets. They are persistent configuration, not a password manager.

Inspect a variable and its scope with:

set --show EDITOR

Add directories to PATH

PATH is the list of directories where the shell looks for executable commands.

Fish represents it as a real list. You can inspect one directory per line with:

string join \n $PATH

The easiest way to add a directory is fish_add_path:

fish_add_path $HOME/.local/bin

By default, Fish avoids adding the same directory more than once. It also ignores a path that is not an existing directory.

It is safe to put this command in config.fish:

fish_add_path $HOME/.cargo/bin
fish_add_path $HOME/go/bin

This is clearer than rebuilding PATH by hand.

Command substitutions

A command substitution runs a command and inserts its output into another command.

Fish 4 supports the familiar $() form:

set today $(date +%F)
echo "Today is $today"

Fish also supports its traditional parentheses syntax:

set today (date +%F)

You will see both forms in Fish code.

A command substitution can produce a list:

set markdown_files (find . -name '*.md')

for file in $markdown_files
    echo $file
end

Fish splits command-substitution output on newlines, not on every space. This avoids many surprising word-splitting problems found in traditional shell code.

Pipes and redirections

The common Unix forms work as you expect.

Send the output of one command into another:

find . -name '*.md' | sort

Write output to a file:

echo "Hello" > message.txt

Append instead of replacing the file:

echo "Another line" >> message.txt

Send error output to a file:

npm run build 2> build-errors.txt

Fish also supports pipelines made from several small commands:

history | string match '*git commit*' | head

Conditions

Fish conditions end with end, not fi.

if test -e package.json
    echo "This looks like a Node.js project"
else if test -e Cargo.toml
    echo "This looks like a Rust project"
else
    echo "I do not recognize this project"
end

The test command checks files, strings, and numbers.

Some common file checks are:

test -e path   # the path exists
test -f path   # it is a regular file
test -d path   # it is a directory

Negate a condition with not:

if not test -d build
    mkdir build
end

Fish supports and and or for short command chains:

mkdir -p build; and echo "Build directory is ready"
npm test; or echo "The tests failed"

Modern Fish also accepts && and ||, but and and or are common in Fish code and make the intent easy to read.

Loops

Use for to loop over a list:

for file in *.md
    echo "Checking $file"
end

Use while when a block should continue as long as a condition is true:

set count 1

while test $count -le 3
    echo $count
    set count (math $count + 1)
end

Fish uses the math command for arithmetic:

set total (math '12 * 4')
echo $total

Functions

A Fish function groups commands under a name.

Here is a function that creates a directory and enters it:

function mkcd --description 'Create a directory and enter it'
    if test (count $argv) -ne 1
        echo "Usage: mkcd <directory>"
        return 1
    end

    mkdir -p $argv[1]; or return
    cd $argv[1]
end

Function arguments are stored in the $argv list. The first argument is $argv[1].

After defining the function interactively, test it:

mkcd demo

The function only exists in the current session at this point. Save it with:

funcsave mkcd

Fish writes it to:

~/.config/fish/functions/mkcd.fish

The next time you call mkcd, Fish automatically loads that file.

This one-function-per-file system keeps config.fish small and avoids loading every function at startup.

Abbreviations are better than hidden shortcuts

An abbreviation expands text while you type.

Create one like this:

abbr --add gs 'git status'

Now type gs and press Space or Enter. Fish changes the visible command line to:

git status

You can review or edit the expanded command before running it.

This is the main advantage over a traditional alias. An alias hides the real command behind another name. An abbreviation shows you the command it created.

Abbreviations only expand interactively. They do not change the meaning of words inside scripts.

Add abbreviation commands to config.fish or a file such as:

~/.config/fish/conf.d/abbreviations.fish

List the current abbreviations with:

abbr --show

Remove one with:

abbr --erase gs

Fish also has an alias helper. It creates a function behind the scenes:

alias ll 'ls -lah'

Use funcsave ll if you want that function to survive after the current session.

For simple interactive shortcuts, I prefer abbreviations because the expanded command stays visible.

Create custom tab completions

You can teach Fish how to complete your own commands.

Imagine a command named serve with --port and --open options:

complete -c serve -s p -l port -r -d 'Port to listen on'
complete -c serve -l open -d 'Open the browser'

The options mean:

  • -c serve selects the command
  • -s p adds the short option -p
  • -l port adds the long option --port
  • -r says that the option needs a value
  • -d adds the description displayed in the completion menu

Put permanent completions in a file named after the command:

~/.config/fish/completions/serve.fish

This small feature makes personal scripts feel like polished command-line tools.

Write a Fish script

A Fish script is a text file containing Fish commands.

Create backup.fish:

#!/usr/bin/env fish

if test (count $argv) -ne 1
    echo "Usage: backup.fish <directory>"
    exit 1
end

set target $argv[1]

if not test -d $target
    echo "Directory not found: $target"
    exit 1
end

set archive "backup-"(date +%Y%m%d-%H%M%S)".tar.gz"

tar -czf $archive $target; or exit 1
echo "Created $archive"

Make it executable:

chmod +x backup.fish

Run it:

./backup.fish ~/Documents

The first line is the shebang. It tells the operating system to find Fish and use it to interpret the file.

You can also run a script explicitly:

fish backup.fish ~/Documents

Check a script for syntax errors without running it:

fish --no-execute backup.fish

Format Fish code with:

fish_indent --write backup.fish

Fish scripts are a good choice for your personal commands when Fish is installed on every machine where they will run.

For a project installer, CI script, or server script that must work without Fish, choose a more widely available interpreter.

Fish is not a POSIX shell

POSIX defines standards shared by Unix-like systems, including a shell language.

The sh shell is the traditional interface for that language. Bash can run a large amount of POSIX shell code, although a Bash script can also use Bash-only features.

Fish intentionally uses a different language. Fish scripts only work in Fish.

Here are some common differences:

TaskBash or POSIX-style syntaxFish syntax
Set a variablename=Flavioset name Flavio
Export a variableexport EDITOR=codeset -gx EDITOR code
Create a listfiles=(one two) in Bashset files one two
Read the first list item${files[0]} in Bash$files[1]
Command substitution$(date +%F)$(date +%F) or (date +%F)
Arithmetic$((2 + 2))math '2 + 2'
End an iffiend
End a for loopdoneend
Function arguments$1, $2, and so on$argv[1], $argv[2], and so on

Do not paste a long Bash command into Fish and assume it will work. Simple external commands often do, but shell syntax around them might not.

For example, this Bash assignment is not valid Fish:

NODE_ENV=production npm start

The Fish equivalent is:

env NODE_ENV=production npm start

Or set and export the variable first:

set -lx NODE_ENV production
npm start

The -l keeps the variable local to the current scope, while -x exports it to npm.

Run Bash scripts from Fish

Fish can launch Bash like any other program.

A properly executable Bash script starts with a Bash shebang such as:

#!/usr/bin/env bash

You can run it from Fish normally:

./deploy.sh

The operating system reads the shebang and starts Bash for that script.

You can also be explicit:

bash deploy.sh

To run a short Bash command:

bash -c 'name=Flavio; echo "$name"'

Or open an interactive Bash session:

bash

Run exit to return to Fish.

What you cannot do is source a Bash configuration file into Fish:

source ~/.bashrc

That asks Fish to parse Bash syntax, which will fail as soon as the file uses language features Fish does not understand.

If a tool only provides Bash setup code, look for a Fish integration. If none exists, run the tool through Bash or translate the small piece of configuration into Fish.

Make Fish your default shell

Only do this after you have used Fish for a while and confirmed your development tools work correctly.

First find the real executable path:

command -v fish

Check whether that path is already listed in /etc/shells:

grep -Fx "$(command -v fish)" /etc/shells

If the command prints the Fish path, continue to chsh.

If it prints nothing, add the path:

command -v fish | sudo tee -a /etc/shells

Then change your login shell:

chsh -s "$(command -v fish)"

Log out and back in, or restart your terminal application, for the change to take effect.

Changing the default shell to Fish

There is a useful caveat. Some systems expect the login shell to be compatible with the Bourne shell and to read /etc/profile. On those systems, making Fish the login shell can produce an unexpected PATH or other environment differences.

An alternative is to leave your login shell unchanged and configure your terminal application to start Fish. That gives you Fish for interactive work without changing the shell used for every login, including SSH sessions.

To switch back later, replace fish with your previous shell in the same command:

chsh -s "$(command -v zsh)"

Install plugins with Fisher

Fish is already useful without plugins. Start with the built-in features and add plugins when you can name the problem they solve.

Fisher is a small, popular plugin manager for Fish.

Its current installation command is:

curl -sL https://raw.githubusercontent.com/jorgebucaran/fisher/main/functions/fisher.fish | source && fisher install jorgebucaran/fisher

This downloads and executes a remote script. Read the project and installation script first if you want to verify what it does before running it.

Install a plugin with:

fisher install jorgebucaran/nvm.fish

List installed plugins:

fisher list

Update them:

fisher update

Remove one:

fisher remove jorgebucaran/nvm.fish

Fisher records the plugin list in:

~/.config/fish/fish_plugins

Keep this file in your dotfiles if you want Fisher to restore the same plugin set on another computer.

Avoid installing a large bundle only because it is popular. Every plugin adds behavior you may need to understand later.

Keep your Fish configuration in dotfiles

If you use more than one computer, store the configuration you wrote in a Git repository.

Useful files and directories to track include:

~/.config/fish/config.fish
~/.config/fish/conf.d/
~/.config/fish/functions/
~/.config/fish/completions/
~/.config/fish/fish_plugins

Do not copy fish_variables blindly. Fish manages that file for universal variables, and it can contain machine-specific state.

Also do not commit secrets. Put API tokens and passwords in a password manager or your platform’s secret store.

The goal of dotfiles is to make your environment reproducible, not to preserve every experiment forever.

Troubleshoot Fish

When something behaves strangely, first find out whether the problem comes from Fish, your configuration, or an external command.

Start Fish without your configuration

Run:

fish --no-config

If the problem disappears, it is probably in config.fish, a file in conf.d, a function, or a plugin.

Find which command will run

Use type:

type -a node

This shows every matching function, builtin, or executable Fish can find.

Use command -v when you only need the executable lookup:

command -v node

Inspect your changes from the defaults

Run:

fish_delta

Fish shows functions and completions that differ from its defaults. This is useful when you no longer remember where a behavior came from.

Check and format scripts

Validate syntax:

fish --no-execute script.fish

Check formatting without changing the file:

fish_indent --check script.fish

Format it in place:

fish_indent --write script.fish

Disable one piece at a time

If fish --no-config fixes the issue, temporarily move through your configuration in this order:

  1. comment out plugin setup
  2. inspect files in conf.d
  3. inspect custom functions
  4. inspect custom completions
  5. reduce config.fish to the smallest failing example

A small configuration makes this process much easier.

How I would use Fish

I would use Fish as my everyday interactive shell and keep the configuration deliberately small.

I would choose a built-in prompt, set my editor, add the few directories I need to PATH, and create abbreviations for commands I type several times per day.

I would put larger helpers in named function files instead of turning config.fish into one long script. I would add a plugin only when the built-in shell cannot solve a specific problem.

For personal automation on my own computer, I would happily write a Fish script. The syntax is readable, lists behave well, and functions are easy to organize.

For repository scripts, CI jobs, server provisioning, or commands other people must run, I would first ask what interpreters are guaranteed to exist. Fish is a poor choice when the script must work on a clean system that may not have Fish installed.

This separation gives me the best part of Fish without pretending every shell has the same job.

Frequently asked questions

Is Fish a terminal?

No. Fish is a shell. It runs inside a terminal application such as Terminal.app, iTerm2, Ghostty, or the VS Code terminal.

Is Fish better than Zsh?

It depends on what you value. Fish provides more friendly behavior with little configuration. Zsh offers deep customization and can be made to behave in many different ways. Try both with their default configuration before choosing.

Can Fish run normal Unix commands?

Yes. Commands such as git, ls, cd, curl, npm, and docker work normally. The differences appear when the command line contains shell syntax such as assignments, conditions, loops, or expansions.

Can Fish run Bash scripts?

Yes. Run an executable Bash script normally, or pass it to bash. The script’s shebang chooses Bash. Fish does not need to understand the script’s contents.

Can I use Fish in VS Code?

Yes. If Fish is your default shell, a new integrated terminal may use it automatically. You can also select Fish as a terminal profile in VS Code without changing your system login shell.

Where is .fishrc?

Fish does not use .fishrc. Its main user configuration file is ~/.config/fish/config.fish.

Do I need Fisher or Oh My Fish?

No. Fish’s strongest interactive features are built in. Use a plugin manager only when you want a plugin that solves a specific need.

Should I make Fish my default shell?

Try it first. If your tools and login environment work correctly, making it the default can be convenient. If your system expects a Bourne-compatible login shell, configure the terminal to start Fish instead.

A good way to start

Install Fish, run fish, and use it without changing anything for a day.

Pay attention to three features:

  1. accept a useful autosuggestion with the right arrow
  2. press Tab after a command option
  3. type a command prefix and search its history with the up arrow

Then open fish_config, choose a prompt you like, and create one abbreviation for a command you use often.

That is enough.

Fish is at its best when it makes the terminal calmer and faster, not when it becomes another configuration project.

Tagged: CLI · All topics

Want me to talk about your product? You can sponsor this site.

~~~

Related posts about cli: