Production-Grade Bash
The patterns that separate a throwaway script from a production-grade one — structure, logging, idempotency, configuration, and testing.
The Standard Production Script Header
#!/usr/bin/env bash
set -euo pipefail
IFS=$'\n\t' # safer default word-splitting (newline/tab only, not plain space)
readonly SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
readonly SCRIPT_NAME="$(basename "$0")"
Resolving SCRIPT_DIR this way lets a script reliably find files relative to itself regardless of the current working directory it was invoked from.
Structuring a Real Script
#!/usr/bin/env bash
set -euo pipefail
# --- Configuration ---
readonly LOG_FILE="/var/log/myapp/deploy.log"
readonly MAX_RETRIES=3
# --- Logging ---
log() { echo "[$(date '+%Y-%m-%d %H:%M:%S')] [INFO] $*" | tee -a "$LOG_FILE"; }
error() { echo "[$(date '+%Y-%m-%d %H:%M:%S')] [ERROR] $*" | tee -a "$LOG_FILE" >&2; }
# --- Cleanup ---
cleanup() { log "Script finished with exit code $?"; }
trap cleanup EXIT
# --- Functions ---
validate_environment() {
[ -f "/etc/myapp/config.yaml" ] || { error "Config file missing"; exit 1; }
}
deploy() {
log "Starting deployment..."
# ... actual deploy logic ...
log "Deployment complete"
}
# --- Main ---
main() {
validate_environment
deploy
}
main "$@"
Idempotency: Safe to Run Twice
A production script should be safe to re-run without causing harm if it’s interrupted and retried.
# NOT idempotent — fails the second time
mkdir /opt/myapp
# Idempotent
mkdir -p /opt/myapp
# NOT idempotent — duplicates the line on a re-run
echo "export PATH=/opt/myapp/bin:\$PATH" >> ~/.bashrc
# Idempotent — checks before appending
grep -qxF 'export PATH=/opt/myapp/bin:$PATH' ~/.bashrc || \
echo 'export PATH=/opt/myapp/bin:$PATH' >> ~/.bashrc
Configuration: Environment Variables Over Hardcoding
ENVIRONMENT="${ENVIRONMENT:-production}"
LOG_LEVEL="${LOG_LEVEL:-info}"
CONFIG_FILE="${CONFIG_FILE:-/etc/myapp/config.yaml}"
# Load from a .env-style file if present
if [ -f ".env" ]; then
set -a # auto-export every variable sourced below
source .env
set +a
fi
Structured (Parseable) Logging
log_json() {
local level="$1"; shift
printf '{"timestamp":"%s","level":"%s","message":"%s"}\n' \
"$(date -u +'%Y-%m-%dT%H:%M:%SZ')" "$level" "$*"
}
log_json "INFO" "Deployment started"
log_json "ERROR" "Health check failed"
Structured JSON logs are far easier to ship into centralized logging (ELK, CloudWatch, Loki) than free-form text.
Dry-Run Mode
DRY_RUN="${DRY_RUN:-false}"
run() {
if [ "$DRY_RUN" = "true" ]; then
echo "[DRY RUN] Would execute: $*"
else
"$@"
fi
}
run rm -rf /tmp/old_cache
run systemctl restart myapp
DRY_RUN=true ./cleanup.sh # preview destructive operations before actually running them
Basic Testing With bats
# deploy.bats — using the bats-core testing framework
@test "validate_environment fails without config file" {
run ./deploy.sh
[ "$status" -eq 1 ]
[[ "$output" =~ "Config file missing" ]]
}
bats deploy.bats
Production Considerations
- Every production script should be idempotent — safe to re-run after a partial failure without duplicating work or corrupting state.
- Support a
DRY_RUNmode for anything destructive — it costs little to add and prevents a very expensive mistake. - Structured logging (JSON) pays for itself the moment scripts run somewhere their output needs to be searched or alerted on, rather than just eyeballed in a terminal.
Quick Interview Answer
“Production-grade Bash means:
set -euo pipefailat the top, amain()function as the explicit entry point,local-scoped functions, structured logging instead of scatteredecho,trap ... EXITfor cleanup, and idempotency — the script should be safe to re-run if it fails partway through. Supporting aDRY_RUNmode for destructive operations and testing with a framework likebatsare the marks of a script that’s actually trusted in production, not just something that happened to work once.”
Common Mistakes
- Writing scripts that aren’t idempotent, so a retry after a partial failure duplicates work or errors out entirely.
- No structured logging, making it hard to search/alert on script output at scale.
- No dry-run capability for destructive scripts, making every real run a leap of faith.
Add More Questions to This Guide
Know a question that should be here? Share it and help the community!
Open Google Form