Skip to content
Go back

fish vs zsh vs nushell in 2026

By SumGuy 10 min read
fish vs zsh vs nushell in 2026

Your Shell Is Probably Fine. It’s Also Probably Holding You Back.

Bash ships with everything. It works everywhere. It’s also a 1989 design that hasn’t fundamentally changed since your parents were listening to Guns N’ Roses. You’ve been tolerating it, and you deserve better.

By 2026, three shells have genuinely earned the “worth switching to” conversation: fish (Friendly Interactive Shell, now Rust-based after the 4.x rewrite), zsh (the old reliable that macOS made mainstream), and nushell (the structured-data upstart that makes grep | awk | sed pipelines feel prehistoric). They each solve the shell problem differently, and picking the wrong one for how you actually work will frustrate you into crawling back to bash.

Let’s settle this.


The Quick Lay of the Land

fish 4.xzsh 5.9nushell 0.99+
Written inRust (since 4.0)CRust
POSIX compatibleNoMostlyNo
Script compat (bash)NoYes (mostly)No
Default autocompleteYes, from man pagesPlugins neededYes, external completers
Syntax highlightingBuilt-inPluginBuilt-in
Config stylefish functions + fish_config.zshrc + plugin managerenv.nu + config.nu
Pipe data modelByte streamsByte streamsStructured objects
Startup speedFastSlow without lazy loadingFastest

fish 4.x — The Shell That Doesn’t Make You Read the Manual

fish made a big bet: sane defaults over backward compatibility. You install it, and it already works like you spent a weekend configuring it. Autosuggestions show up in grey as you type, pulling from history and completions. Syntax is highlighted live — typo a command and it’s red before you even hit Enter. Tab completion works out of the box without sourcing anything.

The 4.x rewrite in Rust dropped the C++ codebase and made startup noticeably snappier. It also cleaned up some long-standing edge cases in completion generation. fish auto-parses man pages to generate completions for commands it doesn’t explicitly know about, which sounds gimmicky until you’re completing flags for some obscure backup tool you installed yesterday.

Terminal window
# fish: set a variable (not POSIX, but readable)
set -x MY_VAR "hello"
# Conditionals look like a real language
if test -f ~/.config/fish/config.fish
echo "config exists"
end
# Functions are first-class
function greet --description "Greet someone"
echo "Hey $argv[1], welcome to the good shell"
end

Configuration is done via fish_config, a web UI that runs a local server so you can pick your prompt, colors, and functions in a browser. Unironically useful. Functions you define get saved as individual files in ~/.config/fish/functions/, which means your config is already modular without doing anything clever.

Here’s the thing though: fish doesn’t run bash scripts. Not even close. Its syntax is intentionally incompatible. If you paste a bash one-liner from Stack Overflow, there’s a decent chance it silently does something wrong or just errors out. The fish_indent tool and the shell itself will tell you, but you need to know going in that fish is a lifestyle choice, not a drop-in upgrade.

fish is for you if:


zsh 5.9 — The Swiss Army Knife That Requires Assembly

zsh has been the power user default for a decade. macOS shipping it as the default shell in Catalina didn’t hurt adoption. It’s POSIX-ish (close enough that most bash scripts run fine), has the deepest completion ecosystem in any shell, and its plugin ecosystem via Oh My Zsh, zinit, or antidote gives you essentially anything you want.

The tradeoff is that a fresh zsh install is… bash but with slightly better tab completion. To get the fish-like experience you have to stack plugins: zsh-autosuggestions, zsh-syntax-highlighting, fzf integration, starship for the prompt. That’s not a knock — it’s a feature for people who want control — but it means your shell startup time can spiral if you’re not careful.

