The shape of a reviewable shell script
Useful scripts make their interpreter, inputs, state changes, functions, main flow, and cleanup limits visible. Treat the examples as sketches to verify against a real environment, not production recipes to paste unchanged.
backup.sh — rsync to dated archive
#!/usr/bin/env bash
set -euo pipefail
src="${1:-$HOME}"
dest="${BACKUP_ROOT:-/Volumes/backup}/$(date +%Y/%m/%d)"
mkdir -p "$dest"
rsync -a --dry-run --itemize-changes --exclude='.cache' "$src/" "$dest/"
printf 'review the dry run, then repeat without --dry-run\n'
# Add --delete only when the destination is a verified disposable mirror.deploy.sh — test then push
#!/usr/bin/env bash
set -euo pipefail
log() { printf '%(%H:%M:%S)T %s\n' -1 "$*"; }
trap 'log "deploy failed at line $LINENO"' ERR
log 'running tests'
./test.sh
log 'pushing'
git push origin main
log 'done'daily-check.sh — health gate
#!/usr/bin/env bash
set -u
set -o pipefail
fail=0
check_http() { curl --fail --silent --show-error --max-time 10 https://api.example/health; }
check_worker() { pgrep -x my-worker >/dev/null; }
check_disk() { df -P / | awk 'NR==2 { exit !(\$5+0 < 90) }'; }
for name in http worker disk; do
if "check_${name}"; then
printf 'OK %s\n' "$name"
else
printf 'FAIL %s\n' "$name" >&2
fail=1
fi
done
exit "$fail"Assign ownership before scheduling
Keep one purpose per script and record the runtime owner, interpreter, inputs, outputs, logs, retry policy, and stop procedure beside it. A readable file is only one part of an operable job.
Name the purpose and lifetime
A purpose-named file and an ownership note make a script easier to find, stop, and replace. Date-prefixed scratch names hide whether a job is temporary or persistent.
Put verification between example and operation
Preview source and destination, permissions, and deletion behavior before turning a teaching sketch into a scheduled command. A dry run is evidence to review, not ceremonial output.
Use functions and argument arrays instead of eval
Store data as data. Named functions and quoted arrays preserve argument boundaries; eval reparses strings as shell syntax and turns untrusted text into commands.