Skip to content
shellmap

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

    Zsh

    Mostly POSIX-compatible — most bash scripts run unchanged. Quirks: word-splitting is disabled by default (`$var` does not split).

    Fish

    Deliberately 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

    Zsh

    Native arrays. 1-indexed (different from bash's 0-indexed!). `arr=(a b c); echo $arr[1]` prints `a`.

    Fish

    Every 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.

    Fish

    No `$((...))`. 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

    Zsh

    Zsh has glob qualifiers — `ls *(.m-1)` lists files modified in last day, `*(/)` lists only dirs. Powerful, terse.

    Fish

    No 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'
    Fish
    alias ll='ls -la'
  • awkPattern scanning and processing language for structured text.
    Zsh
    awk '{print $1}' file
    Fish
    awk '{print $1}' file
  • catPrint file contents to standard output.
    Zsh
    cat file.txt
    Fish
    cat file.txt
  • chmodChange file mode bits (read / write / execute permissions) on Unix files.
    Zsh
    chmod 755 file
    Fish
    chmod 755 file
  • cpCopy files and directories.
    Zsh
    cp source dest
    Fish
    cp source dest
  • curlTransfer data from or to a server over HTTP, HTTPS, FTP, and many other protocols.
    Zsh
    curl https://example.com
    Fish
    curl https://example.com
  • cutExtract sections (fields or characters) from each line.
    Zsh
    cut -d',' -f1 file.csv
    Fish
    cut -d',' -f1 file.csv
  • echoPrint arguments to standard output, separated by spaces, followed by a newline.
    Zsh
    echo "hello world"
    Fish
    echo "hello world"
  • exportSet an environment variable and mark it for export to child processes.
    Zsh
    export NAME=value
    Fish
    set -gx NAME value
  • findLocate files by name, size, time, or other attributes.
    Zsh
    find . -name "*.log"
    Fish
    find . -name "*.log"
  • grepSearch file contents for a pattern.
    Zsh
    grep -r "pattern" .
    Fish
    grep -r "pattern" .
  • headOutput the first lines of a file.
    Zsh
    head -n 10 file
    Fish
    head -n 10 file
  • historyShow previously run commands from the shell history.
    Zsh
    history
    Fish
    history
  • killSend a signal to a process (typically to terminate it).
    Zsh
    kill <pid>
    Fish
    kill <pid>
  • lsList directory contents.
    Zsh
    ls -la
    Fish
    ls -la
  • mkdirCreate a new directory.
    Zsh
    mkdir dir
    Fish
    mkdir dir
  • mvMove or rename files and directories.
    Zsh
    mv source dest
    Fish
    mv source dest
  • pingSend ICMP echo requests to test reachability and round-trip latency.
    Zsh
    ping example.com
    Fish
    ping example.com
  • psList a snapshot of currently running processes.
    Zsh
    ps aux
    Fish
    ps aux
  • rmDelete files and directories.
    Zsh
    rm file
    Fish
    rm file
  • sedStream editor for filtering and transforming text.
    Zsh
    sed 's/old/new/g' file
    Fish
    sed 's/old/new/g' file
  • sortSort lines of text.
    Zsh
    sort file
    Fish
    sort file
  • sshOpen a secure shell on a remote host or run a remote command.
    Zsh
    ssh user@host
    Fish
    ssh user@host
  • tailOutput the last lines of a file.
    Zsh
    tail -n 10 file
    Fish
    tail -n 10 file
  • tarBundle and unbundle files into a single archive (often combined with gzip / bzip2 / xz).
    Zsh
    tar -czf archive.tar.gz dir/
    Fish
    tar -czf archive.tar.gz dir/
  • topInteractive process viewer — show running processes sorted by CPU or memory, refreshed in place.
    Zsh
    top
    Fish
    top
  • touchCreate an empty file or update its modification time if it exists.
    Zsh
    touch file.txt
    Fish
    touch file.txt
  • wcCount lines, words, or bytes.
    Zsh
    wc -l file
    Fish
    wc -l file
  • wgetNon-interactive network downloader for HTTP, HTTPS, and FTP.
    Zsh
    wget https://example.com/file.zip
    Fish
    wget https://example.com/file.zip
  • whichLocate an executable in PATH and print its full path.
    Zsh
    which python3
    Fish
    which python3
  • xargsBuild and execute command lines from standard input.
    Zsh
    find . -name '*.tmp' | xargs rm
    Fish
    find . -name '*.tmp' | xargs rm
  • zipPackage and compress files into a ZIP archive.
    Zsh
    zip -r archive.zip dir/
    Fish
    zip -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.

Full references

Other comparisons