Dualo
Bash / Shell Scripting

Scripts, shebang & safe defaults

A script is a file of commands. The shebang picks the interpreter; `set -euo pipefail` turns silent bugs into loud failures.

2 min read

Shebang resolution: the kernel reads the first line; if it starts with #!, the rest (up to 127 chars on Linux, 32 on older BSD) is the interpreter path + one optional argument. #!/usr/bin/env bash is a portability trick: env is almost always at /usr/bin/env, and it looks up bash via $PATH. Cannot pass flags in a portable way (#!/usr/bin/env bash -e works on Linux, may not on all BSDs — prefer set -e inside the script).

set -e gotchas: does NOT fire inside if, while, until, &&/|| chains, or for commands followed by |. cmd1 && cmd2 — if cmd1 fails, the conjunction fails, but set -e sees it as 'expected failure' and continues. In practice, set -e is a best-effort safety net; combine it with explicit checks for critical paths.

set -u gotchas: unset variable = fatal. But ${var:-} (default to empty) is allowed and idiomatic for optional vars. Problem: "$arr[@]" on an empty array errors under Bash <4.4; use "${arr[@]+${arr[@]}}" or upgrade. PS1 and interactive vars may be unset in non-interactive shells — don't source interactive configs from scripts.

set -o pipefail: makes the pipeline exit status = the rightmost non-zero (or 0 if all succeed). With set -e + pipefail, a curl fail | jq '.' aborts. Without pipefail, curl's error is swallowed by jq's success. Nearly always what you want for scripts.

IFS=$'\n\t': changes word splitting to NOT split on spaces, only newlines and tabs. Protects against filenames with spaces in loops and substitutions. Controversial — some prefer explicit quoting everywhere. Both approaches work; consistency matters more than the choice.

trap: register handlers for signals and special events. trap 'rm -rf "$tmp"' EXIT cleans up a temp dir no matter how the script ends (normal, error, Ctrl-C — but NOT SIGKILL). Multiple traps: trap - EXIT clears, trap '...' INT TERM handles Ctrl-C. Idiom: tmp=$(mktemp -d) && trap 'rm -rf "$tmp"' EXIT.

Argument parsing: simple scripts use $1/$2 + positional. Real scripts use getopts (POSIX, short flags only: while getopts "hv" opt; do case $opt in ...) or parse $@ manually for long flags (--verbose). For non-trivial CLIs, consider whether the script should really be a Python/Go program.

Grounded on https://redsymbol.net/articles/unofficial-bash-strict-mode/

Next up

Signals, jobs & processes

Run commands in the background, wait on them, and handle Ctrl-C gracefully. The runtime side of shell scripting.