Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Fish (Friendly Interactive Shell)

Introduction

Fish is a smart, user-friendly command-line shell designed to be discoverable and readable out of the box. Unlike Bash and Zsh, Fish is not POSIX-compatible — it deliberately breaks from POSIX syntax to provide a cleaner, more consistent language. Fish provides syntax highlighting, autosuggestions, and tab completion without any plugins or configuration.

Key Philosophy

Fish prioritizes:

  1. Discoverability: Features work without reading manuals
  2. Consistency: Fewer special cases and gotchas
  3. User-friendliness: Sensible defaults, rich color
  4. Scripting clarity: Cleaner syntax than POSIX sh

Syntax Highlighting (Built-in)

Fish highlights commands as you type:

# Valid commands appear in the default color
ls /tmp

# Unknown commands appear in red
nonexistent_command

# Strings are quoted with color
echo "hello world"

# Errors (unclosed quotes, invalid syntax) are highlighted
echo "unterminated

# Redirections are highlighted differently
echo test > /dev/null

# Command-specific coloring
# Options: specific color
# File paths: colored by type

Configuration

# Disable specific highlighting
set -g fish_color_command normal
set -g fish_color_error red --bold
set -g fish_color_param cyan
set -g fish_color_quote yellow
set -g fish_color_redirection magenta
set -g fish_color_comment brblack

# View all color settings
set -n | grep fish_color

Autosuggestions (Built-in)

Fish shows autosuggestions from history as gray text after the cursor:

# Type partial command
doc<TAB>
# Suggests: docker-compose up -d  (from history)

# Accept suggestion: → (right arrow) or End
# Accept one word: Alt+→ or Ctrl+→
# Accept and execute: Alt+Enter

# Autosuggestion sources (in order):
# 1. History matches
# 2. Completions
# 3. Known directories

Abbreviations

Abbreviations are like aliases but expand inline, so you can see the full command:

# Define abbreviation
abbr -a gs git status
abbr -a gc git commit
abbr -a gp git push
abbr -a gl git log --oneline --graph

# Use
gs<TAB>
# Expands to: git status

# Abbreviations with positional arguments
abbr -a gcmsg git commit -m

# With command-specific expansion
abbr -a --position command gc git commit

# List abbreviations
abbr -l

# Remove abbreviation
abbr -e gs

Abbreviations vs Aliases

FeatureAbbreviationAlias
ExpansionInline (visible)Hidden
HistoryExpanded form storedAlias name stored
EditingCan modify before executingRuns immediately
Tab completionWorks after expansionMay need setup

Universal Variables

Fish’s universal variables persist across all sessions automatically:

# Universal variable (persists across sessions, written to disk)
set -U EDITOR vim
set -U fish_greeting "Welcome to Fish!"

# Global variable (current session only)
set -g my_var "hello"

# Local variable (current scope only)
set -l temp "temp"

# Function variable (current function)
set -f func_var "in function"

# Export to child processes
set -gx LANG en_US.UTF-8
set -gx PATH $PATH /usr/local/bin

# View all universal variables
set -U

# View where variables are stored
cat ~/.config/fish/fish_variables

Variable Scoping

graph TD
    subgraph "Variable Scopes"
        U[Universal -U: All sessions, on disk]
        G[Global -g: Current session]
        L[Local -l: Current scope/block]
        F[Function -f: Current function]
    end
    U --> G --> L --> F

Tab Completion

Fish has rich built-in completion:

# Command completion
git <TAB>
# add        - Add file contents to the index
# bisect     - Find the change that introduced a bug
# branch     - List, create, or delete branches
# checkout   - Switch branches or restore files
# ...

# Option completion
git commit --<TAB>
# --all             - Tell the command to automatically stage files
# --amend           - Amend the tip of the branch
# --author          - Override the commit author
# --message         - Use the given message as the commit message
# ...

# File completion with type indicators
ls <TAB>
# Documents/    (directory, with trailing /)
# file.txt      (file)
# script.sh     (executable, green)

