Shell scripts often begin as three commands pasted into a file. Then they gain arguments, environment checks, cleanup logic, and production responsibilities. At that point, structure is no longer cosmetic—it determines whether the script can fail predictably and be maintained safely.
Bash does not have Python’s __main__ convention, but the same design idea is useful: define small functions, keep startup logic explicit, and call a single main function at the end.
#!/usr/bin/env bash
set -Eeuo pipefail
usage() {
printf 'Usage: %s --environment <name>\n' "${0##*/}"
}
cleanup() {
local exit_code=$?
# Release temporary resources here.
exit "$exit_code"
}
main() {
local environment=""
while (($#)); do
case "$1" in
--environment)
[[ $# -ge 2 ]] || { usage >&2; return 2; }
environment=$2
shift 2
;;
-h|--help)
usage
return 0
;;
*)
printf 'Unknown argument: %s\n' "$1" >&2
usage >&2
return 2
;;
esac
done
[[ -n "$environment" ]] || { usage >&2; return 2; }
printf 'Running for environment: %s\n' "$environment"
}
trap cleanup EXIT
main "$@"
What this structure improves
Passing "$@" to main preserves arguments exactly. Declaring variables as local reduces accidental global state. Returning from functions keeps control flow testable, while the final process exit code still communicates success or failure to automation.
Strict mode deserves nuance. set -u catches unset variables and pipefail prevents an early pipeline failure from being hidden. set -e can be surprising inside conditions, substitutions, and compound commands, so it does not replace explicit error handling. Understand each command whose failure is acceptable and handle it intentionally.
Quoting is equally important. Use "$value" unless word splitting or glob expansion is explicitly required. Prefer arrays when passing multiple arguments, and use printf instead of relying on inconsistent echo behavior.
Make failure safe
Temporary files should be created securely and removed through a trap. Validate prerequisites before changing state. Log actions without printing secrets. For scripts that modify infrastructure or data, offer a dry-run mode and make repeated execution idempotent whenever possible.
If portability across POSIX shells is required, do not write Bash-specific syntax and label it sh. If Bash is the dependency, declare it in the shebang and document the supported version range.
Add automated checks
Run ShellCheck in CI, format consistently, and test important branches. Libraries such as Bats can exercise functions and command behavior, but even a small suite covering invalid arguments, missing dependencies, cleanup, and exit codes adds real confidence.
There is also a boundary to respect. When a script needs complex data structures, extensive concurrency, or a large dependency graph, a general-purpose language may be easier to test and operate.
A main function does not make a script production-ready by itself. It creates a clear entry point around which validation, error handling, observability, and tests can be built—the practices that actually make automation dependable.
References
The GNU Bash manual is the language reference, while ShellCheck provides static analysis for common shell defects.
