# A Production Script Template — Bash / Shell Scripting

Source: https://www.skillbyai.com/en/bash/p-template

> Start every serious script from a template with strict mode, logging, usage and cleanup.

## Good habits in one file

Most of this course fits into a reusable template. Start with `#!/usr/bin/env bash` and `set -Eeuo pipefail`. Resolve the script's own directory so it works from anywhere. Define small helpers: `log` and `die` writing timestamped messages to stderr, `require_command` for dependencies. Parse options with `getopts` and print `usage`. Create temporary space with `mktemp -d` and remove it in an `EXIT` trap, plus an `ERR` trap that reports the failing line. Put the main logic in a `main` function called at the bottom with `main "$@"`, so the whole file is parsed before anything runs, which also protects against a script being edited while it executes. Support `--dry-run` for anything destructive, make the script **idempotent** (running it twice is safe), and keep it ShellCheck-clean. With this skeleton, a new script starts safe instead of being made safe later.

## The skeleton of a robust script

Header, helpers, option parsing, traps and a main function, in a fixed order.

![A tall document icon divided into five horizontal bands of different shades, with a small play icon at the bottom band.](assets/figures/bash/section-8-map.svg) — Figure 8.1 — Sections of a production-ready Bash script.

## Template

Copy, rename and fill in main.

```bash
#!/usr/bin/env bash
# rotate-logs.sh - compress and prune application logs
set -Eeuo pipefail

readonly SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
log() { printf '%s %s\n' "$(date -u +%FT%TZ)" "$*" >&2; }
die() { log "ERROR: $*"; exit 1; }
require_command() { for c in "$@"; do command -v "$c" > /dev/null || die "missing: $c"; done; }

usage() { printf 'usage: %s [-n] [-d DAYS] DIR\n' "${0##*/}"; }

main() {
    local dry_run=0 days=14 opt
    while getopts ":nd:h" opt; do
        case $opt in
            n) dry_run=1 ;;
            d) days=$OPTARG ;;
            h) usage; return 0 ;;
            *) usage >&2; return 2 ;;
        esac
    done
    shift $(( OPTIND - 1 ))
    local dir="${1:?$(usage)}"
    require_command find gzip

    workdir=$(mktemp -d)
    trap 'rm -rf "${workdir:?}"' EXIT
    trap 'log "failed at line $LINENO: $BASH_COMMAND"' ERR

    log "compressing logs older than 1 day in $dir"
    if (( dry_run )); then
        find "$dir" -name '*.log' -mtime +1 -print
    else
        find "$dir" -name '*.log' -mtime +1 -exec gzip -- {} +
        find "$dir" -name '*.log.gz' -mtime +"$days" -delete
    fi
    log "done"
}

main "$@"
```

## Idempotency makes scripts safe to rerun

Use `mkdir -p`, check before creating users or files, and compare desired and current state. When a script fails halfway, the fix should be to run it again, not to clean up by hand.

**Quiz:** Why wrap a script's logic in a main function called at the end with main "$@"?

- [ ] Bash requires a main function
- [ ] It makes the script run faster
- [x] The whole file is parsed before anything runs, and the structure is clearer
- [ ] It disables set -e

*Answer:* The whole file is parsed before anything runs, and the structure is clearer. Calling main at the bottom ensures every function is defined first and guards against partial execution.