# Process completion
kill <TAB>
# Shows running processes with PIDs

# Variable completion
echo $fish_<TAB>
# $fish_color_command  $fish_pid  $fish_version  ...

Writing Custom Completions

# ~/.config/fish/completions/mycommand.fish

# Simple completion
complete -c mycommand -a "start stop restart status" -d "Service action"

# File completion
complete -c mycommand -f  # Disable file completion
complete -c mycommand -a "(__fish_complete_suffix .txt)" -d "Text files"

# Conditional completion
complete -c mycommand -n "__fish_seen_subcommand_from start" \
    -a "option1 option2" -d "Start options"

complete -c mycommand -n "__fish_seen_subcommand_from stop" \
    -a "--force --graceful" -d "Stop options"

# Dynamic completion from command output
complete -c mycommand -a "(mycommand --list-choices)"

# Exclusive subcommands (no file completion)
complete -c git -n "__fish_use_subcommand" -a commit -d "Record changes"
complete -c git -n "__fish_seen_subcommand_from commit" \
    -s m -d "Commit message" -r

Fish Functions

# Define a function
function greet
    echo "Hello, $argv[1]!"
end

# With description
function greet -d "Greet someone by name"
    echo "Hello, $argv[1]!"
end

# With arguments handling
function mkcd
    mkdir -p $argv[1]
    cd $argv[1]
end

# With event handlers
function on_exit --on-event fish_exit
    echo "Goodbye!"
end

function on_pwd --on-variable PWD
    echo "Directory changed to: $PWD"
end

# Wrapping existing commands
function ls --wraps ls --description "List with colors"
    command ls --color=auto $argv
end

# Function with options parsing
function myutil
    argparse 'h/help' 'v/verbose' 'n/name=' -- $argv
    or return

    if set -q _flag_help
        echo "Usage: myutil [-h] [-v] [-n NAME]"
        return
    end

    if set -q _flag_verbose
        echo "Verbose mode"
    end

    if set -q _flag_name
        echo "Name: $_flag_name"
    end

    echo "Args: $argv"
end

Web Configuration

Fish includes a web-based configuration UI:

# Open web config
fish_config

# Or specific tabs
fish_config prompt   # Prompt configuration
fish_config colors   # Color scheme configuration

# This opens a browser at http://localhost:8000

Available Prompts

# List available prompts
fish_config prompt show

# Set a prompt theme
fish_config prompt choose <name>

# Popular prompts:
# informative_vcs  - Git info, command duration
# classic          - Traditional prompt
# classic_vcs      - Classic with git info
# terlar           - Compact, informative
# astronaut        - Unicode decorations

Color Schemes

# Browse color schemes
fish_config colors show

# Apply a scheme
fish_config colors choose <name>

# Popular schemes:
# default    - Fish default colors
# solarized  - Solarized dark/light
# dracula    - Dracula theme
# nord       - Nord theme
# zenburn    - Zenburn theme

Fish Scripting

Conditionals

if test -f /etc/passwd
    echo "File exists"
else if test -d /etc
    echo "Directory exists"
else
    echo "Neither"
end

# String comparison
if test "$var" = "hello"
    echo "Match"
end

# Numeric comparison
if test $count -gt 10
    echo "More than 10"
end

# Modern test syntax
if string match -q "*.txt" $filename
    echo "Text file"
end

Loops

# For loop
for file in *.txt
    echo "Processing: $file"
end

# C-style for loop
for i in (seq 1 10)
    echo $i
end

# While loop
while read -la line
    echo "Line: $line"
end < input.txt

# Piping into while
cat file.txt | while read -la line
    echo $line
end

Error Handling

# Check exit status
if command_that_might_fail
    echo "Success"
else
    echo "Failed"
end

# or: run second command if first fails
command || fallback_command

# and: run second command if first succeeds
command && next_command

# begin/end blocks
begin
    command1
    command2
