🐚

A language tour

The Power of Bash

The glue of Unix. A language that turns a hundred programs into one workflow β€” and has been doing it since 1989.

scroll

01 β€” Pipelines

Programs that compose

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
pipelines.sh
#!/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

Everything is a string

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.

variables.sh
#!/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

Scripts that decide

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.

control_flow.sh
#!/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

Reusable logic, Unix style

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.

functions.sh
#!/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

Inline data and inverted pipes

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.

heredoc.sh
#!/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

Why Bash endures

🌍

Everywhere

Every Unix system has bash or sh. CI pipelines, Docker containers, deployment scripts β€” bash is the universal scripting layer.

πŸ”§

The Unix Philosophy

Do one thing well. Compose via pipes. Text is the universal interface. Bash is the philosophy made executable.

⚑

Instant Startup

No VM warmup, no runtime, no JIT. Bash scripts start immediately β€” critical for tools that run thousands of times a day.

🀝

Glue Language

Bash doesn't compete with Python or Go. It orchestrates them β€” calling binaries, redirecting output, wiring programs together.

πŸ“œ

POSIX Shell

Scripts targeting #!/bin/sh run on macOS, Linux, BSD, Solaris, and AIX. Portability across four decades of Unix.

πŸ”„

CI/CD Backbone

GitHub Actions, GitLab CI, Jenkins β€” all run bash steps. Understanding bash means understanding how software ships.