skip to content

fish β€” Friendly Interactive Shell

Comprehensive fish shell reference covering syntax, variables, functions, abbreviations, completions, config files, path management, prompt customization, Fisher plugins, and migration from bash/zsh.

14 min read 28 snippets 2d ago intermediate

fish β€” Friendly Interactive Shell#

fish is a smart, user-friendly command line shell with autosuggestions, syntax highlighting, tab completions, and a sane scripting language β€” all out of the box, zero config needed.

Installation#

# macOS
brew install fish

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

# Arch / Manjaro
sudo pacman -S fish

# Fedora
sudo dnf install fish

# Set as default shell
which fish                         # find fish path, e.g. /usr/local/bin/fish
echo /usr/local/bin/fish | sudo tee -a /etc/shells
chsh -s /usr/local/bin/fish        # set for current user

# Verify
fish --version

Configuration files#

~/.config/fish/config.fish         # main config (runs on every shell start)
~/.config/fish/functions/          # one file per function: name.fish
~/.config/fish/completions/        # custom completions: cmd.fish
~/.config/fish/conf.d/             # drop-in config files (loaded alphabetically)
~/.config/fish/fish_variables      # universal variables (auto-managed, don't edit)
/etc/fish/config.fish              # system-wide config
/usr/share/fish/functions/         # built-in functions (read-only reference)
# config.fish β€” typical layout
set -gx EDITOR nvim
set -gx PAGER less

fish_add_path /usr/local/bin
fish_add_path ~/.local/bin

# Aliases live as abbreviations or functions (see below)
abbr -a g git
abbr -a k kubectl

Basic syntax#

Variables#

set name "Alice"                   # set a local variable
echo $name                         # Alice
echo "Hello, $name"                # Hello, Alice
echo "args: $argv"                 # $argv = positional args in functions

set -l name "local"                # local scope (inside function)
set -g name "global"               # global scope (current session)
set -x name "exported"             # export to environment (same as -gx)
set -gx NAME "value"               # global + exported (most common for env vars)
set -U name "universal"            # universal: persists across all sessions

set -e name                        # erase/unset a variable
set -q name                        # test if variable is set (exit 0 = set)
set -S name                        # show variable scope info

# Lists (fish arrays are 1-indexed)
set fruits apple banana cherry
echo $fruits[1]                    # apple
echo $fruits[2]                    # banana
echo $fruits[-1]                   # cherry (last)
echo $fruits[1..2]                 # apple banana (slice)
count $fruits                      # 3

# Append to a list
set -a fruits mango
set fruits $fruits grape           # prepend

Command substitution#

set today (date +%F)               # $(…) in bash = (…) in fish
set files (ls *.txt)
echo "Today is $today"

# Inline
echo "You have "(count (ls))" files"

Conditionals#

if test $status -eq 0
    echo "success"
else if test $status -eq 1
    echo "failure"
else
    echo "other"
end

# String tests
if test -n "$name"                 # non-empty string
if test -z "$name"                 # empty string
if test "$a" = "$b"                # string equal
if test "$a" != "$b"               # string not equal

# File tests
if test -f file.txt                # is a regular file
if test -d /tmp                    # is a directory
if test -e path                    # exists
if test -x script.sh               # is executable
if test -s file.txt                # non-empty file

# Combining conditions
if test -f file.txt; and test -r file.txt
    echo "exists and readable"
end

# One-liner
test -d /tmp && echo "yes" || echo "no"

Loops#

# for loop
for f in *.txt
    echo $f
end

for i in (seq 1 10)
    echo $i
end

for item in $list
    echo $item
end

# while loop
while test $count -lt 10
    set count (math $count + 1)
end

# Loop over lines of a file
while read -l line
    echo $line
end < file.txt

# break / continue
for i in (seq 1 10)
    if test $i -eq 5; continue; end
    if test $i -eq 8; break; end
    echo $i
end

Functions#

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

greet Alice                        # Hello, Alice!

# With named arguments (fish convention)
function backup
    set src $argv[1]
    set dst $argv[2]
    cp -r $src $dst
    echo "Backed up $src β†’ $dst"
end

# With flags using argparse
function my_tool
    argparse 'n/name=' 'v/verbose' -- $argv
    or return

    if set -q _flag_verbose
        echo "Verbose mode"
    end
    echo "Name: $_flag_name"
end

my_tool --name Alice --verbose

# Functions are auto-loaded from ~/.config/fish/functions/name.fish
# funcsave greet                   # saves current function to that file

# View a function definition
functions greet
functions --all                    # list all defined functions
type greet                         # same as functions greet

String operations#

# string β€” fish's powerful string builtin
string length "hello"              # 5
string upper "hello"               # HELLO
string lower "HELLO"               # hello
string trim " hello "              # hello
string trim --left " hello"        # hello
string trim --right "hello "       # hello

