Bash Scripting cheat sheet

A scannable Bash Scripting reference: 16 short snippets across 9 topics, each linking back to the lesson it came from.

At a glance

TopicWhat it covers
Variables, quoting and exit codesAn unquoted expansion is split on whitespace and then treated as a glob pattern. That is the source of most problemslesson
Writing a robust scriptStrict mode, traps that clean up after a crash, and argument parsing that fails with a useful messagelesson
Shells, startup files and running scriptsA shell is a program that reads commands and runs them. The same script behaves differently depending on which shelllesson
Parameter expansion and string manipulationThe patterns are globs, not regular expressions: *, ? and [a-z]. Use #/% for path surgery and reach for sed only whenlesson
Text processing: grep, sed and awk in scriptsUnder set -e, a 1 from grep aborts the script even though "no match" is a legitimate result. Put the call in an if orlesson
Process handling, signals and parallelismBackground jobs and wait, job control, signals and traps, timeouts, xargs -P, GNU parallel, and controlling concurrencylesson
Testing and linting shell scriptsTreat every suppression as a comment that needs a reason. A file with a dozen unexplained disables is linted in namelesson
Portability and POSIX shellA script that runs on your laptop may run under dash in a Debian container, busybox ash in an Alpine image, or bash 3.2lesson
A practical automation projectIdempotency is the property that makes automation safe to retry. Aim for every step to be either naturally repeatablelesson

Quick snippets

Variables, quoting and exit codes

Variables

name=ada                     # no spaces around =
count=3
files=$(ls -1 | wc -l)       # command substitution
today=$(date +%F)

echo "$name has $count items"   # double quotes expand
echo '$name is literal'         # single quotes do not

readonly MAX_RETRIES=5
export SERVICE_URL="https://api.example.com"   # visible to child processes

Quoting and word splitting

file="my report.txt"

cat $file        # two arguments: "my" and "report.txt"
cat "$file"      # one argument: the whole name

rm *.txt         # the glob expands in the current directory
rm "*.txt"       # tries to delete a file literally named *.txt

set -- one "two three"
for arg in "$@"; do echo "[$arg]"; done   # keeps each argument intact

Full lesson: Variables, quoting and exit codes →

Writing a robust script

Strict mode

#!/usr/bin/env bash
set -euo pipefail
IFS=$'\n\t'          # a safer default separator for unquoted expansions

Traps and cleanup

tmpdir=$(mktemp -d)

cleanup() {
    local status=$?
    rm -rf "$tmpdir"
    exit "$status"
}
trap cleanup EXIT INT TERM

trap 'echo "error on line $LINENO" >&2' ERR

exec {lock_fd}>/var/lock/myscript.lock || { echo "already running" >&2; exit 1; }

Full lesson: Writing a robust script →

Shells, startup files and running scripts

Which shell is running

echo "$0"                 # name of the running shell or script
echo "$BASH_VERSION"      # set only under bash
ps -p $$ -o comm=         # the executable behind this shell

ls -l /bin/sh             # on Debian/Ubuntu this is usually a symlink to dash
bash --version
sh --version 2>/dev/null || echo "sh is not bash"

Shebangs, permissions and startup files

#!/usr/bin/env bash
# ^ the kernel runs the named interpreter, not your login shell

chmod +x deploy.sh
./deploy.sh              # honoured: shebang decides the interpreter
bash deploy.sh           # ignores the shebang, forces bash
sh deploy.sh             # forces sh: bashisms break here

source deploy.sh         # run in the CURRENT shell: variables and cd persist
. ./deploy.sh            # identical, POSIX spelling

Shebangs, permissions and startup files

# make a login shell pick up interactive settings
[[ -f ~/.bashrc ]] && . ~/.bashrc

# and keep interactive-only code out of scripts
case $- in
    *i*) ;;
    *)   return ;;   # not interactive: stop here
esac

Full lesson: Shells, startup files and running scripts →

Parameter expansion and string manipulation

Defaults, errors and alternatives

: "${API_URL:?API_URL must be set}"     # abort with your own message
name=${1:-guest}                          # use the default if unset OR empty
count=${COUNT:=10}                        # default, and assign it for later
quiet=${VERBOSE:+no}                      # 'no' when VERBOSE is set and non-empty
raw=${MAYBE_BLANK-unchanged}              # '-' form: only an UNSET variable triggers it

# the ':' forms also treat the empty string as missing; without it they do not
echo "${EMPTY:-fallback}"     # fallback
echo "${EMPTY-fallback}"      # (empty line: the variable exists, so it is used)

Full lesson: Parameter expansion and string manipulation →

Text processing: grep, sed and awk in scripts

sed: substitutions and edits

