cheat sheet

fish

Comprehensive fish shell reference covering syntax, variables, functions, abbreviations, completions, config files, path management, prompt customization, Fisher plugins, fish 4.x features (Rust rewrite), and migration from bash/zsh.

fish — Friendly Interactive Shell

What it is

fish (Friendly Interactive SHell) is a smart, user-friendly Unix shell maintained as an open-source project at fishshell.com. It ships with autosuggestions, syntax highlighting, tab completions, and a cleaner scripting syntax out of the box — no configuration required. Reach for fish when you want a highly usable interactive shell without spending time setting up plugins; for scripting shared across systems, bash or sh remains the more portable choice.

Installation

bash
# 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

Output (fish --version):

text
fish, version 4.7.1

Fish 4.0+ note — Fish 4.0 (Feb 2025) was a major release: the core was rewritten from C++ to Rust. Building from source now requires cargo and Rust ≥ 1.70 instead of a C++ compiler, and ncurses is no longer a dependency (terminfo is still consulted at runtime). All scripts and config files from fish 3.x continue to work unchanged — the rewrite was internal. See the What's new in fish 4.x section below for user-visible changes.

Configuration files

config.fish is the primary entry point and runs on every shell start, making it the right place for environment variables and abbreviations. Files dropped into conf.d/ are loaded alphabetically before config.fish, which is useful for splitting config across plugins or topics without editing the main file.

ruby
~/.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)
fish
# 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

Output: (none — exits 0 on success)

Basic syntax

Variables

Fish variables are set with set rather than VAR=val assignment syntax. Scope is explicit: -l local, -g global (current session), -gx global+exported, and -U universal (persists across all sessions via ~/.config/fish/fish_variables).

fish
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

Output: (none — exits 0 on success)

Command substitution

Fish uses (cmd) instead of bash's $(cmd). The output is split on newlines into a list, so command substitution naturally produces fish arrays — no IFS manipulation needed.

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

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

Output: (none — exits 0 on success)

Conditionals

Fish conditionals use if/else if/else/end — there are no curly braces. The test builtin is the standard way to evaluate conditions; [[ ]] from bash does not exist in fish.

fish
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"

Output: (none — exits 0 on success)

Loops

Both for and while loops close with end rather than done. for iterates over a list (or glob expansion) directly; use seq inside a command substitution for numeric ranges since fish has no C-style for ((i=0; ...)) syntax.

fish
# 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

Output: (none — exits 0 on success)

Functions

Fish functions are first-class and autoloaded: placing a function in ~/.config/fish/functions/name.fish makes it available in every session without sourcing anything. Use argparse for robust flag handling instead of manually shifting $argv.

fish
# 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

Output (greet Alice):

text
Hello, Alice!

Output (my_tool --name Alice --verbose):

text
Verbose mode
Name: Alice

Output (functions greet):

text
# Defined in  ~/.config/fish/functions/greet.fish @ line 1
function greet
    echo "Hello, $argv[1]!"
end

String operations

The string builtin replaces most uses of sed, awk, tr, and grep for string manipulation within fish scripts. It covers splitting, joining, trimming, matching (glob or regex), replacing, and padding — all without spawning a subprocess.

fish
# 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

Output (representative string calls):

text
5
HELLO
hello
hello
a
b
c
a,b,c
file.txt
abc123
bar baz
ell
hahaha
local

Math

The math builtin handles integer and floating-point arithmetic, including trig and logarithm functions, without requiring bc or awk. Use --scale to control decimal precision; the default is integer output.

fish
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

Output (representative math calls):

text
5
3
3.333333
1
256
0.1429
1
12
7
1
15

Status & error handling

$status holds the exit code of the last command, analogous to bash's $?. Fish's ; and / ; or chaining is the idiomatic replacement for && / || — they read naturally and work correctly with fish's whitespace rules.

fish
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

Output: (none — exits 0 on success)

Abbreviations (smart aliases)

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

fish
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

Output (abbr --list):

text
abbr d docker
abbr dc 'docker compose'
abbr gs 'git status'
abbr gc 'git commit'
abbr gp 'git push'
abbr k kubectl
abbr ll 'ls -la'
abbr pip pip3
abbr py python3

PATH management

fish_add_path is the recommended way to extend $PATH in fish: it deduplicates entries and persists the change via a universal variable so it survives shell restarts without re-sourcing config. Avoid directly manipulating $PATH with set -x except for temporary, session-only changes.

fish
# 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

Output (printf '%s\n' $PATH):

text
/usr/local/bin
/Users/alice/.local/bin
/Users/alice/.cargo/bin
/Users/alice/go/bin
/usr/bin
/bin
/usr/sbin
/sbin

Useful built-in functions

fish
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"

Output: (none — exits 0 on success)

Job control

fish
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

Output: (none — exits 0 on success)

History

fish
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

Output: (none — exits 0 on success)

Read & user input

fish
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

