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.x | zsh 5.9 | nushell 0.99+ | |
|---|---|---|---|
| Written in | Rust (since 4.0) | C | Rust |
| POSIX compatible | No | Mostly | No |
| Script compat (bash) | No | Yes (mostly) | No |
| Default autocomplete | Yes, from man pages | Plugins needed | Yes, external completers |
| Syntax highlighting | Built-in | Plugin | Built-in |
| Config style | fish functions + fish_config | .zshrc + plugin manager | env.nu + config.nu |
| Pipe data model | Byte streams | Byte streams | Structured objects |
| Startup speed | Fast | Slow without lazy loading | Fastest |
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.
# fish: set a variable (not POSIX, but readable)set -x MY_VAR "hello"
# Conditionals look like a real languageif test -f ~/.config/fish/config.fish echo "config exists"end
# Functions are first-classfunction greet --description "Greet someone" echo "Hey $argv[1], welcome to the good shell"endConfiguration 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:
- You want something that works immediately with zero plugin archaeology
- You’re not maintaining a pile of bash scripts
- You bounce between languages and don’t want shell to be yet another thing to configure
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.
# .zshrc basics with antidotesource ~/.antidote/antidote.zshantidote load
# Load completions properlyautoload -Uz compinitcompinit
# History that doesn't suckHISTSIZE=100000SAVEHIST=100000setopt HIST_IGNORE_DUPSsetopt SHARE_HISTORY
# zsh-specific glob expansionls **/*.log # recursive glob, built-inLazy loading is how you keep zsh fast. With zinit or antidote you can defer plugins until first use:
# antidote bundle filezsh-users/zsh-autosuggestionszsh-users/zsh-syntax-highlightingzsh-users/zsh-completionsThe 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.
# Install atuin and hook it into zsheval "$(atuin init zsh)"zsh is for you if:
- You have existing bash/zsh scripts you’re not rewriting
- You want the richest completion ecosystem available
- You’re on macOS and want to stay close to the system defaults
- You like configuring things exactly how you want them
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.
# nushell: ls returns a table, not stringsls | where size > 1mb | sort-by modified -r | first 10
# ps returns structured recordsps | where name =~ "python" | select pid name cpu mem
# Parse JSON inline, no jq neededopen package.json | get dependencies | transpose name version
# HTTP requests return structured datahttp get https://api.github.com/repos/nushell/nushell | get stargazers_countThat 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.
# 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.
# carapace as external completer in config.nulet 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:
- You work heavily with JSON, CSV, TOML, or API responses at the command line
- You’re done with
grep | awk | cut | sedpipelines for structured data - You’re comfortable running bash externally for compatibility scripts
- You want a scripting language that actually makes sense
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.
# Test your shell startup timetime zsh -i -c exittime fish -i -c exittime 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.
# nushell: Docker container status as a tabledocker ps --format json | lines | each { from json } | select Names Status PortsPrompt
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 = 3Migration 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.