Terminal window
# .zshrc basics with antidote
source ~/.antidote/antidote.zsh
antidote load
# Load completions properly
autoload -Uz compinit
compinit
# History that doesn't suck
HISTSIZE=100000
SAVEHIST=100000
setopt HIST_IGNORE_DUPS
setopt SHARE_HISTORY
# zsh-specific glob expansion
ls **/*.log # recursive glob, built-in

Lazy loading is how you keep zsh fast. With zinit or antidote you can defer plugins until first use:

Terminal window
# antidote bundle file
zsh-users/zsh-autosuggestions
zsh-users/zsh-syntax-highlighting
zsh-users/zsh-completions

The curated completions are legitimately impressive — kubectl, helm, aws, docker, git, all with flag and argument awareness. fish auto-generates from man pages (hit or miss), but zsh completions are hand-tuned and usually smarter.

History search with atuin has become the standard move in 2026. It syncs history across machines, gives you per-directory search, and drops into an fzf-like UI on Ctrl+R.

Terminal window
# Install atuin and hook it into zsh
eval "$(atuin init zsh)"

zsh is for you if:


nushell 0.99+ — The Shell That Actually Reads the Data

Nushell is not a bash replacement. It’s a different thing entirely. Pipes in nushell don’t carry bytes — they carry structured data: tables, records, lists, JSON. Every built-in command (ls, ps, df, sys) returns a typed object, not a string you then need to parse.

Terminal window
# nushell: ls returns a table, not strings
ls | where size > 1mb | sort-by modified -r | first 10
# ps returns structured records
ps | where name =~ "python" | select pid name cpu mem
# Parse JSON inline, no jq needed
open package.json | get dependencies | transpose name version
# HTTP requests return structured data
http get https://api.github.com/repos/nushell/nushell | get stargazers_count

That ls | where size > 1mb is doing actual numeric comparison on file sizes. In bash, you’d be writing find . -size +1M with its completely different syntax. In nushell, it’s just data filtering. The SQL-like feel (where, select, sort-by, group-by) transfers immediately if you’ve ever touched a database.

Configuration lives in two files: env.nu for environment setup and config.nu for shell behavior. The syntax is nushell’s own — not bash, not POSIX.

Terminal window
# env.nu — environment config
$env.EDITOR = "nvim"
$env.PATH = ($env.PATH | split row (char esep) | prepend "~/.cargo/bin")
# config.nu — shell behavior
$env.config = {
show_banner: false
history: {
max_size: 100_000
sync_on_enter: true
file_format: "sqlite"
}
completions: {
case_sensitive: false
algorithm: "fuzzy"
}
}

External completions hook in cleanly — pipe your completions through fzf or carapace for rich fuzzy completion on external commands.

Terminal window
# carapace as external completer in config.nu
let carapace_completer = {|spans|
carapace $spans.0 nushell ...$spans | from json
}

The catch? Scripts. Nushell scripting is genuinely good, but it’s its own language. Nothing bash/zsh from the internet runs here. You’re not copy-pasting install scripts into nushell — you run those in bash (or bash -c "..." from within nushell). And its interactive history search is built-in, which is clean, but if you want atuin-level sync, you’re doing a bit more wiring.

Nushell 0.99+ is also the most stable the project has ever been. The earlier rapid API changes that burned early adopters have largely settled. Plugin API is stable. The stdlib has grown up.

nushell is for you if:


The Battles That Actually Matter

Startup Speed

Fresh installs, no plugins: nushell wins. fish is close. A stock zsh with a full Oh My Zsh setup can hit 400–800ms cold start before lazy loading. With zinit/antidote lazy loading you can get it under 100ms, but that’s configuration work.

Terminal window
# Test your shell startup time
time zsh -i -c exit
time fish -i -c exit
time nu -c ""

Compatibility

Honestly the most underrated consideration. If you SSH into servers and work in remote bash environments, zsh’s POSIX-ness is irrelevant — you’re running bash there anyway. But if you have a fat ~/.zshrc worth of functions and aliases you’ve built up over years, fish and nushell both require you to rewrite them. That’s a real cost.

zsh can source most bash scripts directly. source my_script.sh usually works. fish cannot. nushell cannot.

Data Wrangling at the Prompt

nushell wins this unconditionally. Comparing fish or zsh to nushell for JSON/table manipulation is like comparing grep to jq. They’re not the same tool class.

Terminal window
# nushell: Docker container status as a table
docker ps --format json | lines | each { from json } | select Names Status Ports

Prompt

All three shells work with starship.rs, which is the obvious answer in 2026. Configure once, carry it across all three. If you’re switching shells, keep starship across the migration.

# ~/.config/starship.toml — works in fish, zsh, nushell
[character]
success_symbol = "[➜](bold green)"
error_symbol = "[➜](bold red)"
[directory]
truncation_length = 3

Migration Reality Check

Bash → fish: Easiest. You stop caring about POSIX syntax, you gain autosuggestions and highlighting immediately, and for day-to-day interactive use the UX jump is the biggest of any option. The first week you’ll look up “how to do X in fish” instead of “how to do X in bash” — budget for that.

Bash → zsh: Lowest friction. Most bash scripts run. You’re mostly adding plugins on top of familiar syntax. You can migrate gradually: start with Oh My Zsh, tune from there.

Bash → nushell: Highest conceptual lift, biggest payoff if your work involves structured data. Plan to keep bash around for compatibility scripts. nushell as your interactive shell, bash for automation — that’s a reasonable combo.


The Bottom Line

Use fish if you want the best out-of-the-box interactive experience and you’re not dragging a suitcase full of bash scripts. Install it this afternoon, fish_config in the browser, done. It won’t make you smarter but it’ll make you faster.

Use zsh if you’re on macOS and don’t want to fight your system, or if you have years of shell muscle memory and scripts you’re not rewriting. Spend a weekend with antidote + atuin + starship and it’ll match fish’s UX with more configurability and zero compatibility headaches.

Use nushell if you spend meaningful time massaging JSON, querying APIs, or processing structured output at the prompt. It genuinely changes how you think about the command line. Run bash externally for the compatibility work; use nushell for the rest.

And if you can’t decide: pick fish. The defaults are good enough that you’ll actually use it instead of spending three weekends configuring something and then reverting to bash anyway.


Share this post on:

Send a Webmention

Written about this post on your own site? Send a webmention and it'll show up above once verified.


Next Post
Boundary vs Teleport

Discussion

Powered by Garrul . Sign in with GitHub or Google, or post anonymously.

Related Posts