Bash patterns¶
Defensive patterns for scripts that run in production or CI.
set -euo pipefail¶
Put this at the top of every non-trivial script:
#!/usr/bin/env bash
set -euo pipefail
-e— exit immediately on any command returning non-zero.-u— treat unset variables as an error.-o pipefail— a pipeline fails if ANY command in it fails (not just the last).
Without pipefail, grep pattern file | wc -l returns 0 on success even if grep found nothing — the exit code is silently swallowed.
[[ ]] tests¶
Prefer [[ ]] over [ ] — it's a bash built-in, supports &&/|| inside and doesn't split on spaces:
file="/path/to/data.csv"
if [[ -f "$file" ]]; then
echo "File exists"
fi
if [[ "$1" == "--dry-run" ]] || [[ "$1" == "-n" ]]; then
DRY=true
fi
if [[ ${#items[@]} -eq 0 ]]; then
echo "No items to process" >&2
exit 1
fi
Loops¶
Iterate over an array:
services=(web worker scheduler)
for svc in "${services[@]}"; do
echo "Restarting $svc"
systemctl restart "$svc"
done
Loop over files (safe for names with spaces):
while IFS= read -r -d '' f; do
echo "Processing: $f"
done < <(find . -name "*.log" -print0)
C-style loop for numeric ranges:
for ((i=1; i<=5; i++)); do
echo "Attempt $i"
done
${var:-default}¶
Provide a default value when a variable is unset or empty:
LOG_LEVEL="${LOG_LEVEL:-info}"
OUTPUT_DIR="${OUTPUT_DIR:-/tmp/output}"
Related forms:
${var:?error message} # exit with message if unset
${var:+replacement} # substitute replacement if var IS set
${var%suffix} # strip shortest matching suffix
${var##prefix} # strip longest matching prefix
trap — cleanup on exit¶
Always clean up temp files, even on error or Ctrl-C:
TMPDIR=$(mktemp -d)
trap 'rm -rf "$TMPDIR"' EXIT
# do work in $TMPDIR — it's always cleaned up
cp important_file "$TMPDIR/work.txt"
Trap multiple signals:
trap 'echo "Interrupted"; exit 130' INT TERM
Heredoc¶
Pass multi-line strings cleanly (no escaping):
cat > /etc/myapp/config.toml <<EOF
[server]
host = "0.0.0.0"
port = 8080
log_level = "${LOG_LEVEL}"
EOF
Use <<'EOF' (quoted) to prevent variable expansion inside the block.
Functions¶
Define reusable logic and keep scripts readable:
log() {
local level="$1"; shift
echo "[$(date -Iseconds)] [$level] $*" >&2
}
die() {
log ERROR "$@"
exit 1
}
check_dependency() {
command -v "$1" >/dev/null 2>&1 || die "$1 is required but not installed"
}
check_dependency curl
check_dependency jq
log INFO "Dependencies OK"
getopts¶
Parse short options portably (POSIX, works in sh too):
usage() { echo "Usage: $0 [-v] [-o outfile] input"; exit 1; }
verbose=false
outfile="result.txt"
while getopts ":vo:" opt; do
case $opt in
v) verbose=true ;;
o) outfile="$OPTARG" ;;
*) usage ;;
esac
done
shift $((OPTIND - 1))
input="${1:-}"
[[ -z "$input" ]] && usage
See also: Python snippets for argparse when you need long options or subcommands.