sh, bash, and zsh are all Unix shells but differ in scope and features. sh (the Bourne shell, or a POSIX-compliant equivalent like dash) is the minimal, standardized shell used for portable scripts. bash (Bourne Again SHell) extends sh with interactive features like command history, tab completion, and additional scripting capabilities, and is the default shell on most Linux distributions. zsh (Z Shell) extends bash further with more advanced interactive features — better tab completion, spell correction, and extensive customization — and is popular among power users and the default shell on macOS since 2019.
Three Shells, One Lineage
sh, bash, and zsh are all part of the same family tree of Unix shells, each building on the ideas of its predecessors while adding new capabilities. Understanding their relationship — what each one is, how they differ, and where compatibility boundaries lie — resolves a common source of confusion: why does a script that works in one shell fail in another? Why do some systems use sh and others bash? Why do so many developers switch to zsh?
The short version: sh represents the original, minimal Unix shell specification — a small, standardized, portable core. bash is GNU’s popular reimplementation and extension of sh, adding substantial interactive and scripting features while remaining largely backward-compatible. zsh takes bash’s capabilities further still, adding even more powerful interactive features aimed at making the shell experience as smooth and customizable as possible.
This article traces each shell’s history and purpose, details the concrete feature and syntax differences, explains the compatibility issues that arise when scripts assume the wrong shell, and helps you decide which shell to use for interactive work versus scripting.
sh: The Original Standard
What “sh” Actually Refers To
“sh” is both a specific historical shell (the Bourne shell, written by Stephen Bourne at Bell Labs in 1979) and, on modern systems, a generic term for “whatever shell fulfills the POSIX sh specification” — which is often not literally the original Bourne shell binary but a POSIX-compliant substitute.
On most Linux systems today, /bin/sh is a symbolic link to another shell running in a compatibility mode:
$ ls -la /bin/sh
lrwxrwxrwx 1 root root 4 Jan 15 2023 /bin/sh -> dash
On Ubuntu and Debian, /bin/sh points to dash (Debian Almquist Shell) — a small, fast, POSIX-compliant shell optimized for script execution speed rather than interactive use. On some other systems, /bin/sh might point to bash running in POSIX-compatibility mode (bash --posix), which disables bash-specific extensions to behave more like standard sh.
Why sh Is Minimal by Design
The POSIX sh standard defines a baseline of shell functionality that any compliant shell must support — basic variables, conditionals (if/then/else), loops (for, while), functions, and I/O redirection. It deliberately excludes many convenience features that later shells added, because the goal of POSIX sh is portability: a script written strictly for POSIX sh should run identically on any Unix-like system, regardless of which specific shell provides /bin/sh on that system.
What sh/dash Lacks Compared to bash
- No arrays
- No
[[ ]]extended test syntax (only[ ], the older test command) - No
==string comparison in[ ](must use single=) - No
{1..10}brace expansion for ranges - No
+=string/array append operator - No
localin some minimal implementations (though dash does support it) - No process substitution (
<(...)) - No
selectloops for menus - No associative arrays
- Simpler prompt customization
Why Scripts Should Target sh When Portability Matters
System startup scripts, package installation scripts (postinst, prerm in Debian packages), and scripts intended to run across diverse Unix-like systems (Linux, BSD, macOS, embedded systems) often use #!/bin/sh deliberately, restricting themselves to POSIX-compliant syntax to guarantee they will run correctly regardless of which specific shell /bin/sh happens to be on a given system.
#!/bin/sh
# Portable script — avoid bash-specific syntax
if [ "$1" = "start" ]; then
echo "Starting service"
fi
bash: The GNU Extension
What bash Adds Over sh
Bash (Bourne Again SHell), created by Brian Fox for the GNU Project in 1989, implements the full POSIX sh specification while adding a substantial set of extensions for both interactive use and scripting power.
Interactive features:
- Command history with
Ctrl+Rreverse search - Tab completion (basic, extended significantly with bash-completion package)
- Command-line editing (Emacs or Vi-style keybindings)
- Job control (
Ctrl+Z,bg,fg,jobs) - Aliases
- Programmable prompt (
PS1with escape sequences)
Scripting extensions:
- Indexed arrays:
arr=(1 2 3) - Associative arrays:
declare -A map - Extended test syntax:
[[ condition ]]with pattern matching and regex support - Arithmetic expansion:
$(( expression )) - Brace expansion:
{1..10},{a,b,c} - String manipulation:
${var//pattern/replacement},${var:0:5} +=operator for appending to variables and arrays- Process substitution:
<(command)and>(command) selectloops for interactive menuslocalkeyword for function-scoped variables
bash Script Example Using Extensions
#!/bin/bash
# This script uses bash-specific features and requires bash, not sh
declare -A fruit_prices
fruit_prices[apple]=1.50
fruit_prices[banana]=0.75
for fruit in "${!fruit_prices[@]}"; do
echo "$fruit costs ${fruit_prices[$fruit]}"
done
# Brace expansion
mkdir -p project/{src,tests,docs}
# Extended test with pattern matching
filename="report.pdf"
if [[ "$filename" == *.pdf ]]; then
echo "This is a PDF file"
fi
None of this syntax works correctly in strict POSIX sh — running this script with sh script.sh instead of bash script.sh (or ./script.sh with the correct #!/bin/bash shebang) would produce errors or unexpected behavior.
Why bash Became the Default
Bash’s combination of full sh compatibility (for running existing scripts) with substantially improved interactive usability made it the natural default when Linux distributions needed a shell. Its GNU pedigree also aligned with the broader GNU/Linux ecosystem, and its widespread adoption created a virtuous cycle — more documentation, more tutorials, more scripts written for bash specifically, further cementing its position as the default choice.
zsh: The Power User’s Shell
What zsh Adds Over bash
Zsh (Z Shell), created by Paul Falstad in 1990, takes the trajectory of adding interactive convenience even further than bash. Zsh maintains substantial bash compatibility (most bash scripts run correctly in zsh, though not perfectly identically) while adding:
Superior tab completion: Zsh’s completion system is significantly more powerful — context-aware completions that understand command-specific options, menu-driven selection when multiple completions exist, and completion for a vastly larger set of commands out of the box.
$ git chec[TAB]
checkout (shows description and lets you select with arrow keys in some configurations)
Spelling correction:
$ cd /hmoe/sarah
zsh: correct '/hmoe/sarah' to '/home/sarah' [nyae]?
Zsh notices likely typos in commands and paths and offers to correct them.
Advanced globbing:
$ ls **/*.txt # Recursive glob — find .txt files in all subdirectories
$ ls *.txt~exclude*.txt # Glob with exclusion
Zsh’s extended globbing (setopt extended_glob) provides pattern-matching capabilities beyond what bash’s globbing supports natively.
Right-side prompt: Zsh supports RPROMPT — content displayed on the right side of the terminal (like the current git branch or time), separate from the main left-side prompt.
Better array handling: Zsh arrays are 1-indexed by default (unlike bash’s 0-indexed arrays) and have somewhat different, arguably more intuitive syntax for certain operations.
Extensive theming and plugin ecosystem: The “oh-my-zsh” framework, along with alternatives like “Prezto” and “zinit,” provides hundreds of pre-built themes and plugins, making zsh customization accessible without deep shell scripting knowledge.
$ sh -c "$(curl -fsSL https://raw.githubusercontent.com/ohmyzsh/ohmyzsh/master/tools/install.sh)"
zsh Compatibility Considerations
While zsh runs most bash scripts successfully, subtle differences exist:
- Array indexing starts at 1 in zsh by default (vs. 0 in bash) — though
setopt KSH_ARRAYScan change this - Word splitting behaves differently in some contexts
- Certain bash-specific parameter expansions behave slightly differently
For portable scripts meant to run under bash specifically, always use #!/bin/bash as the shebang rather than relying on zsh’s compatibility mode, and test scripts under the shell they explicitly target.
Why zsh Became macOS’s Default
Apple switched macOS’s default shell from bash to zsh in Catalina (2019), largely due to licensing considerations — bash’s later versions (4.0+) are licensed under GPLv3, which Apple’s legal position avoids using in system software, while macOS had been stuck shipping an old bash 3.2 for years. Zsh’s more permissive licensing and its rich feature set made it the natural replacement.
This decision has had ripple effects: developers using macOS for daily work more frequently install zsh (with oh-my-zsh) on Linux systems too, contributing to zsh’s growing popularity even outside macOS.
Feature Comparison Table
| Feature | sh (dash) | bash | zsh |
|---|---|---|---|
| POSIX compliant | Yes (strictly) | Yes (with extensions) | Mostly (with extensions) |
| Arrays | No | Indexed + associative | Indexed + associative (1-indexed) |
[[ ]] extended test |
No | Yes | Yes |
| Command history | Minimal | Yes, with Ctrl+R search | Yes, more powerful search |
| Tab completion | Basic | Good (with bash-completion) | Excellent, context-aware |
| Spelling correction | No | No | Yes |
| Brace expansion | No | Yes | Yes |
| Globbing power | Basic | Standard | Extended, very powerful |
| Right-side prompt | No | No | Yes (RPROMPT) |
| Plugin frameworks | No | Limited (bash-it exists) | Extensive (oh-my-zsh, Prezto) |
| Startup speed | Fastest | Fast | Slightly slower (more features loading) |
| Default on | Ubuntu/Debian scripts (/bin/sh) | Most Linux distros (interactive) | macOS (since Catalina) |
| Script portability | Highest | Good (ubiquitous but not universal) | Lower (fewer systems have it by default) |
Checking and Changing Your Shell
Checking Your Current Shell
$ echo $SHELL
/bin/bash
$ echo $0
-bash
Checking What Shells Are Available
$ cat /etc/shells
/bin/sh
/bin/bash
/usr/bin/bash
/bin/dash
/usr/bin/dash
/bin/zsh
/usr/bin/zsh
Installing zsh
$ sudo apt install zsh # Ubuntu/Debian
$ sudo dnf install zsh # Fedora
Changing Your Default Login Shell
$ chsh -s /usr/bin/zsh
This changes your account’s default shell (stored in /etc/passwd), taking effect the next time you log in (or open a new terminal, on some configurations). To switch back:
$ chsh -s /bin/bash
Trying a Shell Temporarily Without Changing Defaults
$ zsh # Start a zsh session within your current terminal
$ exit # Return to your original shell
This lets you experiment with zsh without committing to it as your default.
Which Shell Should You Use?
For Scripts: Choose Deliberately Based on Portability Needs
Use #!/bin/sh when:
- Writing system-level scripts (package pre/post-install scripts, init scripts)
- Portability across different Unix-like systems matters
- The script’s logic is simple enough not to need bash’s extensions
- You want maximum execution speed for frequently-run scripts
Use #!/bin/bash when:
- You need arrays,
[[ ]], brace expansion, or other bash-specific features - The script only needs to run on systems where bash is available (which is nearly universal on Linux, though not guaranteed on minimal containers or embedded systems)
- You are writing for an audience/environment where bash is the established standard
Avoid targeting zsh specifically for scripts unless you have a specific reason — zsh scripts are less portable than bash scripts because zsh is not installed by default on as many systems, particularly servers and minimal environments.
For Interactive Daily Use: Personal Preference
Stick with bash if:
- You want maximum compatibility with tutorials, Stack Overflow answers, and documentation (most assume bash)
- You prefer stability and minimal configuration
- You work across many different Linux systems and want consistency
Try zsh if:
- You want the best possible tab completion and command-line convenience
- You enjoy customizing your environment (themes, plugins)
- You are already comfortable with bash and want to level up your interactive experience
- You use macOS regularly and want consistency across systems
The interactive shell choice does not lock you into anything permanent — your login shell can be zsh for daily interactive convenience while all your scripts still explicitly specify #!/bin/bash or #!/bin/sh in their shebang lines, ensuring they run correctly regardless of what your personal interactive shell happens to be.
Common Pitfalls When Shells Are Confused
Running a bash Script with sh
$ sh bash_script.sh
If bash_script.sh uses bash-specific syntax (arrays, [[ ]], etc.) but is run explicitly with sh rather than respecting its own shebang, you get errors:
bash_script.sh: 5: [[: not found
The fix: Always run scripts as ./script.sh (respecting the shebang) rather than sh script.sh or bash script.sh explicitly, unless you specifically intend to override the interpreter.
Writing #!/bin/sh But Using bash Syntax
A very common mistake: a script starts with #!/bin/sh (intending portability) but the author uses bash-specific syntax without realizing it, because they tested it by running bash script.sh (which works, since bash can interpret it) rather than ./script.sh (which invokes /bin/sh, i.e., dash on Ubuntu, and fails).
The fix: If you write #!/bin/sh, actually test with sh script.sh or dash script.sh, not bash script.sh, to catch compatibility issues.
Checking Script Portability
$ checkbashisms script.sh # Install: sudo apt install devscripts
checkbashisms analyzes a script and flags any bash-specific constructs that would fail under strict POSIX sh — invaluable when writing scripts intended to be portable.
Other Shells Worth Knowing About
Beyond sh, bash, and zsh, several other shells serve specific niches:
ksh (Korn Shell) — influenced both bash and zsh’s design; still used in some enterprise Unix environments (particularly older AIX and Solaris systems).
fish (Friendly Interactive Shell) — prioritizes ease of use with syntax highlighting and autosuggestions out of the box, but deliberately breaks POSIX compatibility for a cleaner, more consistent syntax — meaning fish scripts look quite different from sh/bash/zsh scripts.
tcsh/csh — C shell family, historically popular but with scripting quirks that are widely discouraged for new scripts; still found in some older Unix environments and academic settings.
Quick Reference: Shell Comparison
| Aspect | sh | bash | zsh |
|---|---|---|---|
| Created | 1979 (original) | 1989 | 1990 |
| Primary purpose | POSIX standard baseline | General-purpose default shell | Enhanced interactive shell |
| Script shebang | #!/bin/sh |
#!/bin/bash |
#!/bin/zsh (rare for scripts) |
| Best for scripts | Maximum portability | Feature-rich scripting | Not recommended for portable scripts |
| Best for interactive use | Not designed for this | Solid, universal default | Best interactive experience |
| Arrays | No | Yes (0-indexed) | Yes (1-indexed by default) |
| Default on Ubuntu | /bin/sh → dash | Yes (interactive default) | No (optional install) |
| Default on macOS | /bin/sh → bash (old) | No (as of Catalina+) | Yes (since Catalina) |
| Plugin ecosystem | None | Limited | Extensive (oh-my-zsh) |
Conclusion: Choosing the Right Tool for Each Context
The relationship between sh, bash, and zsh reflects a natural evolution: sh established the portable, POSIX-compliant foundation; bash extended it into a full-featured default shell balancing compatibility with convenience; zsh pushed the interactive experience further still, prioritizing user comfort and customization.
For scripts, the choice should be deliberate: #!/bin/sh when portability across any POSIX-compliant system matters, #!/bin/bash when you need bash’s richer feature set and can assume bash’s availability. For interactive daily use, bash remains the universal, dependable default that matches nearly every tutorial and piece of documentation you will encounter, while zsh offers a meaningfully improved experience for those willing to invest a little time in setup and customization.
Understanding these distinctions — not just that the shells exist, but why each was created and what problem it solves — turns a source of common confusion into a clear decision framework you can apply confidently in any Linux or Unix environment.




