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
BashPOSIX-extended. The de facto target of "shell scripts" on most systems.
FishDeliberately 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
BashArrays with `arr=(a b c)`. 0-indexed. `${arr[@]}` for all elements, `${#arr[@]}` for length.
FishEvery 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.
FishNo `$((...))`. 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.
FishNo 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'Fishalias ll='ls -la' - awkPattern scanning and processing language for structured text.Bash
awk '{print $1}' fileFishawk '{print $1}' file - catPrint file contents to standard output.Bash
cat file.txtFishcat file.txt - chmodChange file mode bits (read / write / execute permissions) on Unix files.Bash
chmod 755 fileFishchmod 755 file - cpCopy files and directories.Bash
cp source destFishcp source dest - curlTransfer data from or to a server over HTTP, HTTPS, FTP, and many other protocols.Bash
curl https://example.comFishcurl https://example.com - cutExtract sections (fields or characters) from each line.Bash
cut -d',' -f1 file.csvFishcut -d',' -f1 file.csv - echoPrint arguments to standard output, separated by spaces, followed by a newline.Bash
echo "hello world"Fishecho "hello world" - exportSet an environment variable and mark it for export to child processes.Bash
export NAME=valueFishset -gx NAME value - findLocate files by name, size, time, or other attributes.Bash
find . -name "*.log"Fishfind . -name "*.log" - grepSearch file contents for a pattern.Bash
grep -r "pattern" .Fishgrep -r "pattern" . - headOutput the first lines of a file.Bash
head -n 10 fileFishhead -n 10 file - historyShow previously run commands from the shell history.Bash
historyFishhistory - killSend a signal to a process (typically to terminate it).Bash
kill <pid>Fishkill <pid> - lsList directory contents.Bash
ls -laFishls -la - mkdirCreate a new directory.Bash
mkdir dirFishmkdir dir - mvMove or rename files and directories.Bash
mv source destFishmv source dest - pingSend ICMP echo requests to test reachability and round-trip latency.Bash
ping example.comFishping example.com - psList a snapshot of currently running processes.Bash
ps auxFishps aux - rmDelete files and directories.Bash
rm fileFishrm file - sedStream editor for filtering and transforming text.Bash
sed 's/old/new/g' fileFishsed 's/old/new/g' file - sortSort lines of text.Bash
sort fileFishsort file - sshOpen a secure shell on a remote host or run a remote command.Bash
ssh user@hostFishssh user@host - tailOutput the last lines of a file.Bash
tail -n 10 fileFishtail -n 10 file - tarBundle and unbundle files into a single archive (often combined with gzip / bzip2 / xz).Bash
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.Bash
topFishtop - touchCreate an empty file or update its modification time if it exists.Bash
touch file.txtFishtouch file.txt - wcCount lines, words, or bytes.Bash
wc -l fileFishwc -l file - wgetNon-interactive network downloader for HTTP, HTTPS, and FTP.Bash
wget https://example.com/file.zipFishwget https://example.com/file.zip - whichLocate an executable in PATH and print its full path.Bash
which python3Fishwhich python3 - xargsBuild and execute command lines from standard input.Bash
find . -name '*.tmp' | xargs rmFishfind . -name '*.tmp' | xargs rm - zipPackage and compress files into a ZIP archive.Bash
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 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.