end; or echo "Something failed"

String Manipulation

# String operations (no need for external commands)
set str "Hello, World!"

echo (string length $str)          # 13
echo (string sub -s 1 -l 5 $str)  # Hello
echo (string upper $str)           # HELLO, WORLD!
echo (string lower $str)           # hello, world!
echo (string replace "World" "Fish" $str)  # Hello, Fish!
echo (string match "*.txt" file.txt)       # file.txt
echo (string split "," $str)       # "Hello" " World!"

# Trim
echo (string trim "  hello  ")    # hello

# Repeat
echo (string repeat -n 5 "*")     # *****

Fish vs Bash Scripting

FeatureFishBash
Variable assignmentset var valuevar=value
Variable access$var$var or ${var}
Environment exportset -gx VAR valexport VAR=val
Conditionalsif test ...if [ ... ] or if [[ ... ]]
Loopsfor x in ...for x in ...
Command substitution(command)$(command) or `command`
Arraysset arr a b carr=(a b c)
String quotingSingle = literal, Double = expandSame
Heredocsread in pipe<<EOF
Arithmeticmath command$((expr))

Migration Tips

# Bash
export PATH="$PATH:/usr/local/bin"
for i in {1..10}; do echo $i; done
result=$(grep -r "pattern" /path)
array=("one" "two" "three")
echo ${array[1]}

# Fish equivalent
set -gx PATH $PATH /usr/local/bin
for i in (seq 1 10); echo $i; end
set result (grep -r "pattern" /path)
set array one two three
echo $array[1]  # Fish arrays are 1-indexed

Useful Built-in Commands

# math - Calculator
math "3 + 4 * 2"        # 11
math "sqrt(2)"          # 1.414214
math "2 ^ 10"           # 1024

# string - String manipulation
string match "*.txt" file.txt  # file.txt
string split ":" $PATH         # Split PATH

# status - Shell status
status is-login         # True if login shell
status is-interactive   # True if interactive
status current-command  # Current command name

# fish_config - Configuration
fish_config theme       # List themes
fish_config prompt      # List prompts

# type - Show command type
type ls                 # ls is /usr/bin/ls
type cd                 # cd is a builtin

# help - Open documentation
help                    # Open docs in browser
help command            # Open command docs

Installation and Setup

# Install on Ubuntu/Debian
sudo apt-add-repository ppa:fish-shell/release-3
sudo apt update
sudo apt install fish

# Install on macOS
brew install fish

# Install on Fedora
sudo dnf install fish

# Set as default shell
echo /usr/bin/fish | sudo tee -a /etc/shells
chsh -s /usr/bin/fish

Configuration Directory

~/.config/fish/
├── config.fish          # Main configuration (like .bashrc)
├── fish_variables       # Universal variables (auto-managed)
├── functions/           # Autoloaded functions
│   ├── fish_prompt.fish
│   ├── fish_greeting.fish
│   └── my_function.fish
├── completions/         # Autoloaded completions
│   └── mycommand.fish
└── conf.d/              # Configuration snippets
    └── my_config.fish

Plugin Ecosystem

Fisher

Fisher is the most popular Fish plugin manager — it has zero dependencies and installs plugins directly into ~/.config/fish/:

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

# Install a plugin
fisher install jorgebucaran/autopair.fish    # Auto-close brackets/quotes
fisher install PatrickF1/fzf.fish            # fzf integration
fisher install jethrokuan/z                  # Directory jumping (like z.sh)
fisher install ilancosman/tide@v6            # Powerline-style prompt
fisher install jorgebucaran/nvm.fish         # Node.js version manager
fisher install franciscolourenco/done        # Notification when long commands finish

# List installed plugins
fisher list

# Update all plugins
fisher update

# Remove a plugin
fisher remove jorgebucaran/autopair.fish

# Install from a gist
fisher install https://gist.github.com/user/id

# Fisher installs files directly into:
# ~/.config/fish/functions/   (functions)
# ~/.config/fish/completions/ (completions)
# ~/.config/conf.d/           (conf.d snippets)

