Writing a robust script
Strict mode, traps that clean up after a crash, and argument parsing that fails with a useful message.
Strict mode
#!/usr/bin/env bash
set -euo pipefail
IFS=$'\n\t' # a safer default separator for unquoted expansions| Option | Effect |
|---|---|
-e / errexit | Exit as soon as a command returns non-zero |
-u / nounset | Treat an unset variable as an error |
-o pipefail | A pipeline fails if any stage fails, not just the last |
-x / xtrace | Print each command; useful for debugging, but it leaks values into logs |
IFS | Controls how unquoted expansions are split |
set -edoes not apply inside a condition or a&&/||list, so it is not a substitute for checking results you care about.- Use
${VAR:-default}for optional values so-udoes not abort on a variable you intended to leave unset. - Put the shebang and strict mode at the top of every script you keep; consistency matters more than which flavour you pick.
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; }trap ... EXITruns on success, failure and interruption, which makes it the reliable place for cleanup.- Capture
$?on the first line of the trap: any command inside it overwrites the original status. - Use
mktemp -dinstead of a fixed path in/tmp, or two concurrent runs will delete each other's files. - A lock file stops two copies of a cron job from overlapping and fighting.
⚠️
A trap on
EXIT does not run if the shell is killed with SIGKILL. Never rely on cleanup for correctness: make the next run able to detect and repair the leftover state.Arguments and usage
#!/usr/bin/env bash
set -euo pipefail
usage() {
cat >&2 <<EOF
usage: $(basename "$0") [-e ENV] [-v] TARGET
-e ENV environment to deploy to (default: staging)
-v verbose output
EOF
exit 2
}
verbose=false
env=staging
while getopts ":e:vh" opt; do
case "$opt" in
e) env=$OPTARG ;;
v) verbose=true ;;
h) usage ;;
\?) echo "invalid option: -$OPTARG" >&2; usage ;;
:) echo "option -$OPTARG needs a value" >&2; usage ;;
esac
done
shift $((OPTIND - 1))
target=${1:-}
[[ -n "$target" ]] || usage
if $verbose; then set -x; fi
echo "deploying to $env: $target"shift $((OPTIND - 1))drops the parsed options so"$@"holds only the positional arguments.- The leading colon in
getopts ":e:vh"switches it to silent mode so you can print your own error messages. --ends option parsing explicitly, which lets a target name start with a dash.- Validate what you accept: required arguments present, files readable, numbers in range. Fail with a clear message and a non-zero status.
FAQ
Why does set -e not stop my script?
It does not apply to commands in conditions, in
&& or || lists, or inside while and until tests. Check those explicitly, and prefer if ! cmd; then ... so the failure is visible.How do I debug a failing script?
Run
bash -x script.sh, or add set -x after argument parsing. Combine it with a trap that reports $LINENO so you learn both the line and the values in play.Related
Variables, quoting and exit codes Conditionals and loops
Last refreshed 2026-09-18.