Output: (none — exits 0 on success)

Source & eval

fish
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)

Output: (none — exits 0 on success)

Type checking

fish
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

Output (type git):

text
git is /usr/bin/git

Output (command -v nvim / command -q nvim; and echo "found"):

text
/usr/bin/nvim
found

Completions

Fish ships with completions for hundreds of common commands, loaded automatically from /usr/share/fish/vendor_completions.d/. Custom completions live in ~/.config/fish/completions/<cmd>.fish and are loaded on demand; the complete builtin registers individual options with descriptions, condition functions, and argument types.

fish
# 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

Output: (none — exits 0 on success)

Prompt customization

The prompt is a regular fish function named fish_prompt; redefine it to change what appears before the cursor. Run fish_config to open a browser-based UI for previewing and selecting built-in prompt themes without editing files manually.

fish
# 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

Output: (none — exits 0 on success)

Environment & interop

Fish does not support VAR=value command inline assignment syntax — use env VAR=value command or a set -lx before the command. When you need to run a bash snippet (e.g., to source a bash-only script), invoke bash -c "..." explicitly.

fish
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

Output: (none — exits 0 on success)

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)
fish
# 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

Output: (none — exits 0 on success)

Fisher — plugin manager

Fisher is a minimal, fast plugin manager for fish that installs plugins directly from GitHub repos and manages them through a plain-text fish_plugins file, making plugin state reproducible across machines. It is the most widely used fish plugin manager as of 2025.

fish
# 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

Output (fisher list):

text
jorgebucaran/fisher
jethrokuan/z
PatrickF1/fzf.fish
jorgebucaran/autopair.fish
ilancosman/tide

Oh My Fish (OMF)

Oh My Fish is an older, heavier plugin framework modelled after Oh My Zsh, offering a curated package registry and theme ecosystem. Fisher is generally preferred for new setups; OMF remains useful if you already rely on its theme collection.

fish
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

Output (omf list):

text
Installed:
  Packages  bobthefish z
  Themes    bobthefish

Scripting & scripts

Fish scripts use the #!/usr/bin/env fish shebang and share the same syntax as interactive commands — no separate "scripting mode." Because fish is not POSIX-compatible, scripts intended to run on arbitrary systems should still be written in sh or bash.

fish
#!/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

Output: (none — exits 0 on success)

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

fish
# 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

Output: (none — exits 0 on success)

Debugging

fish
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

Output (fish -n script.fish — no errors):

text

Output (status features):

text
stderr-nocaret  on
qmark-noglob  on
regex-easyesc  on
ampersand-nobg-in-token  on

Output (functions --details greet):

text
/Users/alice/.config/fish/functions/greet.fish

What's new in fish 4.x

Fish 4.0 (Feb 2025) shipped the Rust rewrite and introduced readable key notation, OSC 133 prompt marking, and per-command abbreviations. Subsequent 4.x releases through 4.7 (May 2026) added transient prompts, vi-mode parity with Vim, light/dark color themes, and a status language builtin. Everything below is additive — fish 3.x scripts run unchanged.

Readable key bindings (4.0)

bind now accepts human-readable names like ctrl-right, alt-up, and shift-f5 in place of escape sequences. When the terminal supports xterm's modifyOtherKeys or the kitty keyboard protocol (both auto-enabled by fish 4.0), modifier combinations like ctrl-i and ctrl-shift-tab become bindable independently.

fish
# Old (still works)
bind \cl 'clear; commandline -f repaint'

# New, readable form (fish 4.0+)
bind ctrl-l    'clear; commandline -f repaint'
bind alt-enter newline
bind ctrl-shift-tab prev-completion   # only with kitty/modifyOtherKeys

# List all keys currently bound in a mode
bind --mode default

Output: (none — exits 0 on success)

Per-command abbreviations (4.0)

abbr --command <cmd> restricts an abbreviation to expand only when the surrounding command matches. Useful for short subcommand aliases that should not collide with the same token typed at the prompt.

fish
# 'co' expands to 'checkout' only after 'git'
abbr -a --command git co checkout
abbr -a --command git st status

# Function-style abbreviation that consults position
function _last_dir; basename (pwd); end
abbr -a --function _last_dir cwd

Output: (none — exits 0 on success)

Brace-grouped commands (4.0)

Fish 4.0 added { … } as a compound command grouping syntax, matching the muscle memory bash users already have. The block runs in the current shell (no subshell) and the trailing } must sit on its own token boundary.

fish
{ echo one; echo two; } > both.log

if test -d build
    { cd build; and make; and ctest }
end

Output: (none — exits 0 on success)

Silent-optional input redirection (4.0)

<? file reads from file if it exists, otherwise silently falls back to /dev/null. Replaces [ -f file ] && cat file style guards before piping optional config into a command.

fish
mycmd <? ~/.config/mycmd/extra.conf

Output: (none — exits 0 on success)

OSC 133 prompt marking (4.0, refined 4.3)