Oh My Fish (OMF)

Oh My Fish is an older plugin framework with a theme system:

# Install Oh My Fish
curl https://raw.githubusercontent.com/oh-my-fish/oh-my-fish/master/bin/install | fish

# Install a theme
omf install agnoster

# List available themes
omf list

# Install a plugin
omf install bass          # Bash utility support (source bash env)
omf install foreign-env   # Import env from other shells

# Change theme
omf theme bobthefish

# Update everything
omf update

# Remove Oh My Fish
omf destroy
PluginDescription
autopair.fishAuto-close (), [], {}, "", ''
fzf.fishfzf integration for history, files, variables
z or zoxideFast directory jumping by frecency
tidePowerline prompt (like Powerlevel10k for Zsh)
bassSource Bash/Zsh scripts in Fish
doneNotify when long-running commands complete
grcGeneric colouriser for common commands
nvm.fishNode Version Manager for Fish
sdkman-for-fishSDKMAN integration for Java ecosystem
piscesAuto-close pairs (lighter alternative to autopair)
spongeClean failed commands from history
puffer-fishTyping shorthand expansions (...../..)

fzf Integration

fzf (fuzzy finder) integrates deeply with Fish:

# Install the fzf.fish plugin
fisher install PatrickF1/fzf.fish

# fzf.fish provides these key bindings (default):
# Ctrl+R  — Search command history
# Ctrl+T  — Search files in current directory
# Ctrl+V  — Search environment variables
# Alt+C   — Search subdirectories and cd into selection
# Alt+L   — Search git log

# Manual fzf integration (without plugin)
# History search
function fzf_history
    history | fzf --no-sort | read -l command
    and commandline -rb $command
end
bind \cr fzf_history

# File search
function fzf_files
    set -l file (find . -type f 2>/dev/null | fzf)
    and commandline -i $file
end
bind \ct fzf_files

# Git branch checkout
function fzf_git_branch
    set -l branch (git branch -a | fzf --height 40% | string trim)
    and git checkout $branch
end

Key Bindings

Fish has configurable key bindings for both Emacs (default) and Vi mode:

# View current bindings
bind --all

# Emacs mode (default) key bindings:
# Ctrl+A  — Beginning of line
# Ctrl+E  — End of line
# Ctrl+W  — Delete word backward
# Ctrl+U  — Delete to beginning of line
# Ctrl+K  — Delete to end of line
# Ctrl+Y  — Paste (yank) from killring
# Alt+D   — Delete word forward
# Alt+L   — Lowercase word
# Alt+U   — Uppercase word

# Vi mode
fish_vi_key_bindings

# Switch back to Emacs mode
fish_default_key_bindings

# Custom key binding
bind \cg 'git status'           # Ctrl+G runs git status
bind \el 'ls -la'               # Alt+L runs ls -la

# Bind in different modes
bind -M insert \ce 'edit_config'  # In insert mode
bind -M default \ce 'edit_config'  # In normal mode

# Custom function bound to a key
function __fish_toggle_sudo
    set -l cmd (commandline)
    if string match -q 'sudo *' $cmd
        commandline (string replace 'sudo ' '' $cmd)
    else
        commandline "sudo $cmd"
    end
end
bind \es __fish_toggle_sudo     # Alt+S toggles sudo prefix

Vi Mode

# Enable Vi mode permanently
set -U fish_key_bindings fish_vi_key_bindings

# Vi mode indicator in prompt
function fish_mode_prompt
    switch $fish_bind_mode
        case default
            set_color --bold red
            echo '[N] '
        case insert
            set_color --bold green
            echo '[I] '
        case replace_one
            set_color --bold cyan
            echo '[R] '
        case visual
            set_color --bold magenta
            echo '[V] '
    end
    set_color normal
end

Performance and Startup Time

Fish is notably fast compared to Zsh with many plugins:

