Guide Bash-Scripting Advanced

Production-Grade Bash

The patterns that separate a throwaway script from a production-grade one — structure, logging, idempotency, configuration, and testing.

3 min read

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_RUN mode 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 pipefail at the top, a main() function as the explicit entry point, local-scoped functions, structured logging instead of scattered echo, trap ... EXIT for cleanup, and idempotency — the script should be safe to re-run if it fails partway through. Supporting a DRY_RUN mode for destructive operations and testing with a framework like bats are 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