Skip to content
shellmap

Bash vs Fish

Fish is everything bash isn't friendly to do — at the cost of bash script compatibility.

Summary

Bash is the lingua franca of Unix scripting — POSIX-extended, the assumed interpreter behind almost every `#!/bin/bash`/`#!/usr/bin/env bash` shebang. It is what tutorials, install scripts, CI runners, and Dockerfiles default to.

Fish was designed to be friendly first: autosuggestions from history, syntax highlighting at the prompt, sane defaults, and a smaller, cleaner scripting language. The trade-off is that fish is **not** POSIX — most bash scripts fail to parse.

Practical rule: use fish as your interactive shell if you like the UX, but keep writing scripts in bash (or POSIX `sh`). The `#!/usr/bin/env bash` shebang protects you — fish only runs scripts if fish is explicitly the interpreter.

Syntax & semantic differences

  • POSIX compatibility

    Bash

    POSIX-extended. The de facto target of "shell scripts" on most systems.

    Fish

    Deliberately non-POSIX. Bash scripts almost never run as-is in fish — fish parses them and rejects bash-only syntax.

  • Variable assignment

    Bash

    `name=value` (no spaces around `=`). `export name=value` to put it in the environment of child processes.

    Fish

    `set name value` (verb form). `set -x name value` to export. `name=value` only works inline as a one-off env override: `name=value cmd`.

    Fish's `set` is the assignment command, not a shell-option toggle. `set -x` is **export**, not "trace mode" as in bash — bash's `set -x` is `fish_trace 1` (fish 3.4+).

  • Reading variables

    Bash

    `$name`, `${name}`, `${name:-default}`, `${name#prefix}`, `${name%suffix}` — full POSIX parameter-expansion syntax.

    Fish

    `$name` works; `${name}` does **not** — no brace expansion. For defaults / fallbacks use `set -q name; or set name default`. For string slicing use `string sub` / `string replace` / `string match`.

  • Arrays / lists

    Bash

    Arrays with `arr=(a b c)`. 0-indexed. `${arr[@]}` for all elements, `${#arr[@]}` for length.

    Fish

    Every variable is implicitly a list. 1-indexed. `set arr a b c; echo $arr[1]` prints `a`. `count $arr` for length.

    The 0 vs 1 indexing difference is the most common porting bug. `${arr[0]}` in bash is `arr[1]` in fish.

  • Conditional

    Bash

    `if [[ -f file ]]; then ...; fi`. `[[ ]]` is the bash test built-in.

    Fish

    `if test -f file; ...; end`. No `[[ ]]`. Use `test` (also aliased as `[`) and `end` instead of `fi`.

  • Loops

    Bash

    `for f in *.log; do echo "$f"; done` — terminator is `done`.

    Fish

    `for f in *.log; echo $f; end` — terminator is `end`. Same `for ... in ...` shape otherwise.

  • Functions

    Bash

    `my_fn() { echo "$1"; }`. Args are `$1`, `$2`, `$@`. Functions live in shell memory until exit unless sourced from rc.

    Fish

    `function my_fn; echo $argv[1]; end`. Args are in the list `$argv`. Functions are typically saved as one file per function in `~/.config/fish/functions/<name>.fish`, autoloaded on first use.

  • Command substitution

    Bash

    `$(cmd)` (modern) or backticks (legacy). Both supported.

    Fish

    `(cmd)` — bare parentheses. `$(cmd)` is **not** valid syntax; backticks are not supported.

    Inside a string: bash `"prefix-$(date +%F)"` becomes fish `"prefix-"(date +%F)` — the substitution sits *outside* the quoted string, concatenated.

  • Aliases / shortcuts

    Bash

    `alias name='cmd'`. Sticks in interactive shells. Aliases are not exported to subshells unless `shopt -s expand_aliases`.

    Fish

    `alias name='cmd'` exists but is just a function wrapper. Idiomatic fish uses `abbr -a name expansion` — expands at the prompt so you see the real command before running.

  • Arithmetic

    Bash

    `$((1 + 2))` is built-in arithmetic expansion. Integer-only.

    Fish

    No `$((...))`. Use `math 1 + 2` or `math "1.5 + 2.5"`. Supports floating-point.

  • Logical operators in pipelines

    Bash

    `cmd1 && cmd2`, `cmd1 || cmd2`. Standard since forever.

    Fish

    `cmd1 && cmd2` works (fish 3.0+). Older fish required `cmd1; and cmd2` / `cmd1; or cmd2`, which is still the documented script idiom.

  • PATH manipulation

    Bash

    `export PATH="$HOME/bin:$PATH"` in `~/.bashrc` or `~/.profile`.

    Fish

    `fish_add_path $HOME/bin` (universal helper) or `set -Ux PATH $HOME/bin $PATH` once (`-U` = universal, persists across sessions). Stored in `~/.config/fish/fish_variables`, not in any rc file.

  • Configuration file

    Bash

    `~/.bashrc` (interactive non-login), `~/.bash_profile` / `~/.profile` (login). The sourcing chain is famously confusing.

    Fish

    `~/.config/fish/config.fish`. Functions live in `~/.config/fish/functions/`, completions in `~/.config/fish/completions/`. No login/non-login distinction in the same way.

  • Heredocs

    Bash

    `cat <<EOF\nline 1\nline 2\nEOF` is built-in. Quoting `<<'EOF'` disables interpolation.

    Fish

    No heredoc syntax. Use `printf` with `\n`, `echo` over multiple lines, or `string join \n a b c | …`.

