Shell reference
Fish Shell Cheat Sheet
A practical reference for bash and zsh users switching to fish: syntax, commands, configuration, completions, scripting patterns, and the main POSIX-shell differences that cause shell shrapnel.
1. Mental model
What fish is
fish is an interactive shell with friendly defaults: autosuggestions, syntax highlighting, rich completions, universal variables, and a cleaner scripting syntax.
It is excellent as a daily interactive shell.
What fish is not
fish is not POSIX-compatible. Most bash and zsh scripts do not run unchanged in fish. Treat fish syntax as its own dialect, not a cosmetic bash variant.
Rule of thumb
Use fish interactively. Keep portable scripts in sh, bash scripts in bash, and fish scripts in files starting with:
#!/usr/bin/env fish
2. Basic command syntax
| Task | fish | bash/zsh equivalent |
|---|---|---|
| Run a command | ls -la | ls -la |
| Separate commands | echo one; echo two | echo one; echo two |
| Command substitution | echo (pwd) | echo $(pwd) |
| Use variable | echo $name | echo $name |
| Set variable | set name Alex | name=Alex |
| Export variable | set -x EDITOR vim | export EDITOR=vim |
| Erase variable | set -e name | unset name |
| Exit status | $status | $? |
VAR=value command, $(...), [[ ... ]], for x in ...; do ...; done, or export FOO=bar syntax.
3. Variables
Set variables
set name Alex
set count 42
set empty ""
set words alpha beta gamma
No =. Values are separated by spaces. Multiple values make a list.
Scope flags
set -l tmp value # local to current block/function
set -g theme dark # global to current fish process
set -U theme dark # universal, persisted across sessions
set -x EDITOR nvim # exported environment variable
set -gx PATH /opt/bin $PATH
Erase and inspect
set -e name # erase variable
set -q name # test whether variable exists
echo $name
set --show name # detailed variable info
Lists and indexing
set fruits apple banana cherry
echo $fruits # apple banana cherry
echo $fruits[1] # apple, fish uses 1-based indexes
echo $fruits[2..3] # banana cherry
echo $fruits[-1] # cherry
set fruits[2] pear # replace item 2
set -a fruits mango # append
set -p fruits lime # prepend
Strings
set greeting "hello world"
echo $greeting # one argument because it is one variable element
string length $greeting
string replace hello hi $greeting
string split / $PATH
4. Expansion, quoting, and substitution
Command substitution
set here (pwd)
echo "Current directory: $here"
# Multiple lines become separate arguments by default
for file in (find . -name '*.go')
echo $file
end
Variable expansion
set dir /tmp
echo $dir/file.txt # /tmp/file.txt
set name world
echo hello_$name # hello_world
echo "hello $name" # hello world
Braces and globs
echo file.{txt,md} # brace expansion
ls *.fish # globbing
ls **.fish # recursive glob, fish-specific
5. Conditionals and tests
if / else
if test -f config.toml
echo "config exists"
else if test -d config.toml
echo "config.toml is a directory"
else
echo "missing"
end
test shortcuts
| Check | fish |
|---|---|
| File exists | test -e file |
| Regular file | test -f file |
| Directory | test -d dir |
| String equals | test "$a" = "$b" |
| String not empty | test -n "$a" |
| Integer greater than | test $n -gt 10 |
| Command available | command -q git |
Combining commands
command -q git; and echo "git found"
test -f README.md; or echo "no README"
if command -q rg; and test -d .git
echo "ripgrep in a git repo"
end
fish supports and, or, and not instead of relying mainly on &&, ||, and !. Modern fish versions also accept && and ||, but the native words are idiomatic.
6. Loops
for
for file in *.txt
echo $file
end
for n in (seq 1 5)
echo $n
end
while
while test (count *.log) -gt 0
echo "logs exist"
break
end
Read lines
cat urls.txt | while read -l url
echo "fetching $url"
end
break and continue
for file in *
test -d $file; and continue
echo $file
end
7. Functions and aliases
Define a function
function mkcd
mkdir -p $argv[1]
cd $argv[1]
end
Arguments
function greet
echo "Hello, $argv[1]"
echo "All args: $argv"
end
Function arguments live in $argv. fish does not use $1, $2, $@, or $#.
Named arguments with argparse
function deploy
argparse 'e/env=' 'dry-run' -- $argv
or return
set -q _flag_env; or set _flag_env staging
if set -q _flag_dry_run
echo "Dry-run deploy to $_flag_env"
else
echo "Deploying to $_flag_env"
end
end
Aliases
alias ll='ls -lh'
alias gs='git status'
# Persist an alias by turning it into a function
funcsave ll
Save functions
functions # list functions
functions mkcd # show function body
funcsave mkcd # save to ~/.config/fish/functions/mkcd.fish
funced mkcd # edit function interactively
8. Pipes, redirects, and jobs
| Task | fish |
|---|---|
| Pipe stdout | cmd1 | cmd2 |
| Redirect stdout | cmd > out.txt |
| Append stdout | cmd >> out.txt |
| Redirect stderr | cmd 2> err.txt |
| Redirect stderr to stdout | cmd 2>&1 |
| Redirect both stdout and stderr | cmd &> all.txt |
| Background job | long-command & |
| List jobs | jobs |
| Bring job to foreground | fg |
| Resume in background | bg |
Exit status
false
echo $status # 1
true | false
echo $pipestatus # statuses of pipeline commands
9. Common and useful fish commands
Help and discovery
help # open fish help
help set # help for command
man fish # manual
fish --version
fish_config # web UI for config/theme/functions
Variables
set # list variables
set --show PATH
set -q VAR # exists?
set -e VAR # erase
set -Ux NAME value # universal exported var
Path management
fish_add_path ~/.local/bin
fish_add_path /opt/homebrew/bin
contains /opt/bin $PATH; or fish_add_path /opt/bin
Functions
functions
functions name
funced name
funcsave name
functions -e name # erase function
Completions
complete -c mycmd # show completions
complete -c mycmd -e # erase completions
fish_update_completions # generate from man pages
History
history
history search git
history delete --exact "bad command"
Prompt
fish_config prompt # choose prompt interactively
funced fish_prompt
funcsave fish_prompt
Abbreviations
abbr -a gco git checkout
abbr -a gst git status
abbr -a k kubectl
abbr --show
abbr -e gst
Abbreviations expand as you type. They are often better than aliases because the final command is visible and editable.
10. Configuration files and startup
| Path | Purpose |
|---|---|
~/.config/fish/config.fish | Main user config, similar in spirit to .zshrc or .bashrc. |
~/.config/fish/functions/*.fish | Autoloaded function files. One function per file is the usual pattern. |
~/.config/fish/completions/*.fish | User completions. |
~/.config/fish/conf.d/*.fish | Small config snippets loaded automatically. |
~/.config/fish/fish_variables | Universal variables. Usually edited via set -U, not manually. |
Typical config.fish
# ~/.config/fish/config.fish
# Only run interactive config in interactive shells
if status is-interactive
fish_add_path ~/.local/bin
fish_add_path /opt/homebrew/bin
set -gx EDITOR nvim
set -gx VISUAL nvim
abbr -a gst git status
abbr -a gco git checkout
abbr -a k kubectl
end
Login shell
# Check path to fish
command -v fish
# Add fish to allowed shells if needed
# Example path may vary:
echo /opt/homebrew/bin/fish | sudo tee -a /etc/shells
# Change login shell
chsh -s /opt/homebrew/bin/fish
11. Completions
fish completions are first-class and often more readable than bash completion scripts.
Simple completion
complete -c mytool -s h -l help -d "Show help"
complete -c mytool -s v -l verbose -d "Verbose output"
complete -c mytool -l env -a "dev staging prod" -d "Environment"
Completion from command output
complete -c killbyname -a "(ps -axo comm | sort -u)" -d "Process name"
Completion files
~/.config/fish/completions/mytool.fish
12. Scripting patterns
Script skeleton
#!/usr/bin/env fish
set -l script_dir (dirname (status filename))
function usage
echo "usage: myscript <name>"
end
if test (count $argv) -lt 1
usage
exit 1
end
set -l name $argv[1]
echo "Hello, $name from $script_dir"
Status helpers
status is-interactive # true if interactive shell
status is-login # true if login shell
status filename # current script/function file
status dirname # directory of current script/function file
Error handling
mkdir -p build
or begin
echo "failed to create build" >&2
exit 1
end
command -q git
or begin
echo "git is required" >&2
exit 1
end
Temporary environment for one command
# bash/zsh:
# FOO=bar command
# fish:
env FOO=bar command
# or:
begin
set -lx FOO bar
command
end
13. Main differences from bash/zsh
| Concept | bash/zsh | fish | Notes |
|---|---|---|---|
| POSIX compatibility | sh-like | Not POSIX | Do not expect shell scripts to run unchanged. |
| Set variable | x=1 | set x 1 | No equals sign. |
| Export | export x=1 | set -x x 1 | Use set -gx for global exported values. |
| Temporary env | x=1 cmd | env x=1 cmd | Or use a begin block with set -lx. |
| Command substitution | $(cmd) | (cmd) | Parentheses, not dollar-parens. |
| Exit code | $? | $status | Pipeline statuses in $pipestatus. |
| Function args | $1 $2 $@ $# | $argv[1] $argv[2] $argv | Fish lists are 1-indexed. |
| Blocks | if ...; then ... fi | if ...; ...; end | All major blocks close with end. |
| Loops | for x in y; do ...; done | for x in y; ...; end | No do or done. |
| Conditionals | [[ ... ]] | test ... | Use test, string, or command status. |
| Logical operators | &&, ||, ! | and, or, not | Native fish style is word-based. |
| Subshell grouping | (cd dir; cmd) | begin; cd dir; cmd; end | begin does not create a subshell by itself. |
| Arrays | arr=(a b) | set arr a b | All variables can be lists. |
| Indexing | Usually 0-based in bash arrays | 1-based | $arr[1] is first element. |
| Word splitting | Variables split unless quoted | No automatic whitespace splitting | Less quoting anxiety, fewer footguns. |
| Here docs | cat <<EOF | No traditional heredocs | Use printf, string join, or external files. |
| Aliases | Alias syntax | Aliases are wrapper functions | alias exists, but functions and abbreviations are often better. |
14. Everyday recipes
Append to PATH safely
fish_add_path ~/.local/bin
Set default editor
set -Ux EDITOR nvim
set -Ux VISUAL nvim
Run command if available
if command -q rg
rg TODO
else
grep -R TODO .
end
Make a project shortcut
function cproj
cd ~/src/my-project
end
funcsave cproj
Find files and loop
for file in (find . -name '*.json')
jq . $file >/dev/null
or echo "invalid: $file"
end
Prompt confirmation
read -l -P "Deploy to prod? [y/N] " answer
if test "$answer" = y
echo "deploying"
end
Count arguments
if test (count $argv) -eq 0
echo "no args"
end
Capture command output
set branch (git branch --show-current)
echo "branch: $branch"
15. Migration tips for bash/zsh users
- Do not paste your entire
.bashrcor.zshrcintoconfig.fish. Port it deliberately. - Start with PATH, editor, and abbreviations. Most of the value comes quickly.
- Use
fish_add_pathinstead of manually rebuildingPATH. - Prefer
abbrfor command shortcuts. It expands visibly, so your history contains the real command. - Use functions for logic. They are cleaner than aliases for anything with arguments.
- Keep existing scripts as-is. Run bash scripts with bash, zsh scripts with zsh, and fish scripts with fish.
- Remember the big five syntax swaps:
setfor assignment,(cmd)for substitution,$statusfor exit status,$argvfor function args, andendfor closing blocks.
16. Mini reference
Variables
set x value
set -l x value
set -g x value
set -U x value
set -x x value
set -gx x value
set -e x
set -q x
Blocks
if test ...
...
else if test ...
...
else
...
end
for x in ...
...
end
while test ...
...
end
switch $x
case a
...
case '*'
...
end
Functions
function name
echo $argv
end
funcsave name
funced name
functions name
Commands
help
fish_config
fish_add_path
abbr
complete
history
status
string
math
argparse