Many Linux users treat their shell like wallpaper -- something that was there when they arrived and will still be there when they leave. That is a mistake.
The shell is the single most frequently invoked piece of software on any Linux system. It is the interpreter of your intent, the orchestrator of processes, and the connective tissue between you, the kernel, and every tool you depend on daily. It shapes how fast you move, how readable your automation is, how portable your scripts are, and how deeply you understand what Linux is actually doing beneath the surface.
This article goes well past the surface. We will examine what each shell is from the inside out -- how it manages processes, how its completion systems work, where its design philosophy originated, and exactly what you gain or lose by choosing one over the others.
A Brief History That Explains Everything
To understand Bash, Zsh, and Fish, you have to understand the conceptual war they are each fighting -- and that war has been going on since 1977.
Stephen Bourne wrote the original Bourne shell (sh) at Bell Labs as part of Unix Version 7, released in 1979. His goal was a scripting environment -- a language for writing portable automation. The syntax, famously odd by modern standards (fi closing if, esac closing case, done closing loops), was borrowed from ALGOL 68. Bourne was not optimizing for interactive convenience. He was building a programming language for system administrators.
That philosophy established the DNA of everything that followed. The Portable Operating System Interface (POSIX) standard, formalized by IEEE under standard 1003.1, codified the Bourne shell's behavior as the baseline every conformant shell must meet -- meaning POSIX-compliant scripts can be executed on any Unix or Unix-like operating system implementing the standard.
In 1989, Brian Fox built Bash for the GNU Project as a free software replacement for the proprietary Bourne shell. Fox announced the first beta in June 1989 and also authored the readline and history libraries. Readline is important and often overlooked: it is the library that gives Bash its command-line editing capability -- the Emacs-style keybindings that have since become default terminal behavior for an entire generation of Linux users. Readline's current maintainer is Chet Ramey of Case Western Reserve University, who has also maintained Bash itself since the early 1990s. Bash has since become the default interactive shell across the major Linux distributions.
In 1990, a Princeton student named Paul Falstad wrote the Z shell (Zsh). The name came from the login ID of Zhong Shao, then a teaching assistant at Princeton (later a professor at Yale). What made it remarkable was its ambition: Falstad folded features from Bash, the Korn shell, and the C shell into a single environment designed for interactive power users.
Fish arrived on February 13, 2005, created by Axel Liljencrantz. It made a decision the other shells refused to make: it deliberately broke from POSIX compatibility in exchange for a radically cleaner user experience. This was not carelessness. It was a philosophical statement that the POSIX shell standard had accumulated too much legacy to be a good foundation for interactive work.
Each of these shells was built to win a different argument. Bash was built for portability and scripting reliability. Zsh was built to extend that foundation with interactive power. Fish was built to prioritize the interactive experience above all else. Understanding that distinction is the prerequisite to making the right choice.
Under the Hood: How Shells Execute Commands
Before comparing features, you need to understand the execution model that all three shells share at the kernel level, because this is where their fundamental costs and capabilities live.
When you type a command and press Enter, every shell follows the same basic operating system interaction: fork and exec.
The shell calls fork(), which creates a near-identical copy of the shell process. The child copy then calls execve(), replacing its own memory space with the program you want to run. The parent shell calls wait() to pause until the child completes. Every external command you run -- ls, grep, awk, curl -- creates a new process. Shell builtins (cd, echo, export, source) avoid this overhead because they execute within the shell's own process.
This is why performance-conscious scripting favors builtins. A loop that calls grep ten thousand times is far slower than one that uses Bash's native string matching operators ([[ $var =~ pattern ]]). Every time you reach for an external tool unnecessarily, you pay a fork-exec cost.
Where the shells diverge in their execution models:
Subshells and pipelines. In Bash, the right side of a pipeline runs in a subshell. This creates the classic beginner trap where variables modified inside a while read loop are not visible after the loop ends. Zsh takes a different default: the last element of a pipeline runs in the current shell, not a subshell, which means variable assignments inside pipeline loops actually persist. Fish handles pipelines differently again: each segment runs in its own process, but Fish's scoping rules are designed to make this less surprising than Bash's behavior.
Variable expansion. In Bash and sh-compatible shells, unquoted variable expansion triggers word splitting and globbing. The infamous rm $filename bug -- where a filename containing spaces deletes multiple unintended files -- is a direct consequence of POSIX word-splitting rules. Zsh does not split unquoted variables by default. Fish eliminates the problem entirely with a completely different variable model where lists are first-class objects and word splitting on expansion does not occur.
Bash: The Universal Contract
Bash is the shell that will be present on every Linux system you ever touch. For systems administrators, security engineers, DevOps practitioners, and anyone who works across multiple environments, Bash is the lingua franca of Unix automation.
The Readline Foundation
Bash's interactive experience is built on the readline library. Readline provides the Emacs-style keybindings most Linux users consider "normal" terminal behavior: Ctrl+A to jump to the beginning of a line, Ctrl+E to the end, Ctrl+R for reverse history search, Alt+F and Alt+B for word movement. Readline also allows vi-mode (set -o vi in .bashrc), giving vi users a modal editing experience on the command line.
Command Lookup Order
When you type a name in Bash, it resolves that name through a strict hierarchy: aliases, then shell functions, then builtin commands, then external commands found via $PATH (cached in a hash table after first lookup).
The hash table is an important performance mechanism. After Bash finds an external command for the first time, it caches the full path. You can inspect it with hash -l and clear it with hash -r. This means the second invocation of python3 does not require searching every directory in your $PATH.
Process Substitution
Process substitution is one of Bash's most underused features. The syntax <(command) creates a named file descriptor or FIFO containing the output of command, which can then be used anywhere a filename is expected. The classic use case is comparing two command outputs without creating temporary files:
diff <(sort file1.txt) <(sort file2.txt)
Process substitution is a direct workaround for an architectural limitation Bash inherited from POSIX -- specifically, the subshell behavior of pipelines described above.
Startup Files and the Login Shell Distinction
Bash's startup file behavior confuses even experienced users. A login shell (SSH into a server, or a terminal launched with --login) reads /etc/profile, then the first existing file among ~/.bash_profile, ~/.bash_login, or ~/.profile. An interactive non-login shell (a terminal tab in a GUI) reads ~/.bashrc.
This split exists because login shells need to set up the full environment, while subsequent shells in the same session should be lightweight. The common pattern is to have ~/.bash_profile source ~/.bashrc, so both login and non-login shells get the same configuration. Failing to understand this is why aliases work in one terminal but not another.
Shellshock: An Architectural Security Lesson
On September 24, 2014, the Shellshock vulnerability (CVE-2014-6271) was publicly disclosed. The original bug was discovered by Stéphane Chazelas, a Unix and Linux administrator, who reported it to Bash maintainer Chet Ramey on September 12, 2014 before coordinated public disclosure twelve days later. The bug involved how Bash passed function definitions to subshells through environment variables, and had been present in the source code since August 1989 (version 1.03). Bash passes functions to child processes by encoding them as specially-named environment variables; the exploit worked by appending arbitrary commands after the function definition. CISA rated the primary CVE as the highest possible score under CVSS v2. Five additional related CVEs followed in the days after the initial disclosure.
Shellshock matters not just as a historical vulnerability but as an architectural lesson. The environment is a communication channel, and communication channels can be weaponized. The fact that this bug sat undetected for 25 years reveals something about every security assumption you make about shell scripts: attack surfaces that feel like plumbing are still attack surfaces. Test your own systems: env x='() {:;}; echo vulnerable' bash -c "echo test" -- any patched system should produce no output from the echo inside the variable.
Zsh: The Extensible Powerhouse
Zsh occupies a fascinating position: it is largely POSIX-compatible (your Bash scripts almost always run unmodified), yet it adds capabilities that fundamentally change what interactive shell work feels like.
The Zsh Line Editor (ZLE)
Where Bash uses the external readline library, Zsh uses its own built-in line editor called ZLE. This is not a cosmetic difference. ZLE is programmable in Zsh itself -- you can write shell functions that become editor widgets and bind them to keys. The shell's editing behavior is as customizable as its completion behavior.
ZLE exposes the $BUFFER, $CURSOR, $LBUFFER, and $RBUFFER parameters to shell functions, giving programmatic access to the current command line content and cursor position during editing. This is the mechanism behind the zsh-autosuggestions plugin, which reads ahead of the cursor and displays a grayed-out completion based on history.
The Completion System: compsys
Zsh's completion system, known as compsys, is one of the most sophisticated pieces of software in any shell ecosystem. It is written almost entirely in Zsh shell functions, making it both inspectable and extensible.
The key abstraction in compsys is the context. When you press Tab while typing git commit -, compsys does not simply list files. It evaluates a full context specification representing the position on the command line at which completion is requested. This context drives every completion decision. When you type kill , Zsh shows you running processes. When you type ssh , it reads your ~/.ssh/config for hostnames. When you type git checkout , it queries the git repository for branch names.
The zstyle command configures this system at the user level:
zstyle ':completion:*' menu select
zstyle ':completion:*:*:kill:*' menu yes select
zstyle ':completion:*:descriptions' format '%B%d%b'
When you run compinit in your .zshrc, Zsh scans directories in $fpath for completion functions (files beginning with _) and builds an autoload map. The actual function code is lazy-loaded the first time Tab is pressed for the relevant command.
Globbing: Pattern Matching Extended
Zsh's extended globbing is one of the most underappreciated shell features in widespread use. Where Bash requires shopt -s globstar to enable ** recursive globbing, Zsh has it by default with additional qualifier syntax.
# Log files modified in the last 7 days ls **/*.log(m-7) # Only directories ls *(/) # Only executable regular files ls *(.x) # Zero-byte regular files ls *(.L0)
This is pattern matching and file qualification in a single expression, with no external tool required. The equivalent in Bash often involves find, which means another fork-exec cycle and a different syntax entirely.
The Oh My Zsh Question
Oh My Zsh is not part of Zsh. It is a configuration framework that manages plugins, themes, and the .zshrc file. It is enormously popular because it makes Zsh's power accessible without requiring users to read hundreds of pages of documentation.
Oh My Zsh can make Zsh significantly slower to start. Loading 50 plugins at shell startup can push startup time from a few milliseconds to over a second. Performance-conscious Zsh users use zinit or antibody for lazy plugin loading, or manually curate a minimal .zshrc. Shell startup time is not vanity -- every CI/CD job that spawns a new shell pays this cost.
Fish: The Shell That Refuses the Legacy
Fish (Friendly Interactive Shell) is the most opinionated of the three. It abandoned POSIX compatibility in exchange for a design that is internally consistent, learnable, and immediately pleasant to use.
The Non-POSIX Decision
POSIX word splitting -- where unquoted variables get split on whitespace -- is the source of countless shell scripting bugs. The quoting rules required to write safe Bash scripts are complex enough that they have spawned multiple dedicated websites. Fish simply opted out. Its variable model is different: variables are always lists, word splitting on expansion does not happen, and the quoting rules are dramatically simpler. The trade-off is real: Fish scripts do not run in Bash, and Bash scripts do not run in Fish without modification.
The Rust Rewrite: Architecture Catching Up to Ambition
Fish 4.0 represents one of the most ambitious rewrites in shell history. Fish was originally implemented in C, then ported to C++, and has now been rewritten to almost 100% pure Rust. According to the Fish development team's own post-mortem, the initial pull request was opened on January 28, 2023, merged on February 19, 2023, with the last C++ code removed in January 2024. The final stable release of Fish 4.0 shipped on February 27, 2025, representing nearly two years of development across more than 2,600 commits by over 200 contributors. The codebase grew from approximately 57,000 lines of C++ to roughly 75,000 lines of Rust.
The motivation was not surface-level performance. Fish uses threads for its autosuggestions and syntax highlighting, and the long-term goal is to add concurrency to the language itself. Rust's ownership model and Send/Sync trait system make concurrent code correct by construction in ways that C++ cannot guarantee. The Fish team prototyped fully multithreaded execution in C++, but found it too easy to accidentally share objects across threads without detection until runtime -- a class of bug Rust's type system makes impossible at compile time. The practical downstream benefit: future releases may support asynchronous prompts and non-blocking completions that are architecturally impossible in the current design.
Autosuggestions: The Feature That Changes Everything
As you type, Fish displays a grayed-out completion inline, showing you the most likely completion based on your command history and valid filesystem paths. You accept it with the right arrow key or Ctrl+F.
The experience is cognitively different from tab completion. Tab completion is pull: you request options and choose. Autosuggestion is push: the shell offers its best guess continuously as context shifts. For workflows with repetitive command patterns -- deploying the same services, connecting to the same servers, running the same build commands -- autosuggestion reduces keystrokes dramatically. The technical implementation runs in a background thread (now in Rust), meaning it never blocks your input.
Syntax Highlighting in Real Time
Fish highlights command syntax as you type, before you press Enter. Valid commands appear in one color. Invalid commands (misspellings, commands not in $PATH) appear in red. Strings appear differently from flags. File paths are underlined if they exist.
This is not aesthetic. It catches typos before they become errors. It shows you immediately when you have misspelled a command, mistyped a path, or forgotten a flag. The feedback loop is tightened from "execute and read error message" to "see the problem as you type."
The Variable Model
Fish's variable model is the primary adjustment for users coming from Bash or Zsh. In Bash, unquoted $name expansion can trigger word splitting. In Fish, variables are always lists, and expansion never word-splits:
# Variables as lists -- no word splitting on expansion set name "hello world" echo $name # Outputs: hello world set parts a b c for item in $parts; echo $item; end # Clean iteration
Universal variables are a uniquely Fish concept: variables declared with set -U are shared instantaneously across all running Fish sessions and persist through shell restarts without any file editing. This is qualitatively different from editing .bashrc and opening new terminals.
Fish's Biggest Limitation
POSIX compatibility matters more than Fish users often admit. Many development tools, environment managers, and frameworks provide shell integration scripts that assume POSIX syntax. nvm, pyenv, RVM, and others provide setup instructions that are Bash/Zsh-specific. Fish users must find community-maintained wrappers or manually translate the setup logic. Some work tools are not Fish compatible, requiring a switch back to Bash. For local development workflows this is manageable. For infrastructure engineers who live on remote servers, it is a significant friction point.
Feature Comparison: The Details That Matter
History Management
Bash writes to ~/.bash_history when a session ends, not during. With HISTFILE, HISTSIZE, HISTFILESIZE configured, and shopt -s histappend to prevent overwriting, Bash history management becomes reliable.
Zsh offers shared history through setopt SHARE_HISTORY, which writes commands to the history file immediately and reads new entries from other sessions as you navigate. setopt HIST_IGNORE_ALL_DUPS prevents duplicate entries from cluttering history.
Fish's history is fully automatic and directory-sensitive. Type any substring and press the up arrow -- Fish shows matching history entries filtered by context. No configuration required.
Scripting Syntax: The Key Differences
Command substitution, conditionals, and function definitions each differ between the three shells in ways that have real automation consequences:
# Bash / Zsh today=$(date +%Y-%m-%d) echo "Today is $today" # Fish -- parentheses without the dollar sign set today (date +%Y-%m-%d) echo "Today is $today"
# Bash if [[ -f "$file" ]]; then echo "exists" fi # Fish -- end closes all block structures if test -f $file echo "exists" end
# Bash greet() { echo "Hello, $1" } # Fish -- $argv replaces positional parameters function greet echo "Hello, $argv[1]" end
Performance: What the Numbers Mean
Shell startup time is the most commonly cited performance metric, but it requires context. A stock Bash installation starts in roughly 2--5 milliseconds. Zsh without configuration is similar. Fish without configuration starts in roughly 10--25 milliseconds due to its initialization work. A heavily configured Zsh with Oh My Zsh and multiple plugins might take 0.5 to 1 second to start.
For interactive use, startup time below 100ms is imperceptible. The difference between a 10ms Fish startup and a 3ms Bash startup is not something a human notices when opening a terminal tab.
For scripting, startup time compounds. A Bash script that spawns 1000 subshells accumulates startup costs 1000 times. This is why system scripts use POSIX-compatible shells (often dash, not even Bash) -- dash starts in under 1ms and executes POSIX scripts significantly faster than Bash.
Where Zsh and Fish genuinely excel on performance is interactive responsiveness during heavy completion operations. Fish's threaded autosuggestion engine, particularly after the Rust rewrite, never blocks input even during expensive filesystem scans. Zsh's compsys, with proper lazy loading, handles large completion sets without perceptible latency.
The Cybersecurity Perspective
Shell choice has non-trivial security implications that are rarely discussed in mainstream comparisons.
Script injection vectors. Bash's eval and the way it processes the environment at startup (see Shellshock above) create attack surfaces that more restrictive shells lack. Fish's completely different variable model means that many classic shell injection patterns do not apply to Fish scripts -- but this should not be mistaken for Fish being inherently "more secure," since most production automation does not run in Fish.
Debugging and auditability. bash -x and bash -n (trace mode and syntax-check mode) are invaluable for auditing scripts. Every line executed in trace mode is printed to stderr, prefixed with +. This is the mechanism security auditors use to confirm script behavior in regulated environments. Zsh has equivalent functionality. Fish's error messages are considerably more human-readable but its debugging toolchain -- no built-in trace mode equivalent -- is less mature for formal audit use.
Environment hygiene. In Bash and Zsh, functions can be exported to the environment (export -f function_name) and become visible to child processes. This was the exact mechanism Shellshock (CVE-2014-6271) exploited. Fish does not support exporting functions to the environment at all, which represents a meaningfully smaller attack surface for that class of attack.
Principle of least surprise in automation. Security scripts running in cron or systemd drop into /bin/sh, which on Debian and Ubuntu is dash, not Bash. A security script tested interactively in Bash that uses Bash-specific syntax may fail silently when run as a scheduled job -- with no error message, no alert, and no execution of the intended security logic. This is one of the most common failure modes in production security automation.
The hashbang line at the top of every script (#!/bin/bash, #!/bin/sh, #!/usr/bin/env fish) determines the interpreter, not your current shell. System scripts, cron jobs, and init scripts typically run under /bin/sh, which is dash on Debian/Ubuntu systems. Understanding that your interactive shell and the script execution shell may be different interpreters is essential for writing secure, reliable automation. Use shellcheck -- a widely used static analysis tool for shell scripts -- to catch portability bugs before they become silent failures in production.
Choosing Your Shell: A Decision Framework
The question is not which shell is best in the abstract. It is which shell is best for your specific context.
Choose Bash if your work involves production infrastructure, servers you do not own, or scripts that others will maintain. You work in regulated environments where reproducibility and auditability of tooling matter -- Bash's bash -x trace mode and well-understood execution model are what security auditors and compliance reviewers expect to see. You administer remote systems over SSH where you cannot control the installed shells. You are writing automation that integrates with CI/CD pipelines, systemd units, cron, or init scripts -- all of which drop to /bin/sh and need portable syntax. You are learning Linux fundamentals and want skills that transfer without friction to any environment you will ever touch. Importantly, Bash is also the right answer if your scripts will be read or modified by colleagues who may not share your shell preference -- Bash is the lingua franca and unfamiliar Zsh-isms or Fish syntax create real maintenance cost.
One underappreciated reason to stay with Bash in security-sensitive contexts: its behavior is among the most heavily audited of any shell. The quirks are documented, the failure modes are known, and tooling like shellcheck is mature specifically for Bash. Choosing a less-common shell in production security automation means fewer people can immediately reason about your scripts.
Choose Zsh if you want Bash-compatible scripting with dramatically better interactive features, and you are prepared to invest real configuration time to unlock them. The right use of Zsh goes beyond installing Oh My Zsh and calling it done. The payoff is in compsys customization: building or adapting completion functions for your internal tools, writing ZLE widgets that automate repetitive command construction, and using Zsh's glob qualifiers to replace find invocations with single-expression filesystem queries. Zsh's pipeline behavior change -- where the last pipeline segment runs in the current shell, not a subshell -- removes a class of variable-scoping bugs that regularly trip Bash users writing complex data pipelines. If you are on macOS doing development work, Zsh is the system default since Catalina and the path of least resistance for maintaining parity between your local environment and your team. You write scripts that need to be read by others who know Bash -- Zsh's compatibility layer means this remains practical in ways that Fish's non-POSIX syntax never will be.
Choose Fish if your primary concern is your own daily terminal productivity, and you have a clear plan for handling POSIX scripting outside Fish. That plan matters -- it should not be vague. It means: all scripts get explicit #!/bin/bash hashbangs, all environment manager setup scripts (nvm, pyenv, rvm, conda) are handled through community Fish wrappers or evaluated and replaced, and you have a strategy for onboarding teammates who will encounter Fish-only configuration in your dotfiles. Fish is the right choice for developers who spend most of their time on their own machines doing iterative local development, where the cognitive return on fewer keystrokes and inline syntax feedback compounds over thousands of daily interactions. Fish is also the most defensible choice for someone new to the Linux command line who is not yet writing production automation -- the reduced quoting complexity and immediate error feedback genuinely lower the barrier to understanding what commands are doing. The tradeoff is structural: you are maintaining two mental models and two skill sets for as long as you use Fish, one for interactive work and one for every script that runs anywhere else.
The Deeper Lesson
The shell debate is really a debate about two different problems that have been conflated since 1977.
Problem one: executing programs and automating sequences of operations. This is the scripting problem. POSIX was built to solve it, at the cost of user-hostile syntax and accumulated design decisions that were reasonable in 1977 but are confusing today.
Problem two: providing a fast, intuitive interactive interface for daily work. This is the ergonomics problem. Fish was built to solve it, at the cost of compatibility with the solution to problem one.
Bash and Zsh are both trying to solve both problems simultaneously, with different points of compromise. Bash prioritizes problem one. Zsh tilts toward problem two without abandoning problem one.
The mature conclusion is not to pick one and commit forever. Many experienced Linux practitioners use Fish as their interactive shell and write automation scripts in Bash, launched explicitly with #!/bin/bash. The shell you type in and the shell your scripts run in do not have to be the same. That is not inconsistency -- it is using the right tool for each distinct problem.
The shell is not wallpaper. It is a lens through which you interact with a machine. Choosing that lens thoughtfully -- and understanding what is behind it -- is what separates someone who uses Linux from someone who understands it.
Quick Reference: Key Differences at a Glance
POSIX compliance: Bash (mostly yes), Zsh (mostly yes, not fully), Fish (deliberately no)
Default completion behavior: Bash (basic, requires configuration), Zsh (advanced compsys, requires compinit), Fish (full out of the box)
Syntax highlighting while typing: Bash (no, requires external tools), Zsh (via plugin), Fish (built in)
Autosuggestion from history: Bash (no), Zsh (via plugin), Fish (built in)
Script portability: Bash (runs anywhere), Zsh (runs most places), Fish (Fish only)
Variable word splitting: Bash (yes, must quote defensively), Zsh (configurable, off by default), Fish (never)
Pipeline subshell behavior: Bash (right side in subshell), Zsh (last segment in current shell), Fish (all segments in own processes)
Configuration complexity: Bash (low default, moderate ceiling), Zsh (high ceiling, steep curve), Fish (low, sensible defaults)
Primary use case strength: Bash (portable automation), Zsh (interactive power with POSIX compatibility), Fish (frictionless interactive use)
Sources and Further Reading
The factual claims in this article are verifiable through primary sources. The following are recommended for anyone who wants to go deeper than this overview allows:
On Bash and readline: Brian Fox (Wikipedia) -- authorship of Bash and readline confirmed. Two-Bit History: Things You Didn't Know About GNU Readline -- Chet Ramey's transition to lead maintainer, Readline's POSIX origins. The official GNU Bash Reference Manual covers startup files, process substitution, and the hash table in precise detail.
On Zsh: Z shell (Wikipedia) -- Falstad's Princeton origins, Zhong Shao naming, macOS Catalina adoption. The Zsh manual is the authoritative reference for compsys and ZLE.
On Fish 4.0 and the Rust rewrite: Fish 4.0: The Fish of Theseus -- the official Fish team post-mortem on the C++ to Rust transition, covering timeline, technical rationale, and lessons learned. fish-shell 4.0b1 beta announcement (December 17, 2024). Fish 4.0 stable release: February 27, 2025.
On Shellshock: Shellshock (Wikipedia) -- discoverer Stéphane Chazelas, coordinated disclosure timeline, CVE identifiers. CISA Advisory for CVE-2014-6271 -- the official US government security advisory.
How to Choose the Right Linux Shell for Your Workflow
Step 1: Identify your primary use case: scripting or interactive work
Determine whether your daily shell use is weighted toward writing portable automation scripts or toward interactive terminal work. If you primarily write scripts that run on servers, in cron jobs, or across machines you do not control, the scripting portability of Bash is the decisive factor. If your primary need is a faster, more ergonomic daily terminal experience for local development, interactive features become the priority.
Step 2: Evaluate your environment constraints
Check whether you work primarily on systems you own or on remote servers you administer. On remote servers and shared infrastructure, Bash will almost always be present as the default shell. Zsh is not installed by default on most Linux distributions. Fish is rarely present without manual installation. If you frequently SSH into machines you do not control, choosing Bash as your primary shell minimizes friction. If you work primarily on your own machine or in containers you build yourself, shell availability is not a constraint.
Step 3: Match shell choice to your workflow and commit to a hashbang strategy
Choose Bash if portability and auditability are your priorities -- particularly if your scripts run in regulated environments, CI/CD pipelines, systemd units, or anywhere the execution shell is not under your control. Bash's bash -x trace mode and well-documented failure modes make it the standard for auditable production automation. Choose Zsh if you want Bash-compatible scripting with a significantly better interactive experience and are willing to invest in configuration -- the real returns come from compsys customization, ZLE widgets, and glob qualifiers, not just a different prompt. Choose Fish if interactive productivity on your own machine is your top priority and you have a concrete plan for handling POSIX scripting separately -- that means all scripts carry explicit hashbangs, all environment manager integrations are handled through Fish-compatible wrappers, and colleagues who read your dotfiles understand the two-shell approach. Regardless of which shell you use interactively, always set an explicit hashbang line at the top of every script -- such as #!/bin/bash or #!/bin/sh -- to ensure the script runs under the intended interpreter, not whatever shell happens to be your current interactive session.
Frequently Asked Questions
Should I use Bash or Zsh for shell scripting?
Use Bash for shell scripting whenever your scripts will run on servers, in cron jobs, or in environments you do not control. Bash is available on virtually every Linux system and its scripting behavior is well-understood and auditable. Zsh is largely compatible with Bash scripting but is not installed by default on all Linux distributions, making Bash the safer choice for portable automation.
Is Fish shell POSIX-compliant?
No. Fish deliberately breaks from POSIX compatibility. This was an intentional design decision by its creator Axel Liljencrantz, based on the view that the POSIX shell standard had accumulated too much legacy to serve as a good foundation for interactive work. Fish scripts will not run in other shells, and Fish cannot directly execute standard sh or Bash scripts without invoking those interpreters explicitly.
What was the Shellshock vulnerability and which shells were affected?
Shellshock (CVE-2014-6271) was a critical vulnerability discovered in Bash in 2014. It exploited the way Bash processes environment variables at startup: Bash allowed functions to be exported to the environment, but it also executed any trailing commands appended after the function definition. This meant a remote attacker who could control an environment variable -- such as through a CGI web request -- could execute arbitrary commands. Zsh and Fish were not affected because they do not share this function-export-via-environment mechanism.