Side-by-side commands

The 32 most-compared commands in Bash and Fish. See all Bash commands · See all Fish commands.

  • aliasCreate a shortcut name for a longer command line.
    Bash
    alias ll='ls -la'
    Fish
    alias ll='ls -la'
  • awkPattern scanning and processing language for structured text.
    Bash
    awk '{print $1}' file
    Fish
    awk '{print $1}' file
  • catPrint file contents to standard output.
    Bash
    cat file.txt
    Fish
    cat file.txt
  • chmodChange file mode bits (read / write / execute permissions) on Unix files.
    Bash
    chmod 755 file
    Fish
    chmod 755 file
  • cpCopy files and directories.
    Bash
    cp source dest
    Fish
    cp source dest
  • curlTransfer data from or to a server over HTTP, HTTPS, FTP, and many other protocols.
    Bash
    curl https://example.com
    Fish
    curl https://example.com
  • cutExtract sections (fields or characters) from each line.
    Bash
    cut -d',' -f1 file.csv
    Fish
    cut -d',' -f1 file.csv
  • echoPrint arguments to standard output, separated by spaces, followed by a newline.
    Bash
    echo "hello world"
    Fish
    echo "hello world"
  • exportSet an environment variable and mark it for export to child processes.
    Bash
    export NAME=value
    Fish
    set -gx NAME value
  • findLocate files by name, size, time, or other attributes.
    Bash
    find . -name "*.log"
    Fish
    find . -name "*.log"
  • grepSearch file contents for a pattern.
    Bash
    grep -r "pattern" .
    Fish
    grep -r "pattern" .
  • headOutput the first lines of a file.
    Bash
    head -n 10 file
    Fish
    head -n 10 file
  • historyShow previously run commands from the shell history.
    Bash
    history
    Fish
    history
  • killSend a signal to a process (typically to terminate it).
    Bash
    kill <pid>
    Fish
    kill <pid>
  • lsList directory contents.
    Bash
    ls -la
    Fish
    ls -la
  • mkdirCreate a new directory.
    Bash
    mkdir dir
    Fish
    mkdir dir
  • mvMove or rename files and directories.
    Bash
    mv source dest
    Fish
    mv source dest
  • pingSend ICMP echo requests to test reachability and round-trip latency.
    Bash
    ping example.com
    Fish
    ping example.com
  • psList a snapshot of currently running processes.
    Bash
    ps aux
    Fish
    ps aux
  • rmDelete files and directories.
    Bash
    rm file
    Fish
    rm file
  • sedStream editor for filtering and transforming text.
    Bash
    sed 's/old/new/g' file
    Fish
    sed 's/old/new/g' file
  • sortSort lines of text.
    Bash
    sort file
    Fish
    sort file
  • sshOpen a secure shell on a remote host or run a remote command.
    Bash
    ssh user@host
    Fish
    ssh user@host
  • tailOutput the last lines of a file.
    Bash
    tail -n 10 file
    Fish
    tail -n 10 file
  • tarBundle and unbundle files into a single archive (often combined with gzip / bzip2 / xz).
    Bash
    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.
    Bash
    top
    Fish
    top
  • touchCreate an empty file or update its modification time if it exists.
    Bash
    touch file.txt
    Fish
    touch file.txt
  • wcCount lines, words, or bytes.
    Bash
    wc -l file
    Fish
    wc -l file
  • wgetNon-interactive network downloader for HTTP, HTTPS, and FTP.
    Bash
    wget https://example.com/file.zip
    Fish
    wget https://example.com/file.zip
  • whichLocate an executable in PATH and print its full path.
    Bash
    which python3
    Fish
    which python3
  • xargsBuild and execute command lines from standard input.
    Bash
    find . -name '*.tmp' | xargs rm
    Fish
    find . -name '*.tmp' | xargs rm
  • zipPackage and compress files into a ZIP archive.
    Bash
    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 itself** is interpreting the script. Don't panic about portability if your shebangs are right.
  • `source ~/.bashrc` does not affect a running fish shell — fish has its own rc (`~/.config/fish/config.fish`). Environment variables set by bash login scripts may not be visible in a fish login.
  • `!!` (last command) and `!$` (last argument) — bash history-expansion shortcuts — don't exist in fish. Up-arrow + Alt-. are the fish equivalents.
  • `set -e` (exit on error) has no fish equivalent. Fish exits on errors in *most* cases automatically, but for guaranteed abort-on-failure scripts use explicit `or return` after critical commands.
  • Tab-completing on a fish-aware path expands variables like `$HOME` to their value before completion, which can be surprising — bash leaves `$HOME/` literal until you press Enter.

Full references

Other comparisons