Sagar.BlogArticle
All posts
All posts
Bash

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

SectionPurpose
Safety optionsset -euo pipefail — fail fast, fail loud
ConstantsScript name, directory, version, temp dir
LoggingLevelised log functions → stderr for warnings/errors
CleanupTrap EXIT to always delete temp files and log failure
UsagePrinted on wrong args or -h
Arg parsingManual loop handles short and long flags cleanly
Dependency checkFail 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 bash shebang
  • set -euo pipefail safety options
  • All variables quoted
  • Functions use local for 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:

  1. Takes -d destination (backup destination, required)
  2. Takes -r N (retain last N backups, default 7)
  3. Takes one positional argument: the source directory
  4. Creates a timestamped backup: ${dest}/backup-YYYY-MM-DD.tar.gz
  5. Deletes older backups beyond the retention count
  6. Uses proper logging and cleanup