A language tour
The glue of Unix. A language that turns a hundred programs into one workflow β and has been doing it since 1989.
01 β Pipelines
The Unix pipe is one of the most powerful ideas in computing. Programs read from stdin, write to stdout, and know nothing about each other. Bash wires them together into pipelines β each program doing one thing well, chained into workflows of remarkable power.
"Write programs that do one thing and do it well. Write programs to work together. Write programs that handle text streams, because that is a universal interface."
β Doug McIlroy, inventor of Unix pipes#!/usr/bin/env bash # Count the most common words in a file cat words.txt \ | tr '[:upper:]' '[:lower:]' \ | tr -s ' \t\n' '\n' \ | sort \ | uniq -c \ | sort -rn \ | head -10 # Find all TODO comments in your codebase grep -r "TODO" src/ \ | grep -v ".git" \ | sort -t":" -k1,1
Each step in a pipeline is a separate process. | connects stdout to stdin with a kernel buffer. No temporary files, no coordination β the kernel handles the plumbing.
02 β Variables & Substitution
Bash's type system is simple: everything is a string, unless you need arithmetic. Command substitution lets you capture program output into variables. Parameter expansion gives you powerful string manipulation without a single import.
#!/usr/bin/env bash # Command substitution β capture output of a command TODAY=$(date +%Y-%m-%d) BRANCH=$(git rev-parse --abbrev-ref HEAD) NUM_FILES=$(find . -name "*.go" | wc -l) # Parameter expansion β string manipulation built-in FILE="archive.tar.gz" echo "${FILE%.*}" # archive.tar (remove last extension) echo "${FILE%%.*}" # archive (remove all extensions) echo "${FILE#*.}" # tar.gz (remove up to first dot) echo "${FILE^^}" # ARCHIVE.TAR.GZ (uppercase) # Default values : "${ENVIRONMENT:=production}" # set if unset echo "Running in: $ENVIRONMENT"
${FILE%.*} strips the shortest match from the right. ${FILE%%.*} strips the longest. These parameter expansion operators work without any external program β pure shell built-ins.
03 β Control Flow
Bash's control flow is built around exit codes β every command returns 0 for success and nonzero for failure. This makes error handling idiomatic: && chains success, || handles failure, and set -e exits on any error.
#!/usr/bin/env bash set -euo pipefail # exit on error, undefined vars, pipe failures DEPLOY_ENV="${1:-staging}" # first arg, default to "staging" # && and || are short-circuit operators on exit codes git diff --quiet && echo "clean" || echo "dirty" if [[ "$DEPLOY_ENV" == "production" ]]; then echo "Deploying to production β double-checkingβ¦" read -r -p "Are you sure? [y/N] " confirm if [[ "$confirm" != [yY] ]]; then echo "Aborted."; exit 1 fi fi echo "Deploying to ${DEPLOY_ENV}β¦"
set -euo pipefail is the three-flag safety net: exit on error, error on undefined variable, propagate pipe failures. These three flags turn Bash from a silent failure mode to a strict one.
04 β Functions & Loops
Bash functions are not methods or subroutines β they're just named command sequences that accept positional arguments. Loop over files, over command output, over arrays. The combinatorial power comes from composing simple pieces.
#!/usr/bin/env bash # A function β $1, $2... are positional args log() { local level="${1:-INFO}" local msg="$2" echo "[$(date +%H:%M:%S)] [$level] $msg" } log INFO "Starting deploy" log WARN "Low disk space" log ERROR "Connection failed" # Loop over files matching a glob for file in *.log; do gzip -9 "$file" && log INFO "Compressed: $file" done # Loop over command output while IFS='' read -r line; do echo "Processing: $line" done < "input.txt"
Always quote "$file" in loops β without quotes, filenames with spaces break the loop. Shell quoting is Bash's sharp edge: important to get right, invisible when wrong.
05 β Here Documents & Process Substitution
Here documents let you embed multiline strings directly in a script. Process substitution treats command output as a file β enabling patterns like diff <(cmd1) <(cmd2) that would otherwise require temporary files.
#!/usr/bin/env bash # Here document β inline multiline string cat <<EOF Server: $HOSTNAME Date: $(date) User: $USER EOF # Indented heredoc (bash 4+) psql -U postgres <<-SQL SELECT name, email FROM users WHERE created_at > NOW() - INTERVAL '7 days'; SQL # Process substitution β diff two command outputs without temp files diff <(ssh server1 'cat /etc/hosts') \ <(ssh server2 'cat /etc/hosts') # Tee to multiple files via process substitution command | tee >(gzip > out.gz) >(wc -l > count.txt)
Process substitution <(cmd) creates a named pipe and passes its path to the outer command. diff <(cmd1) <(cmd2) diffs the live outputs of two commands with no temporary files.
06 β The Whole Picture
Every Unix system has bash or sh. CI pipelines, Docker containers, deployment scripts β bash is the universal scripting layer.
Do one thing well. Compose via pipes. Text is the universal interface. Bash is the philosophy made executable.
No VM warmup, no runtime, no JIT. Bash scripts start immediately β critical for tools that run thousands of times a day.
Bash doesn't compete with Python or Go. It orchestrates them β calling binaries, redirecting output, wiring programs together.
Scripts targeting #!/bin/sh run on macOS, Linux, BSD, Solaris, and AIX. Portability across four decades of Unix.
GitHub Actions, GitLab CI, Jenkins β all run bash steps. Understanding bash means understanding how software ships.