I built production-grade Bash using functions, getopts argument parsing, EXIT traps for cleanup, and arrays for structured data. Trap-based cleanup guaranteed temp files and locks were removed even when a script aborted mid-run.

Objective & Context

Robust automation needs argument parsing, guaranteed cleanup, and reuse. This lab adds getopts for flags, traps for deterministic teardown, functions for libraries, and arrays for lists, elevating the basics into maintainable tools used by the cron and sysadmin labs.

Learning Objectives

  • Parse command-line flags and arguments with getopts.
  • Guarantee temp-file cleanup with EXIT traps, even on failure.
  • Organize reusable logic into shell functions and small libraries.
  • Store and iterate lists safely with Bash arrays.
  • Combine strict mode, traps, and getopts into production-style scripts.

Environment & Prerequisites

Prerequisites

  • Bash 5.x with shellcheck installed.
  • Completion of the shell scripting basics lab (strict mode, quoting).
  • A task needing flags and temp files, such as a multi-host health check.
  • SSH access to at least one test host for the array loop (localhost works).

Step-by-Step Execution

1. Parse flags with getopts

#!/usr/bin/env bash
verbose=1
while getopts ":v:o:" opt; do
  case $opt in
    v) verbose=$OPTARG ;;
    o) out=$OPTARG ;;
    *) echo "usage: $0 -v N -o FILE" >&2; exit 2 ;;
  esac
done
shift $((OPTIND - 1))
echo "verbose=$verbose output=${out:-stdout}"

Expected output (script behavior):

$ ./deploy.sh -v 2 -o out.txt
verbose=2 output=out.txt
$ ./deploy.sh
verbose=1 output=stdout
$ ./deploy.sh -z
usage: ./deploy.sh -v N -o FILE

What this teaches: getopts loops over flags, placing each option letter in $opt and its argument in $OPTARG; the leading : in the optstring enables silent error handling so your *) case prints usage. shift $((OPTIND-1)) drops the parsed flags so positional arguments are available afterwards.

2. Guarantee cleanup with a trap

tmp=$(mktemp); trap 'rm -f "$tmp"' EXIT

What this teaches: trap registers a command that runs when the shell exits for any reason, including errors and Ctrl-C. Pairing mktemp with an EXIT trap is the standard leak-proof temp file pattern.

3. Iterate an array

hosts=(web1 web2 db1); for h in "${hosts[@]}"; do ssh "$h" uptime; done

Expected output (one line per host, in order):

 09:14:02 up 3 days,  2:11,  1 user,  load average: 0.12, 0.08, 0.05
 09:14:03 up 12 days,  6:44,  2 users, load average: 0.08, 0.11, 0.09
 09:14:04 up 40 days, 18:02,  3 users, load average: 0.31, 0.27, 0.22

What this teaches: "${hosts[@]}" expands each element as a separate word, preserving entries with spaces. Each ssh prints that host's uptime in turn. The names must resolve and accept your key; test the loop with a single entry like hosts=(localhost) before pointing it at real servers.

4. Package logic in a sourced function library

# lib.sh: shared helpers
log() { local level="$1"; shift; printf '[%s] %s: %s\n' "$(date +%F\ %T)" "$level" "$*"; }
require_cmd() { local cmd="$1"; command -v "$cmd" >/dev/null || { log ERROR "missing: $cmd"; return 1; }; }

# deploy.sh: use the library
#!/usr/bin/env bash
set -euo pipefail
source ./lib.sh
require_cmd rsync
log INFO "starting deploy"

Expected output:

$ ./deploy.sh
[2026-06-17 09:14:02] INFO: starting deploy

What this teaches: Functions package repeated logic behind names, and local keeps their variables out of global scope so helpers never clobber the caller's state. Sourcing a library with source ./lib.sh shares helpers across scripts without copy and paste. Note that functions are defined in the current shell, so the library path must be resolvable from wherever the script runs; scripts that may be invoked from other directories should source via a path relative to the script file itself.

How It Works

getopts exists because hand-rolled flag parsing with $1/$2 breaks the moment flags arrive in a new order or combine. The getopts builtin walks the argument list, handles clustered short flags, and separates option arguments cleanly, so -v 2 and -o out.txt work in any order. After the loop, shift $((OPTIND-1)) drops the parsed flags, leaving positional arguments.

traps solve the cleanup problem that strict mode alone cannot: a script killed by a signal or exiting early still needs its temp files removed and locks released. trap 'cleanup' EXIT registers a handler for every exit path, and stacking traps for ERR or INT covers interrupts too. Deterministic teardown is what makes a script safe to rerun and safe to kill.

Arrays fix the list problem. A space-separated string breaks on entries containing spaces; an array keeps each element intact, and "${arr[@]}" expands them as separate words. Functions then package repeated logic with local variables, so a 200-line script reads as a short main flow calling well-named helpers.

Validation & Testing

Abort the script mid-run (Ctrl-C) and confirm the trap still removes the temp file; pass invalid flags and confirm a clean usage error. Pass criteria: cleanup runs on every exit path and getopts rejects unknown options with exit 2.

Troubleshooting

SymptomLikely causeFix
getopts silently ignores an unknown flagThe optstring lacks a leading colon for custom handlingStart the optstring with : and handle \? and : in the case statement
Trap did not run on Ctrl-COnly EXIT was trapped; INT needs its own handlerAdd trap 'cleanup' INT TERM EXIT, or make the EXIT handler idempotent
Array loop splits entries with spacesUsed ${arr[*]} or unquoted ${arr[@]}Always iterate with "${arr[@]}" quoted
OPTARG is empty for -o without valueMissing-argument case not handledWith leading :, handle the : case to print usage for missing arguments
Function variable leaked into global scopeBash functions share scope unless declared localDeclare locals with local var=... at the top of each function
set -e kills the script inside getopts loopA failing command in the loop body triggers errexitGuard expected failures with || true or check statuses explicitly
Advanced: Troubleshooting
  • Leftover temp files: register the trap immediately after creating the resource.
  • Array word-splitting: always expand with "${arr[@]}" (quoted, @).
  • getopts misses long options: getopts handles short flags only; parse long flags manually.

Key Results

  • Guaranteed resource cleanup on all exit paths via EXIT traps.
  • Added robust flag parsing with getopts and usage errors.
  • Processed host lists safely with quoted array expansion.
  • Packaged reusable functions into a sourced library.

Key Takeaways

  • getopts parses flags in any order; shift $((OPTIND-1)) afterwards.
  • trap ... EXIT guarantees cleanup on every exit path.
  • mktemp plus an EXIT trap is the leak-proof temp file pattern.
  • "${arr[@]}" preserves list elements with spaces.
  • Functions with local variables turn long scripts into readable flows.

Practice Challenges

Challenge 1: Flag-driven backup

Goal: rewrite backup.sh to accept -s source, -d dest, and -k keep-count flags with getopts, printing usage on bad input.

Hint: validate required flags after the loop.

Challenge 2: Lockfile with trap

Goal: add a lockfile to a script so two copies cannot run at once, and ensure the trap removes the lock even on Ctrl-C.

Hint: trap the cleanup on EXIT, INT, and TERM.