$bash
Released in June 1989 by Brian Fox at the FSF — the name is Stallman's pun, "Bourne again shell". Thirty-seven years on it still sits underneath every Linux box, every Dockerfile, every CI/CD pipeline, every curl | bash installer. No "better language" has displaced shell — because no language ships pre-installed everywhere the way shell does.
Brian Fox · GNU / FSF
maintained by Chet Ramey
no real challenger
CVE-2014-6271
What is bash
Bash is GNU's sh-compatible command-line shell, written by Brian Fox in 1989. The word "shell" means both an interactive command interpreter and a scripting language — its single most important design decision: what you type at the prompt and what you write in a file share the same syntax.
Bash is compatible with the 1977 Bourne shell + POSIX.2, and adds arrays, associative arrays, [[, $(), process substitution, brace expansion. A 30-year-old script still runs; new scripts don't have to pretend to live inside strict POSIX.
Prompt = script. A command you just validated at the prompt copies straight into a .sh — the distance from exploration to automation is zero. Python's REPL is still different from a .py; shell isn't.
Unix philosophy made literal: programs read stdin, write stdout, do one thing. | wires them; > redirects; & backgrounds. The 2026 LLM tool-chain is a revival of the same idea.
The hardest-to-replicate advantage: every POSIX system has /bin/sh; nearly every Linux has /bin/bash. No runtime to install — anything you can ssh into, you can run your script on.
import subprocess, pathlib, shutil
from contextlib import contextmanager
@contextmanager
def tmpdir():
d = pathlib.Path("/tmp/build")
d.mkdir(exist_ok=True)
try: yield d
finally: shutil.rmtree(d)
with tmpdir() as d:
subprocess.run(["tar","xf",...], check=True)
subprocess.run(["make"], cwd=d, check=True)
# 12 lines · subprocess everywhere · still calling shell tools#!/usr/bin/env bash
set -euo pipefail
TMP=$(mktemp -d)
trap 'rm -rf "$TMP"' EXIT
tar -xf release.tgz -C "$TMP"
make -C "$TMP"
# 6 lines · cleanup guaranteed · no FFI
# runs on any POSIX box · no pip installFamily Tree : ShellLineage
Every modern shell traces back to the 1971 Thompson shell. Main line: Bourne (sh) → ksh → bash → zsh. Berkeley branch: csh → tcsh. Modern: fish (anti-POSIX) / nushell (Rust). Windows: PowerShell (object pipes). Bash sits on the thickest part of the main line.
History : 55 yr timeline
From 1971's Thompson shell to 2026 — 55 years. The line runs through Bell Labs, Berkeley, GNU, Apple, Microsoft — and through one 25-year-old security hole (Shellshock) and one licensing fight (macOS leaving bash). Everyone complains about it. Everyone uses it.
- 1971
Thompson shell — shipped with Unix V1
Ken Thompson's first shell, written at Bell Labs for Unix V1, installed at
/bin/sh(the name has stuck for 55 years). Minimal: no flow control, no variables, just process spawning and redirection. The origin of every modern shell. - 1977
Bourne shell (sh) — control flow lands
Stephen Bourne at Bell Labs ships sh with
if/for/while/case/ functions / variables — all of it. The Algol-68-flavoured syntax is where the famousfi(reversedif) andesac(reversedcase) come from. POSIX shell is its standardisation;/bin/shon macOS today still means this. - 1979
C shell (csh) — the Berkeley fork
Bill Joy at Berkeley writes csh with deliberately C-like syntax:
if (cond) then ... endif. It introduces lovely interactive features — command history, job control, aliases — but its scripting semantics are infamously broken. Tom Christiansen's 1995 essay "Csh Programming Considered Harmful" is the canonical takedown. - 1983
Korn shell (ksh) — a refined Bourne superset
David Korn at AT&T ships ksh: fully Bourne-compatible, but adds arrays, associative arrays, floats, regex, coprocesses. The long-time default shell on commercial Unixes (Solaris / AIX / HP-UX). ksh93 is still genuinely in production today for some finance / telecom backends.
- 1989·06·08
Bash 1.0 — Brian Fox writes it for GNU
On 8 June 1989 Brian Fox releases Bash 1.0 at the FSF — the name is Stallman's pun, "Bourne again shell". The goal: give GNU a Bourne-compatible shell free of AT&T's licence. Bash becomes the default on nearly every Linux distribution. Chet Ramey takes over maintenance in 1992 and is still doing it today.
- 1990
zsh — Paul Falstad at Princeton
The same year (1990) Paul Falstad writes zsh. Feature-wise it's the union of ksh + bash + tcsh: powerful completion, recursive globs (
**/*.ts), rich parameter expansion, themes. It stays niche for years until oh-my-zsh (Robby Russell, 2009) puts it on every Mac developer's screen. - 1992
POSIX.2 / IEEE 1003.2 — shell standardised
POSIX freezes a Bourne-shell subset into IEEE Std 1003.2. From here on,
#!/bin/shscripts have a formal cross-Unix portability contract. But "strict POSIX shell" is much harder to write than bash — no arrays, for one. The constraint keeps tools likecheckbashismsin business to this day. - 2005
fish (friendly interactive shell)
Axel Liljencrantz releases fish: deliberately POSIX-incompatible for a cleaner language —
(...)instead of$(...), no surprise word-splitting, autosuggestions out of the box. Cost: copy-pasted bash one-liners just don't run. "We accept this is the price of breaking back-compat — but shell needed modernising". - 2006·11
PowerShell 1.0 — Microsoft's counter-experiment
Jeffrey Snover's PowerShell ships 1.0. The pitch: pipe objects, not text — pipelines carry .NET objects end-to-end. The real-world answer to "what if shell were designed from scratch, with a type system". Open-sourced and cross-platform in 2016, still the default for Windows automation today.
- 2010+
DevOps era — shell as CI/CD glue
Containers + cloud bring shell roaring back into the spotlight: Dockerfile
RUNlines, GitHub Actionsrun:blocks, K8s init containers, Ansible tasks, the Homebrew installer one-liner (/bin/bash -c "$(curl ...)"). No newer language has displaced shell because none of them is pre-installed everywhere. - 2014·09·24
Shellshock — a 25-year-old hole
On 24 September Stéphane Chazelas discloses CVE-2014-6271: bash parses environment-variable function definitions and keeps executing the trailing code. The attack surface is huge — any service that pushes user input into env then invokes bash is exposed (CGI, DHCP, git-over-ssh). The bug had been in the code since 1989 — undetected for 25 years. Together with Heartbleed the same year, it cracked the "open source = secure" story.
- 2016
PowerShell open-sourced, cross-platform
Microsoft open-sources PowerShell and ships Linux / macOS builds. It moves from "the strange Windows-only option" to "a cross-platform contender". Today, choosing bash or pwsh for ops automation is a real decision rather than a function of which OS you booted into.
- 2019·10
macOS Catalina — default shell flips from bash to zsh
Apple didn't switch for technical reasons — it switched for licensing. Apple wouldn't ship bash 4+ (GPLv3), so it pinned macOS to bash 3.2 (2007, GPLv2) for over a decade. In October 2019 Catalina cuts the cord and ships zsh as the default (BSD-style licence). A symbolic moment: the world's richest company opts out of GPLv3 — and out of bash.
- 2019
Nushell — a structured shell in Rust
Jonathan Turner and Andrés Robalino kick off Nushell: structured data through pipelines (the PowerShell intuition) + a Rust implementation + modern syntax.
ls | where size > 1mbreads like a table query. Niche but past 0.x maturity; it hasn't dented bash's deployment base, but it's taken the "shell should be better" early adopters. - 2020+
Oil shell / YSH — "a stricter bash"
Andy Chu's Oil (later renamed YSH) takes a different angle: run bash scripts as-is, but offer a clean new language alongside. The philosophy: "bash can't be thrown away, but it needs an upward path." Small in size, but one of the few projects that thinks of shell as a designed language.
- 2026
37 years in — bash is still here
Bash in 2026: the default on nearly every Linux distribution, the default in every CI/CD system, the default in every Dockerfile, the default in every cloud-server bootstrap. Chet Ramey still maintains it (the 5.x series). Zsh has taken Mac desktop interactive use; PowerShell has taken Windows automation; but the server-side and CI scripting base is bash, period. A de-facto monopoly with no real challenger.
Language Essentials : BashGotchas
The eight cards below cover where bash differs hardest from other languages — and where scripts break: [ ] vs [[ ]], parameter expansion, strict mode, here-docs, process substitution, trap, xargs, $(). The ninth is a brief take on "is bash even a programming language".
[ ] vs [[ ]]
[ is an external command (POSIX test); [[ is a bash keyword. [[ ]] skips word-splitting, doesn't explode on unquoted vars, supports == glob matching. In bash, always use [[.
# fragile — splits if $f has spaces
if [ -f $f ]; then ...
# safe — quoting optional inside [[
if [[ -f $f && $f == *.log ]]; then ...${var} parameter expansion
The lightweight replacement for sed/awk in shell: ${f%.*} strips suffix, ${f##*/} is basename, ${f:-default} is a default. Looks like line noise — but learning it saves hundreds of forks.
f=/var/log/app.log
echo $"${f##*/}" # app.log
echo $"${f%.*}" # /var/log/app
echo $"${name:-anon}" # defaultset -euo pipefail
The strict-mode trio: -e exit on command failure, -u error on undefined-variable use, -o pipefail any pipe segment failing = pipeline failure. First line of every bash script. Pair with IFS=$'\\n\\t' to neutralise word-splitting ambushes.
#!/usr/bin/env bash
set -euo pipefail
IFS=$'\\n\\t'
# now `cmd1 | cmd2` failing in cmd1 actually stops youHere-docs <<EOF
Feed a multiline string as if it were a file. <<EOF expands variables; <<'EOF' (quoted) is fully literal. Git commit messages, inline SQL, generated config files — all of them lean on this.
cat >config.yaml <<EOF
host: $$HOSTNAME
port: 8080
EOF
# single-quoted EOF = no expansionProcess substitution <(...)
Hand a command's output as if it were a file path to another command — no temp file needed. diff <(sort a) <(sort b) is the canonical case: compare two sorted streams, never touch disk. bash/zsh only — strict POSIX doesn't have it.
diff <(sort a.txt) <(sort b.txt)
comm -12 <(a-cmd) <(b-cmd)trap — exit-time cleanup
Register an exit hook. trap '...' EXIT = the handler runs once no matter how the script dies (normal return, kill, error-exit). The right place to clean up tempfiles, children, locks — not rm -rf $TMP before every exit.
TMP=$(mktemp -d)
trap 'rm -rf "$TMP"' EXIT
# do whatever; on any exit, $TMP is gonexargs — pipe → command args
Turns stdin lines into the next command's arguments. find . -name '*.log' | xargs grep ERROR is the canonical move — but filenames with spaces blow it up; the safe version is find ... -print0 | xargs -0 or just find -exec. The connective tissue of shell orchestration.
find . -name '*.log' -print0 |
xargs -0 grep -l ERROR
# -print0 / -0 = NUL-delimited, space-safeSubshell $(...) vs backticks
Command substitution. New code uses $(cmd) — nestable, clean quoting. Backticks `cmd` are a 1977-sh artefact, deprecated even in POSIX (1992). You'll still see them — that's not a bug, that's 35 years of muscle memory.
# preferred
today=$(date +%F)
# legacy — nesting gets ugly fast
today=`date +%F`Programming language? Formally yes, practically don't
Bash is Turing-complete — functions, loops, arrays, associative arrays, coprocesses. Formally a real language. But: no variable types, six layers of quoting rules, set -e corner cases worth a small book, implicit string/number coercion no one fully remembers. The existence of BashFAQ + ShellCheck is itself the answer.
"Anyone who's written more than 100 lines of bash without bugs is lying."
Idioms Hall of Fame : 9 patterns
These nine patterns will cut your Stack Overflow searches in half. Each has a decade-plus of stable form and is validated by ShellCheck. Memorise them and you're intermediate.
find . -name '*.ts' -print0 | xargs -0 grep -l TODOIterate a list of files
find the files, xargs them into grep — shell orchestration 101. For space-safety use -print0 / -0.
set -euo pipefail; IFS=$'\\n\\t'Strict-mode opener
The first line of every production script. Turns silent failures into loud ones.
trap 'rm -rf "$TMP"' EXITExit-time cleanup
trap EXIT — runs once however the script dies. The right home for tempfile / child cleanup.
diff <(sort a) <(sort b)
Diff two streams
Process substitution at its best: compare two command outputs without touching disk.
name=$"${path##*/}"; base=$"${name%.*}"
Path slicing
Parameter expansion strips suffixes / takes basenames — no sed/awk needed.
psql -d db <<SQL
SELECT 1;
SQLHere-doc feeds SQL
Pipe multiline SQL straight into psql / mysql with no intermediate .sql file.
mkdir -p "$OUT/data/processed"Idempotent mkdir
mkdir -p's "doesn't fail if it exists" is the 30-year-standard idempotency pattern.
until nc -z host 5432; do sleep 1; done
Wait for a port
A container-era staple: a loop with sleep and exit code waits for a service to be ready.
#!/usr/bin/env bashPortable shebang
#!/usr/bin/env bash is more portable than the hardcoded #!/bin/bash (e.g. Homebrew bash lives in /opt/homebrew/...).
Why It's Still Here : WhyShellPersists
Shell has been complained about for 50 years and no replacement has stuck. Not because shell is great — because the slot it occupies is irreplaceable: pre-installed everywhere, pipes are Unix philosophy made literal, the syntax is 30-year backward compatible, and it's the glue of all of DevOps.
Pre-installed everywhere
This is the root of every other reason: bash ships by default on Linux, sh ships on every POSIX system. No runtime to install — write and run. When you ssh into a stranger's box, the only things you can count on are the shell and ls. Python? Maybe. Node? Definitely not.
# works on any *nix from 1989 onward
#!/bin/sh
echo "hello $(uname -s)"Pipes as a universe model
Unix philosophy made literal: each program reads stdin, writes stdout, does one thing. cmd1 | cmd2 | cmd3 expresses composition in 22 bytes. The 2026 LLM tool-chain revived the same idea: tool calls, streamed tokens, and agent chains are all isomorphic to pipes.
curl -s api/data |
jq '.[].id' |
xargs -I{} curl -s api/detail/{}The DevOps default
Dockerfile RUN, GitHub Actions run:, K8s init containers, Ansible shell:, the Homebrew one-line install, the official Rust / Node installers — all of them are bash underneath. If you write "infrastructure" you almost certainly need shell, even if your day-job language is Python / Go.
# the Homebrew installer, one line
/bin/bash -c "$(curl -fsSL .../install.sh)"Glue stability — 30 years unbroken
A bash script written in 1989 probably still runs. Python 2 → 3 shattered tons of code, Node breaks APIs nearly every year, half of early Ruby's ecosystem is gone — shell doesn't have this problem. "Boring" is its greatest virtue.
# 1989 syntax · still works in 2026
for f in *.txt; do
mv "$f" "$${f%.txt}.bak"
doneInteractive + scripted are the same language
Rare among programming languages, the interactive line and the script file share one syntax. A command you just ran in your history can be pasted into a .sh and it runs. The distance from "explore" to "automate" is zero. Python has a REPL but it's not the same as a .py; shell has no such gap.
# this works at the prompt
grep -r TODO . | wc -l
# …and unchanged inside todo-count.shEcosystem — the whole shell family : ShellFamily
It's not just bash. The twelve below (shells + tools + main contexts) make up the real shell ecosystem in 2026. Every one is in real use: servers run bash, Mac desktops run zsh, Windows automation uses pwsh, and Dockerfiles / Actions default to bash.
vs Python / pwsh / Nushell : bash vs the rest
vs Python: shell for ≤ 100 lines of glue, Python for ≥ 100 lines of logic — the boundary is roughly the moment you want serious error handling. vs PowerShell: text streams vs object streams, two different worldviews. vs Nushell: tries to have both, but 35 years of install base is hard to beat.
| Bash | Python | PowerShell | Nushell | |
|---|---|---|---|---|
| Origin | Brian Fox · 1989 | Guido · 1991 | Snover / MS · 2006 | Turner · 2019 |
| Pipe payload | Text streams | None (function calls) | .NET objects | Structured tables / cols |
| Types | None (all strings) | Dynamic · optional typing | .NET type system | Structured value types |
| Pre-installed | every Linux / macOS / WSL | most Linux · version chaos | Windows default · elsewhere installable | Install required |
| Interactive vs script | same syntax | REPL ≠ .py form | same syntax | same syntax |
| Error handling | set -euo pipefail · corner cases | try / except · first-class | try / catch · ErrorRecord | try { ... } · structured errors |
| String quoting | six layers · word-split traps | A string is a string | two flavours · clean | two flavours · clean |
| Ecosystem size | every *nix tool (find/sed/awk/...) | PyPI · ~500k packages | PowerShell Gallery | Small · early |
| Typical use | CI / deploy / Dockerfile / one-off | Data / web / AI / business | Windows automation / AD / Exchange | Data exploration / personal shell |
| When to leave it | ≥ 100 lines · real error handling · complex JSON/SQL | Don't | Cross-platform deploy | Any script you ship |
Pitfalls & Reality : ShellTraps
Owning bash's bad parts beats pretending. Below: the four most-stepped-in traps + the perennial question ("is bash a programming language?"). Each one maps to a ShellCheck rule or a BashFAQ entry.
"I've been maintaining bash for 30+ years. Every new feature gets me "shell shouldn't do that"; every old bug gets me "shell shouldn't have been designed that way". But every time I look at someone's GitHub Actions log or deploy script, it's still bash. It isn't a good language — it's an irreplaceable one. Those are different claims.
Word splitting — unquoted variables explode
The shell splits variables on IFS (whitespace by default) then expands globs. Filenames with spaces, *, or ? turn into multiple arguments — silently.
Rule: quote every variable. "$f", "$@", "$${arr[@]}". There are almost no exceptions. ShellCheck's SC2086 catches every unquoted $var.
# BAD — file with spaces: silent disaster
rm $f
# GOOD
rm "$f"set -e doesn't catch what you think
set -e does not trigger in more places than you'd guess: inside conditions (if cmd; then), the left side of a pipe (without pipefail), cmd || echo bad, functions invoked with ||, and inside subshells. The corner-case list is long enough that Greg Wooledge's BashFAQ/105 exists to document them. Strict mode reduces failure surface; it doesn't eliminate it.
Associative arrays need bash 4+, but macOS ships bash 3.2
bash 4.0 (2009) added associative arrays via declare -A, but macOS still ships bash 3.2 (2007 — because of GPLv3). On a Mac this trips up other people's scripts constantly: install Homebrew bash into /opt/homebrew/bin/bash and use #!/usr/bin/env bash. After Catalina (2019) Apple just made zsh the default and called it done.
Is bash a programming language?
Formally yes: Turing-complete, with variables, loops, functions, arrays, associative arrays, coroutines. By definition the answer is yes.
In practice: nobody writes more than 100 lines of bash without bugs. Variables are untyped, strings and numbers mix freely, quoting has six layers, and error handling depends on set -e's corner cases. The existence of BashFAQ and ShellCheck is itself the answer: it's a language that works but is not built for large programs.
In one line: In 2026 bash isn't the language you pick — it's the language you already use. Own it, run strict mode, use ShellCheck. Past 100 lines of nontrivial logic, switch to Python. That's honest shell engineering.