# Measure Fish startup time
time fish -i -c exit

# Typical times:
# Fish (no plugins):    ~30-50ms
# Fish (10 plugins):    ~50-100ms
# Zsh (Oh-My-Zsh):     ~200-500ms
# Bash:                 ~20-40ms

# Profile startup
fish --profile /tmp/fish_prof -i -c exit
sort -nk 2 /tmp/fish_prof | tail -20

# Check what autoloads on startup
fish -c 'functions --details fish_prompt'
# /home/user/.config/fish/functions/fish_prompt.fish

Why Fish Is Fast

  1. No plugin loading at startup: Fish uses autoloading — functions in ~/.config/fish/functions/ are loaded on first use, not at startup.
  2. No init file scanning: Unlike Zsh (which sources .zshrc), Fish reads config.fish once, and universal variables are cached.
  3. Built-in completions: Completion scripts are cached and autoloaded per-command.
  4. Efficient C++ core: The core shell is written in C++ with minimal startup overhead.

Fish with Modern Tools

Starship Prompt

# Install Starship
curl -sS https://starship.rs/install.sh | sh

# Add to config.fish
# ~/.config/fish/config.fish
starship init fish | source

# Starship config: ~/.config/starship.toml
# Shows: git status, node/python/rust versions, cmd duration, etc.

zoxide (Smarter cd)

# Install zoxide
curl -sSfL https://raw.githubusercontent.com/ajeetdsouza/zoxide/main/install.sh | sh

# Add to config.fish
zoxide init fish | source

# Now use 'z' instead of 'cd'
z projects         # Jump to most frecent match
z foo bar          # Jump to dir matching both "foo" and "bar"
zi                 # Interactive selection with fzf

direnv (Per-directory Environment)

# Install direnv
sudo apt install direnv   # or brew install direnv

# Add to config.fish
direnv hook fish | source

# Create .envrc in a project directory
echo 'export NODE_ENV=development' > .envrc
direnv allow

# Now the env vars activate automatically when you cd into the directory

Nix and home-manager Integration

# Fish is a first-class citizen in the Nix ecosystem

# Install via Nix
nix-env -iA nixpkgs.fish

# home-manager configuration
# ~/.config/home-manager/home.nix
{
  programs.fish = {
    enable = true;
    shellInit = ''
      set -gx EDITOR vim
    '';
    shellAbbrs = {
      gs = "git status";
      gc = "git commit";
    };
    plugins = [
      {
        name = "autopair";
        src = pkgs.fetchFromGitHub {
          owner = "jorgebucaran";
          repo = "autopair.fish";
          rev = "1.0.4";
          sha256 = "...";
        };
      }
    ];
  };
}

Debugging Fish Scripts

# Enable debug output
fish --debug-level=3

# Debug specific categories
fish --debug='config'      # Debug config loading
fish --debug='complete'    # Debug completion
fish --debug='history'     # Debug history

# Trace function execution
function myfunc --no-scope-shadowing
    set -g fish_trace 1
    # ... commands will be traced ...
    set -g fish_trace 0
end

# Print debug info
fish -c 'echo $version'
# 3.7.0

fish -c 'echo $fish_pid'

# Check Fish installation
type fish
# fish is /usr/bin/fish

# Validate syntax
fish -n ~/.config/fish/config.fish
# (no output = valid)

Fish 4.0+ Features

Recent Fish versions have added significant capabilities:

# Fish 3.6+: Improved string operations
string match -r '(?<year>\d{4})-(?<month>\d{2})' '2024-07'
# year=2024 month=07

# Fish 3.6+: $status_stack
# Access exit status of previous commands in a pipeline

# Fish 4.0+ (planned):
# - Rust rewrite of parts of the core
# - Improved startup performance
# - Better Windows support

References

  • Shell Overview — shell types and fundamentals
  • Zsh — alternative modern shell with POSIX compatibility
  • POSIX Shell — portable scripting (what Fish deliberately avoids)