string split , "a,b,c"             # a  b  c (separate lines)
string split0 "a\0b\0c"            # split on NUL

string join , a b c                # a,b,c
string join \n a b c               # one per line

string match "*.txt" file.txt      # glob match (exit 0 on match)
string match -r '\d+' "abc123"     # regex match
string match -r -g '(\w+)' "hello world"  # capture groups

string replace foo bar "foo baz"   # foo baz β†’ bar baz
string replace -r '\s+' '_' "a b c"  # a_b_c (regex replace)
string replace -a '/' '-' path/to/file  # replace all

string sub -s 2 -l 3 "hello"      # ell (start at index 2, length 3)
string repeat -n 3 "ha"            # hahaha
string pad -r -w 10 "hi"          # "hi        " (right-pad to width 10)
string escape "a b"                # a\ b
string unescape "a\\ b"           # a b

# Containment
if string match -q "*fish*" $description
    echo "mentions fish"
end

# Splitting a string into a list
set parts (string split / /usr/local/bin)
echo $parts[2]                     # local

Math#

math 2 + 3                         # 5
math 10 / 3                        # 3  (integer division)
math 10.0 / 3                      # 3.333333
math "10 % 3"                      # 1
math "2 ^ 8"                       # 256
math --scale 4 "1 / 7"             # 0.1429 (4 decimal places)
math "sin(pi/2)"                   # 1
math "sqrt(144)"                   # 12
math "max(3, 7, 1)"                # 7
math "min(3, 7, 1)"                # 1

set x 5
set y (math $x \* 3)               # 15

Status & error handling#

echo $status                       # exit status of last command (0 = ok)

command_that_might_fail
if test $status -ne 0
    echo "failed with status $status"
end

# or / and chaining
make; and echo "ok"; or echo "failed"

command; or return 1               # early return from function on failure
command; or exit 1                 # exit script on failure

# Suppress errors
command 2>/dev/null
command &>/dev/null

Abbreviations (smart aliases)#

Abbreviations expand in-place as you type β€” unlike aliases, the real command is stored in history.

abbr -a g git                      # g β†’ git
abbr -a gs "git status"
abbr -a gc "git commit"
abbr -a gp "git push"
abbr -a ll "ls -la"
abbr -a k kubectl
abbr -a d docker
abbr -a dc "docker compose"
abbr -a py python3
abbr -a pip pip3

abbr --list                        # show all abbreviations
abbr -e g                          # erase abbreviation

# Abbreviations persist across sessions (stored in universal vars)
# Add to config.fish for explicit management

PATH management#

# Preferred fish way β€” handles duplicates automatically
fish_add_path /usr/local/bin
fish_add_path ~/.local/bin
fish_add_path ~/.cargo/bin
fish_add_path (go env GOPATH)/bin

# Prepend vs append
fish_add_path --prepend /opt/custom/bin
fish_add_path --append /usr/local/opt/coreutils/bin

# View PATH as a list
echo $PATH                         # space-separated
printf '%s\n' $PATH                # one per line

# Persistent via universal variable
set -Ux fish_user_paths $fish_user_paths /new/path

# Temporary (current session only)
set -x PATH /tmp/tools $PATH

Useful built-in functions#

cdh                                # cd history interactive chooser (cd + fzf-like)
prevd                              # go to previous directory (like cd -)
nextd                              # go forward (after prevd)
prevd -l                           # show directory history list
dirh                               # print directory history

pushd /tmp                         # push to dir stack
popd                               # pop from dir stack

# z.fish / zoxide (popular plugins)
z projects                         # jump to frequently used dir matching "projects"

Job control#

jobs                               # list background jobs
fg %1                              # bring job 1 to foreground
bg %1                              # send job 1 to background
disown %1                          # detach job from terminal
wait                               # wait for all background jobs

History#

history                            # print command history
history | grep ssh                 # search history
history delete --prefix "bad cmd"  # remove entries starting with "bad cmd"
history merge                      # sync history from all fish sessions
builtin history search "pattern"   # search history

# Interactive history search: press ↑ or Ctrl-R
# Type partial command then press ↑ to filter history

Read & user input#

read -l name                       # read a line into $name
read -l -p "Enter name: " name     # with prompt
read -l -s password                # silent (no echo, for passwords)
read -l -a list                    # read words into a list

# Read lines from a file
while read -l line
    echo ">> $line"
end < /etc/hosts

Source & eval#

source ~/.config/fish/config.fish  # reload config
source script.fish                 # run a fish script in current session
eval $command                      # evaluate a string as fish code (use sparingly)

Type checking#

