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#
Navigation & directory#
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#
| Key | Action |
|---|---|
Tab | Autocomplete / cycle completions |
Shift-Tab | Previous completion |
β / β | Walk history (or filter if text typed) |
Ctrl-R | History search (interactive) |
Ctrl-F | Accept autosuggestion one char |
Alt-F | Accept autosuggestion one word |
Ctrl-E / End | Accept full autosuggestion |
Ctrl-C | Cancel line |
Ctrl-D | Exit shell (on empty line) |
Ctrl-L | Clear screen |
Ctrl-A / Home | Move to beginning of line |
Ctrl-E / End | Move to end of line |
Alt-Left / Alt-B | Move word left |
Alt-Right / Alt-F | Move word right |
Ctrl-W | Delete word left |
Alt-D | Delete word right |
Alt-β | Insert previous argument (like !$) |
Ctrl-Z | Suspend (fg to resume) |
Alt-Enter | Insert 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 / zsh | fish |
|---|---|
export VAR=val | set -gx VAR val |
VAR=val command | env 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 file | source 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>&1 | 2>&1 (same) |
local var=val | set -l var val |
$RANDOM | random (function) |
echo -e "\n" | echo \n (fish interprets escapes) |
$PS1 | fish_prompt function |
~/.bashrc | ~/.config/fish/config.fish |
nvm use 18 | nvm use 18 (via nvm.fish plugin) |
source venv/bin/activate | source 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