Fish now emits OSC 133 escape sequences around prompts and command output so modern terminals (WezTerm, kitty, iTerm 2, Ghostty, VS Code's terminal) can jump between commands, fold output, and re-run prior invocations. Fish 4.3 also emits OSC 7 (current working directory) on every fresh prompt so terminals can open new tabs in the same directory without shell-specific config.

fish
# Nothing to enable — it's on by default in fish 4.0+
# Test it: in a supporting terminal, look for "previous/next prompt" shortcuts.

Output: (none — exits 0 on success)

History gatekeeping (4.0)

Override fish_should_add_to_history to reject commands from history based on any logic — prefix, regex, env state, or a deny-list file. Returning non-zero skips the entry.

fish
function fish_should_add_to_history
    # Don't record commands starting with a space (zsh HIST_IGNORE_SPACE behavior)
    string match -q ' *' -- $argv[1]; and return 1
    # Don't record secrets-ish commands
    string match -rq '(?i)(passwd|token|secret)' -- $argv[1]; and return 1
    return 0
end

Output: (none — exits 0 on success)

Transient prompts (4.1)

Setting fish_transient_prompt to 1 swaps in a simplified prompt for previously-submitted commands so only the current prompt stays full-detail. Define fish_prompt_transient to control what the collapsed form looks like; falls back to a single $ if undefined.

fish
# In config.fish
set -g fish_transient_prompt 1

function fish_prompt_transient
    set_color brblack
    echo -n '❯ '
    set_color normal
end

Output: (none — exits 0 on success)

Improved argparse (4.1)

argparse gained --move-unknown (forwards unrecognized flags into $argv) and --multiple-vals (collect repeated occurrences of an option into a list). Together they make wrapper functions cleaner.

fish
function deploy
    argparse --move-unknown 'e/env=' 'p/preset=+' -- $argv
    or return

    echo "env: $_flag_env"
    echo "presets: $_flag_preset"   # list of all --preset values
    echo "forwarded: $argv"
end

deploy --env prod --preset fast --preset cached -- --extra-flag

Output (deploy --env prod --preset fast --preset cached -- --extra-flag):

text
env: prod
presets: fast cached
forwarded: --extra-flag

Vi-mode parity with Vim (4.4)

Fish 4.4 rewrote vi-mode word movement to match Vim semantics for w, W, e, E and added Vim-style count prefixes — d3w, 3l, c2W all work. New named functions (forward-word-vi, kill-word-vi, forward-bigword-vi, kill-bigword-vi, …) are available for custom bindings. Ctrl-a / Ctrl-x increment and decrement the number at the cursor, also matching Vim.

fish
# Enable vi mode
fish_vi_key_bindings

# Rebind a vi-mode normal-mode key
bind --mode default ',w' forward-word-vi

Output: (none — exits 0 on success)

Color-theme-aware themes (4.3)

Theme files (~/.config/fish/themes/<name>.theme) can now hold separate [light] and [dark] sections; fish picks the right one based on the terminal's reported background. Most fish_color_* variables also migrated from universal to global-with-default scope, so editing them in config.fish actually takes effect (universals previously masked the new defaults).

fish
# In ~/.config/fish/themes/mytheme.theme
[light]
fish_color_command = 005faf
fish_color_param   = 444444

[dark]
fish_color_command = 5fafff
fish_color_param   = bcbcbc

# Apply it
fish_config theme save mytheme

Output: (none — exits 0 on success)

status language (4.3)

status language reads or sets the locale used for fish's own messages without exporting LC_* to child processes. Solves the long-standing pain of wanting English fish errors while keeping a non-English LANG for everything else.

fish
status language                    # show current
status language en_US.UTF-8        # set to English for fish only

Output (status language after the set):

text
en_US.UTF-8

Path-component editing (4.3)

Three new readline-style functions operate on path components separated by /. Useful for editing long paths at the prompt without word-by-word stepping over each segment.

fish
bind alt-h backward-path-component
bind alt-l forward-path-component
bind alt-w kill-path-component

Output: (none — exits 0 on success)

Multi-line autosuggestions (4.1 / 4.2)

Autosuggestions now expand into multi-line history matches, so a 3-line for loop you ran an hour ago is offered as a single suggestion you can accept with Ctrl-E. Nothing to enable — fish 4.2+ matches across newlines automatically.

fish
# After typing 'for f', fish 4.2+ may suggest:
# for f in *.log
#     mv $f archive/
# end

Output: (none — exits 0 on success)

set_color enhancements (4.6)

set_color gained --foreground/--reset and per-modifier toggles like --italics=off, --bold=off. Combined with 4.1's underline support (double, curly, dotted, dashed, colored independently of text), prompts can now mimic richer-terminal styling without external tools.

fish
set_color --foreground 5fafd7 --underline=curly
echo "spell-check-ish underline"
set_color --reset

Output: (none — exits 0 on success)

Sources