type git                           # show what "git" resolves to
builtin --names                    # list all built-in commands
functions --all | sort             # list all functions
command -v nvim                    # path to nvim, or nothing
command -q nvim; and echo "found"  # quiet check

Completions#

# List completions for a command
complete -c myapp                  # show registered completions for myapp

# Register completions (typically in ~/.config/fish/completions/myapp.fish)
complete -c myapp -f               # no file completions
complete -c myapp -s h -l help -d "Show help"
complete -c myapp -s o -l output -r -d "Output file"   # -r: requires arg
complete -c myapp -n "__fish_use_subcommand" -a build -d "Build project"
complete -c myapp -n "__fish_seen_subcommand_from build" -s r -l release

# Generate completions from --help or man page
myapp --help | fish_indent         # sometimes produces usable completions

# Common condition functions for completions
__fish_use_subcommand              # no subcommand seen yet
__fish_seen_subcommand_from cmd    # specific subcommand was seen
__fish_is_first_token              # completing the first token
__fish_complete_path               # complete file paths
__fish_complete_directories        # complete directories only

Prompt customization#

# Edit prompt interactively
fish_config                        # opens web UI in browser

# Manual: create ~/.config/fish/functions/fish_prompt.fish
function fish_prompt
    set -l status_color (set_color green)
    if test $status -ne 0
        set status_color (set_color red)
    end

    set -l cwd (prompt_pwd)        # abbreviated path
    echo -n -s (set_color blue) $USER (set_color normal) ":" \
               (set_color cyan) $cwd (set_color normal) " \$ "
end

# Right-side prompt: fish_right_prompt
function fish_right_prompt
    if git rev-parse --is-inside-work-tree &>/dev/null
        set branch (git branch --show-current 2>/dev/null)
        echo -n -s (set_color yellow) " " $branch (set_color normal)
    end
end

# Colors
set_color red                      # red foreground
set_color --bold cyan              # bold cyan
set_color --background blue        # blue background
set_color brgreen                  # bright green
set_color normal                   # reset
set_color -c                       # clear colors
fish_color_error                   # variable for error color theming

# Available color names: black red green yellow blue magenta cyan white
# Prefix br for bright: brred brgreen brcyan etc.
# Or hex: set_color 5fafd7

Environment & interop#

set -gx EDITOR nvim                # set env var
set -gx GOPATH ~/go
set -gx DOCKER_BUILDKIT 1

# Run bash/sh snippets from fish
bash -c "source ~/.bashrc && some_bash_fn"
sh -c "export FOO=bar && run_thing"

# Use bash for a single command
bash -c "echo $BASH_VERSION"

# Inline env for a command
env FOO=bar mycommand              # standard
FOO=bar mycommand                  # does NOT work in fish (use env or set -x)
set -lx FOO bar; mycommand         # fish way: local+exported temp var

# Export all variables
export                             # alias to "set -gx" in fish

Key bindings#

KeyAction
TabAutocomplete / cycle completions
Shift-TabPrevious completion
↑ / ↓Walk history (or filter if text typed)
Ctrl-RHistory search (interactive)
Ctrl-FAccept autosuggestion one char
Alt-FAccept autosuggestion one word
Ctrl-E / EndAccept full autosuggestion
Ctrl-CCancel line
Ctrl-DExit shell (on empty line)
Ctrl-LClear screen
Ctrl-A / HomeMove to beginning of line
Ctrl-E / EndMove to end of line
Alt-Left / Alt-BMove word left
Alt-Right / Alt-FMove word right
Ctrl-WDelete word left
Alt-DDelete word right
Alt-↑Insert previous argument (like !$)
Ctrl-ZSuspend (fg to resume)
Alt-EnterInsert newline (multiline command)
# Custom key bindings in config.fish
function fish_user_key_bindings
    bind \cl 'clear; commandline -f repaint'      # Ctrl-L
    bind \ce end-of-line                           # Ctrl-E
    bind \cg 'git status; commandline -f repaint'  # Ctrl-G β†’ git status
    bind \ef accept-autosuggestion                 # Alt-F
end

Fisher β€” plugin manager#

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

# Install plugins
fisher install jethrokuan/z            # directory jumper
fisher install PatrickF1/fzf.fish      # fzf integration
fisher install jorgebucaran/autopair.fish  # auto-pair brackets
fisher install nickeb96/puffer-fish    # useful abbreviations
fisher install ilancosman/tide@v6      # tide prompt

# Manage
fisher list                            # installed plugins
fisher update                          # update all
fisher remove jethrokuan/z             # remove a plugin

# Popular plugins
# jorgebucaran/nvm.fish     β€” Node version manager
# edc/bass                  β€” run bash utilities in fish
# jorgebucaran/autopair.fish β€” auto-close brackets
# PatrickF1/fzf.fish        β€” fzf tab completions + Ctrl-R
# jethrokuan/z              β€” frecency-based directory jump
# ilancosman/tide           β€” powerline-style prompt

