The Bash Script Template — Production-Ready Boilerplate
A complete, annotated Bash script template with error handling, argument parsing, logging, cleanup traps, and a main() function. Copy it for every new script.
February 3, 20268 min read
BashTemplateBest PracticesScripting
A good script template condenses everything you've learned into a starting point you can paste at the top of every new script. Here's a comprehensive template with explanations for each section.
The Complete Template
template.sh
#!/usr/bin/env bash
# =============================================================================
# SCRIPT: template.sh
# DESCRIPTION: Brief one-line description of what this script does
# AUTHOR: Your Name
# DATE: 2026-02-03
# USAGE: ./template.sh [OPTIONS] <required-arg>
# =============================================================================
# ── Safety Options ────────────────────────────────────────────────────────────
set -euo pipefail
IFS=$'\n\t' # safer IFS (word-splitting only on newline and tab)
# ── Constants ─────────────────────────────────────────────────────────────────
readonly SCRIPT_NAME="${BASH_SOURCE[0]##*/}"
readonly SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
readonly SCRIPT_VERSION="1.0.0"
readonly TMPDIR_WORK="$(mktemp -d)"
# ── Logging ───────────────────────────────────────────────────────────────────
LOG_LEVEL=${LOG_LEVEL:-1} # 0=DEBUG 1=INFO 2=WARN 3=ERROR
function log {
local level_name="$1" level_num="$2"; shift 2
local ts; ts=$(date '+%H:%M:%S')
[[ $level_num -ge $LOG_LEVEL ]] || return 0
if [[ $level_num -ge 2 ]]; then
printf '[%s] [%s] %s\n' "$ts" "$level_name" "$*" >&2
else
printf '[%s] [%s] %s\n' "$ts" "$level_name" "$*"
fi
}
function log_debug { log "DEBUG" 0 "$@"; }
function log_info { log "INFO " 1 "$@"; }
function log_warn { log "WARN " 2 "$@"; }
function log_error { log "ERROR" 3 "$@"; }
function die { log_error "$@"; exit 1; }
# ── Cleanup ───────────────────────────────────────────────────────────────────
function cleanup {
local exit_code=$?
rm -rf "$TMPDIR_WORK"
[[ $exit_code -ne 0 ]] && log_error "Script failed (exit code: $exit_code)"
exit $exit_code
}
trap cleanup EXIT
trap 'die "Interrupted"' INT TERM
# ── Usage ─────────────────────────────────────────────────────────────────────
function usage {
cat >&2 <<-EOF
Usage: $SCRIPT_NAME [OPTIONS] <required-arg>
Options:
-v, --verbose Verbose output (LOG_LEVEL=0)
-n, --dry-run Dry run — print what would be done
-h, --help Show this help
Environment:
LOG_LEVEL Log verbosity: 0=DEBUG, 1=INFO, 2=WARN, 3=ERROR (default: 1)
EOF
exit 1
}
# ── Argument Parsing ──────────────────────────────────────────────────────────
VERBOSE=false
DRY_RUN=false
while [[ $# -gt 0 ]]; do
case "$1" in
-v | --verbose) VERBOSE=true; LOG_LEVEL=0; shift ;;
-n | --dry-run) DRY_RUN=true; shift ;;
-h | --help) usage ;;
--) shift; break ;;
-*) die "Unknown option: $1" ;;
*) break ;;
esac
done
# Remaining positional args
[[ $# -ge 1 ]] || { log_error "Missing required argument"; usage; }
REQUIRED_ARG="$1"
shift
# ── Dependency Check ──────────────────────────────────────────────────────────
function require_commands {
local ok=true
for cmd in "$@"; do
command -v "$cmd" &>/dev/null || { log_error "Required: $cmd"; ok=false; }
done
$ok || exit 1
}
require_commands curl jq # add your dependencies
# ── Main Logic ─────────────────────────────────────────────────────────────────
function main {
log_info "Starting $SCRIPT_NAME v$SCRIPT_VERSION"
log_debug "Args: required=$REQUIRED_ARG, verbose=$VERBOSE, dry_run=$DRY_RUN"
if $DRY_RUN; then
log_info "DRY RUN — would process: $REQUIRED_ARG"
return 0
fi
# ── Your logic here ──
log_info "Processing: $REQUIRED_ARG"
log_info "Done"
}
main "$@"What Each Section Does
Template section guide
| Section | Purpose |
|---|---|
| Safety options | set -euo pipefail — fail fast, fail loud |
| Constants | Script name, directory, version, temp dir |
| Logging | Levelised log functions → stderr for warnings/errors |
| Cleanup | Trap EXIT to always delete temp files and log failure |
| Usage | Printed on wrong args or -h |
| Arg parsing | Manual loop handles short and long flags cleanly |
| Dependency check | Fail early if required tools are missing |
| main() | Keep the logic in a named function for readability |
Checklist for Every Script
Before you ship a script
-
#!/usr/bin/env bashshebang -
set -euo pipefailsafety options - All variables quoted
- Functions use
localfor their variables - Errors go to stderr (
>&2) - Temp files cleaned up with
trap cleanup EXIT - Arguments validated with usage message
- ShellCheck passes with no warnings
- Script tested with
bash -n script.sh
Quick Check
Why does the template use `readonly SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"` instead of `SCRIPT_DIR=$(dirname $0)`?
Exercise
Use the template to create a real script backup-dir.sh that:
- Takes
-d destination(backup destination, required) - Takes
-r N(retain last N backups, default 7) - Takes one positional argument: the source directory
- Creates a timestamped backup:
${dest}/backup-YYYY-MM-DD.tar.gz - Deletes older backups beyond the retention count
- Uses proper logging and cleanup