sed -n '10,20p' file              # print only a range
sed '/^$/d' file                   # delete blank lines
sed 's/foo/bar/' file              # first match per line
sed 's/foo/bar/g' file             # every match
sed -E 's/([0-9]+)ms/\1 ms/' file  # capture group, with -E
sed -e 's/a/b/' -e 's/c/d/' file   # several expressions
sed -i.bak 's/localhost/db.internal/g' config.ini   # edit in place, keep a backup
sed '1i # managed by automation' file > out        # insert a line at the top

awk, cut, sort and choosing

awk '{ print $1, $3 }' access.log          # select columns
awk -F, '{ sum += $3 } END { print sum }' data.csv
awk '$9 >= 500 { print $7 }' access.log    # filter by a field
awk -F: 'NR > 1 && $3 >= 1000 { print $1 }' /etc/passwd
awk '/ERROR/ { errors++ } END { print errors + 0 }' app.log

cut -d, -f1,3 data.csv                      # fixed columns, fast
sort -t, -k3,3nr data.csv                   # numeric, descending, third field
sort file | uniq -c | sort -rn | head       # top repeated lines
tr -s ' ' '\t' < file                      # squeeze spaces into tabs

Full lesson: Text processing: grep, sed and awk in scripts →

Process handling, signals and parallelism

Signals, traps and timeouts

trap 'echo "interrupted" >&2; exit 130' INT
trap 'kill 0' TERM            # forward the signal to the whole process group
trap 'cleanup' EXIT           # runs on success, failure and signals
trap - INT                    # reset a handler to the default

timeout 30s curl -fsS "$url"          # SIGTERM after 30s
timeout -s KILL 5m rsync -a src/ dst/ # hard kill after 5 minutes
timeout --preserve-status 10s cmd     # report the command's status, not 124

curl -fsS --max-time 10 --connect-timeout 3 "$url"   # timeouts at the client too

Full lesson: Process handling, signals and parallelism →

Testing and linting shell scripts

ShellCheck and shfmt

shellcheck deploy.sh                    # every finding, with a wiki link
shellcheck -S warning bin/*.sh           # fail only on warning and above
shellcheck -x -s bash lib/*.sh           # follow sourced files, force a dialect
shellcheck -f gcc -e SC1091 script.sh    # CI-friendly output, one rule disabled

shfmt -d -i 2 -ci script.sh              # show a diff of the formatting
shfmt -w -i 2 -ci script.sh              # rewrite in place

# .shellcheckrc in the repository root
# shell=bash
# disable=SC1091        # not following a source we know about
# external-sources=true

Fixtures and CI

# fixtures: real-looking inputs under version control
test/fixtures/invoices/2026-08.csv
test/fixtures/config/minimal.ini

# never write into the checkout; use a temp directory per run
workdir=$(mktemp -d)
trap 'rm -rf "$workdir"' EXIT

# a pre-commit hook that is fast enough to actually leave enabled
shellcheck -S warning bin/*.sh || exit 1
bash -n bin/*.sh || exit 1
bats --print-output-on-failure test/

Full lesson: Testing and linting shell scripts →

Portability and POSIX shell

Which shell is really there

ls -l /bin/sh                     # dash on Debian/Ubuntu, ash on Alpine
busybox --list | head             # what a minimal container actually has
bash --version | head -n 1        # 3.2 on macOS: no associative arrays

dash -n script.sh                 # syntax check under a strict POSIX shell
checkbashisms script.sh           # report bash-only constructs
shellcheck -s sh script.sh        # lint against the POSIX dialect

Full lesson: Portability and POSIX shell →

A practical automation project

Structure, logging and dry-run

bin/deploy          entry point: arg parsing, then calls lib functions
lib/common.sh       log, die, run, config loading
lib/deploy.sh       the actual work, one function per step
test/               bats tests and fixtures
README.md           usage, configuration, exit codes
deploy.conf.example documented sample configuration

Finishing and shipping it

# exit codes your callers can branch on
# 0  success (or nothing to do)
# 1  unexpected runtime failure
# 2  usage error
# 3  another instance is running
# 4  configuration invalid

trap 'status=$?; cleanup; exit "$status"' EXIT INT TERM
trap 'log "failed at line $LINENO" >&2' ERR

if $verbose; then set -x; fi
log "starting $PROG $VERSION env=$env target=$target dry_run=$DRY_RUN"

Full lesson: A practical automation project →

FAQ

Is this Bash Scripting cheat sheet free to use?
Yes. No sign-up and no tracking: the page is static, every example is on the page itself, and you can print it or save it as a one-page reference.
Where do the examples come from?
Every snippet is taken from the 9 lessons of the Bash Scripting course on this site, and each section links back to the lesson it was pulled from.
How do I go deeper than a cheat sheet?
Open the full Bash Scripting course — it carries the worked explanations, the edge cases and the exercises behind every line here.

Git Linux Docker Kubernetes Nginx CI / CD

Last refreshed 2026-09-27.