Oh My Fish (OMF)#

curl https://raw.githubusercontent.com/oh-my-fish/oh-my-fish/master/bin/install | fish

omf install z                      # install a plugin
omf install bobthefish             # install a theme
omf list                           # installed packages
omf update                         # update all
omf theme                          # list themes
omf remove z                       # remove a package
omf doctor                         # diagnose issues

Scripting & scripts#

#!/usr/bin/env fish
# script.fish β€” always start with this shebang

set script_dir (dirname (status --current-filename))

# Argument validation
if test (count $argv) -lt 1
    echo "Usage: "(status --current-filename)" <name>"
    exit 1
end

set name $argv[1]

# Temporary files
set tmpfile (mktemp)
trap "rm -f $tmpfile" EXIT         # cleanup (note: fish uses signal trapping differently)

# Return values β€” fish functions use exit codes + output
function get_version
    echo "1.2.3"
end
set ver (get_version)

# Error propagation
function must_run
    eval $argv
    or begin
        echo "Command failed: $argv" >&2
        return 1
    end
end

Migration: bash β†’ fish equivalents#

bash / zshfish
export VAR=valset -gx VAR val
VAR=val commandenv VAR=val command or set -lx VAR val; command
$()()
${var:-default}if set -q var; echo $var; else; echo default; end
[[ -z "$var" ]]test -z "$var"
&& / ||; and / ; or
alias ll="ls -la"abbr -a ll "ls -la" or function ll; ls -la; end
source filesource file (same)
$# (arg count)count $argv
$@ / $*$argv
$1, $2$argv[1], $argv[2]
for i in $(seq 5)for i in (seq 5)
if [ $a = $b ]if test $a = $b
>>file>>file (same)
2>&12>&1 (same)
local var=valset -l var val
$RANDOMrandom (function)
echo -e "\n"echo \n (fish interprets escapes)
$PS1fish_prompt function
~/.bashrc~/.config/fish/config.fish
nvm use 18nvm use 18 (via nvm.fish plugin)
source venv/bin/activatesource venv/bin/activate.fish

Common patterns & recipes#

# Activate a Python virtualenv
source .venv/bin/activate.fish

# nvm (via jorgebucaran/nvm.fish)
nvm install 20
nvm use 20
nvm list

# Run a command for each line of output
cat urls.txt | while read -l url
    curl -sI $url | grep -i "^HTTP"
end

# Parallel background jobs
for host in server1 server2 server3
    ssh $host "uptime" &
end
wait

# Retry a command N times
function retry
    set -l n $argv[1]
    set -l cmd $argv[2..]
    for i in (seq $n)
        eval $cmd; and return 0
        echo "Attempt $i failed, retrying…"
        sleep 1
    end
    return 1
end
retry 3 curl -sf https://api.example.com

# Add color to output
function info;  echo (set_color cyan)"[INFO]"(set_color normal) $argv; end
function warn;  echo (set_color yellow)"[WARN]"(set_color normal) $argv; end
function error; echo (set_color red)"[ERROR]"(set_color normal) $argv >&2; end

# Check dependency before running
function require
    command -q $argv[1]; or begin
        error "Required command not found: $argv[1]"
        return 1
    end
end
require jq; or exit 1

# Dynamic function: wrap a command with a prefix
function k; kubectl $argv; end
function dc; docker compose $argv; end

# Quick file search alias
function ff
    find . -type f -iname "*$argv[1]*" 2>/dev/null
end

# Extract any archive
function extract
    switch $argv[1]
        case "*.tar.gz";  tar -xzf $argv[1]
        case "*.tar.bz2"; tar -xjf $argv[1]
        case "*.zip";     unzip $argv[1]
        case "*.7z";      7z x $argv[1]
        case "*.gz";      gunzip $argv[1]
        case "*"; echo "Unknown archive type: $argv[1]"
    end
end

# cd and ls together
function cl
    cd $argv[1]; and ls
end

# mkcd β€” create and enter a directory
function mkcd
    mkdir -p $argv[1]; and cd $argv[1]
end

Debugging#

fish -n script.fish                # syntax check without running
fish -c "echo test"                # run a command string
fish -d 5 script.fish              # debug level 5 (very verbose)

set fish_trace 1                   # trace all commands (like bash set -x)
function my_fn
    set fish_trace 1
    # ... code to trace ...
    set fish_trace 0
end

status --is-interactive            # true if interactive session
status --is-login                  # true if login shell
status --current-filename          # path to current script
status features                    # list feature flags

functions --details greet          # show where function is defined