Zsh vs Fish
Both ditched bash defaults — zsh stayed POSIX-ish, fish broke the syntax to make it friendlier.
Summary
Zsh is bash's power-user cousin — adds smarter globbing, better tab-completion, plugin frameworks like Oh My Zsh, but remains close enough to bash that most scripts run unchanged. Default macOS shell since Catalina.
Fish is a fresh design — interactive niceties (autosuggestions, syntax highlighting, sane defaults) at the cost of POSIX compatibility. Scripts that work in bash/zsh will often **fail to parse** in fish.
Practical rule: zsh is what you use when you want bash with fewer rough edges. Fish is what you use when you treat the shell as primarily an interactive tool, not a scripting engine.
Syntax & semantic differences
POSIX compatibility
ZshMostly POSIX-compatible — most bash scripts run unchanged. Quirks: word-splitting is disabled by default (`$var` does not split).
FishDeliberately non-POSIX. Most bash scripts fail to parse in fish — fish is its own language.
Variable assignment
Zsh`name=value` (POSIX) or `typeset name=value`. No `set` needed.
Fish`set name value` — the verb-form is the only way. `name=value` works in `env`-style command prefixes only.
Fish `set` is the assignment command, not a shell option. `set -x name value` is **export**, not "trace mode" as in bash.
Reading variables
Zsh`$name` or `${name}` — same as bash.
Fish`$name` works; `${name}` does **not** — fish has no brace expansion of variables.
Arrays
ZshNative arrays. 1-indexed (different from bash's 0-indexed!). `arr=(a b c); echo $arr[1]` prints `a`.
FishEvery variable is implicitly a list. 1-indexed. `set arr a b c; echo $arr[1]` prints `a`.
Conditional
Zsh`if [[ -f file ]]; then ...; fi` — `[[ ]]` is built-in.
Fish`if test -f file; ...; end` — no `[[ ]]`, use `test` (also aliased as `[ ... ]`) and `end` instead of `fi`.
Loops
Zsh`for f in *.log; do ...; done`
Fish`for f in *.log; ...; end` — terminator is `end`, not `done`.
Functions
Zsh`my_fn() { echo $1 }` — args are `$1`, `$2`, `$@`.
Fish`function my_fn; echo $argv[1]; end` — args are in `$argv`. Functions are auto-saved to `~/.config/fish/functions/`.
Command substitution
Zsh`$(cmd)` (preferred) or backticks (legacy).
Fish`(cmd)` — bare parentheses. `$(cmd)` is **not** supported; backticks are not supported.
Aliases / shortcuts
Zsh`alias name='cmd'` — like bash. Sticks in interactive shells.
Fish`alias name='cmd'` exists but is just a function wrapper. The idiomatic shortcut is `abbr name expansion` — expands at the prompt so you see the real command before running.
String math
Zsh`$((1 + 2))` arithmetic expansion is built-in.
FishNo `$((...))`. Use `math 1 + 2` or `math "1 + 2"`.
"Set and forget" PATH
Zsh`export PATH="$HOME/bin:$PATH"` in `~/.zshrc`.
Fish`fish_add_path $HOME/bin` — universal-variable helper. Or `set -Ux PATH $HOME/bin $PATH` once (`-U` = universal, persists across sessions).
Fish's universal variables are stored in `~/.config/fish/fish_variables`, not in any shell rc — easy to confuse if you're used to editing rc files.
Configuration file
Zsh`~/.zshrc` (interactive), `~/.zprofile` (login), `~/.zshenv` (every invocation).
Fish`~/.config/fish/config.fish`. Per-function and per-completion files live in `~/.config/fish/functions/` and `~/.config/fish/completions/`.
Glob qualifiers
ZshZsh has glob qualifiers — `ls *(.m-1)` lists files modified in last day, `*(/)` lists only dirs. Powerful, terse.
FishNo qualifiers — use `find` or filter with `string match` / `path filter`.
Side-by-side commands
The 32 most-compared commands in Zsh and Fish. See all Zsh commands · See all Fish commands.
- aliasCreate a shortcut name for a longer command line.Zsh
alias ll='ls -la'Fishalias ll='ls -la' - awkPattern scanning and processing language for structured text.Zsh
awk '{print $1}' fileFishawk '{print $1}' file - catPrint file contents to standard output.Zsh
cat file.txtFishcat file.txt - chmodChange file mode bits (read / write / execute permissions) on Unix files.Zsh
chmod 755 fileFishchmod 755 file - cpCopy files and directories.Zsh
cp source destFishcp source dest - curlTransfer data from or to a server over HTTP, HTTPS, FTP, and many other protocols.Zsh
curl https://example.comFishcurl https://example.com - cutExtract sections (fields or characters) from each line.Zsh
cut -d',' -f1 file.csvFishcut -d',' -f1 file.csv - echoPrint arguments to standard output, separated by spaces, followed by a newline.Zsh
echo "hello world"Fishecho "hello world" - exportSet an environment variable and mark it for export to child processes.Zsh
export NAME=valueFishset -gx NAME value - findLocate files by name, size, time, or other attributes.Zsh
find . -name "*.log"Fishfind . -name "*.log" - grepSearch file contents for a pattern.Zsh
grep -r "pattern" .Fishgrep -r "pattern" . - headOutput the first lines of a file.Zsh
head -n 10 fileFishhead -n 10 file - historyShow previously run commands from the shell history.Zsh
historyFishhistory - killSend a signal to a process (typically to terminate it).Zsh
kill <pid>Fishkill <pid> - lsList directory contents.Zsh
ls -laFishls -la - mkdirCreate a new directory.Zsh
mkdir dirFishmkdir dir - mvMove or rename files and directories.Zsh
mv source destFishmv source dest - pingSend ICMP echo requests to test reachability and round-trip latency.Zsh
ping example.comFishping example.com - psList a snapshot of currently running processes.Zsh
ps auxFishps aux - rmDelete files and directories.Zsh
rm fileFishrm file - sedStream editor for filtering and transforming text.Zsh
sed 's/old/new/g' fileFishsed 's/old/new/g' file - sortSort lines of text.Zsh
sort fileFishsort file - sshOpen a secure shell on a remote host or run a remote command.Zsh
ssh user@hostFishssh user@host - tailOutput the last lines of a file.Zsh
tail -n 10 fileFishtail -n 10 file - tarBundle and unbundle files into a single archive (often combined with gzip / bzip2 / xz).Zsh
tar -czf archive.tar.gz dir/Fishtar -czf archive.tar.gz dir/ - topInteractive process viewer — show running processes sorted by CPU or memory, refreshed in place.Zsh
topFishtop - touchCreate an empty file or update its modification time if it exists.Zsh
touch file.txtFishtouch file.txt - wcCount lines, words, or bytes.Zsh
wc -l fileFishwc -l file - wgetNon-interactive network downloader for HTTP, HTTPS, and FTP.Zsh
wget https://example.com/file.zipFishwget https://example.com/file.zip - whichLocate an executable in PATH and print its full path.Zsh
which python3Fishwhich python3 - xargsBuild and execute command lines from standard input.Zsh
find . -name '*.tmp' | xargs rmFishfind . -name '*.tmp' | xargs rm - zipPackage and compress files into a ZIP archive.Zsh
zip -r archive.zip dir/Fishzip -r archive.zip dir/
Gotchas when porting between them
- A bash script with `#!/usr/bin/env bash` runs fine on a fish system — fish only matters when **fish is the interpreter**. Don't panic about portability if your scripts have the right shebang.
- Zsh word-splitting differs from bash: `$var` does **not** split on whitespace by default. Set `SH_WORD_SPLIT` or use `${=var}` to force splitting (bash behavior).
- Fish has no `&&` / `||` — wait, it does, since fish 3.0. Older docs say to use `; and` / `; or`. Both work in fish 3.x+, but `; and` is still the documented idiom for scripts.
- Zsh `setopt` options are different from bash `shopt` options. `setopt nullglob` (no-match → empty) vs bash `shopt -s nullglob`.
- Fish does not honor `~/.bashrc` or `~/.profile`. Environment variables set by login scripts in bash terms may not be visible in a fish login.