Shell Scripts
Bash script structure, lifecycle progression, input validation, and production patterns.
Script Header
The strict-mode header — every script starts here, no exceptions
#!/bin/bash
set -euo pipefail
# -e: exit on first error
# -u: treat unset variables as errors
# -o pipefail: pipe fails if ANY stage fails, not just the last
Without pipefail, curl bad | jq . exits 0 because jq succeeds on empty input. The curl failure is silently swallowed.
Script with usage and argument parsing
#!/bin/bash
set -euo pipefail
usage() {
cat <<EOF
Usage: $(basename "$0") [-v] [-o OUTPUT] <input-file>
Options:
-v Verbose output
-o OUTPUT Output file (default: stdout)
-h Show this help
EOF
exit 1
}
verbose=false
output="/dev/stdout"
while getopts ":vo:h" opt; do
case $opt in
v) verbose=true ;;
o) output="$OPTARG" ;;
h) usage ;;
:) echo "Option -$OPTARG requires an argument" >&2; exit 1 ;;
\?) echo "Unknown option -$OPTARG" >&2; usage ;;
esac
done
shift $((OPTIND - 1))
[[ $# -lt 1 ]] && { echo "Error: input file required" >&2; usage; }
input="$1"
[[ -f "$input" ]] || { echo "Error: $input not found" >&2; exit 1; }
Lifecycle Pattern
Four-stage script progression
Stage 1: /tmp/experiment.sh — throwaway, testing an idea
Stage 2: ~/scripts/staging/ — works, needs refinement
Stage 3: ~/scripts/ — tested, documented, versioned
Stage 4: ~/.local/bin/ — PATH-accessible, production
Stage 1 — quick experiment in /tmp/
cat > /tmp/check-ports.sh << 'EOF'
#!/bin/bash
for port in 22 80 443 8080; do
timeout 2 bash -c "echo >/dev/tcp/localhost/$port" 2>/dev/null \
&& echo "Port $port: OPEN" \
|| echo "Port $port: CLOSED"
done
EOF
chmod +x /tmp/check-ports.sh
/tmp/check-ports.sh
Stage 4 — production script with logging and error handling
#!/bin/bash
set -euo pipefail
readonly SCRIPT_NAME="$(basename "$0")"
readonly LOG_FILE="/var/log/${SCRIPT_NAME%.sh}.log"
log() { printf '%s [%s] %s\n' "$(date '+%Y-%m-%d %H:%M:%S')" "$1" "$2" | tee -a "$LOG_FILE"; }
die() { log "ERROR" "$1"; exit "${2:-1}"; }
log "INFO" "Starting $SCRIPT_NAME"
trap 'log "ERROR" "Failed at line $LINENO"' ERR
trap 'log "INFO" "Finished"' EXIT
Idempotent Operations
Guard with grep — only add if not already present
entry="10.50.1.20 ise-01"
grep -qF "$entry" /etc/hosts || echo "$entry" | sudo tee -a /etc/hosts
Guard with command check — only install if missing
command -v jq &>/dev/null || sudo pacman -S --noconfirm jq
Input Validation
Validate required environment variables
: "${API_TOKEN:?ERROR: API_TOKEN not set}"
: "${API_URL:?ERROR: API_URL not set}"
The : is a no-op. ${var:?message} expands var if set and non-empty, otherwise prints message to stderr and exits (due to set -e).
Validate file arguments
input="${1:?Usage: $(basename "$0") <input-file>}"
[[ -f "$input" ]] || { echo "Not a file: $input" >&2; exit 1; }
[[ -r "$input" ]] || { echo "Not readable: $input" >&2; exit 1; }
[[ -s "$input" ]] || { echo "Empty file: $input" >&2; exit 1; }
Temporary Files and Cleanup
mktemp with trap — guaranteed cleanup
tmpfile=$(mktemp)
tmpdir=$(mktemp -d)
trap 'rm -f "$tmpfile"; rm -rf "$tmpdir"' EXIT
curl -sf https://api.example.com/data > "$tmpfile"
process_data "$tmpfile" "$tmpdir"
# Cleanup happens automatically on exit, error, or signal
Interactive Confirmation
Confirm before destructive action
confirm() {
local prompt="${1:-Are you sure?}"
read -rp "$prompt [y/N] " response
[[ "$response" =~ ^[Yy]$ ]]
}
confirm "Delete all logs older than 30 days?" \
&& find /var/log -name '*.log' -mtime +30 -delete
Practical Patterns
Service health check — verify-before pattern
#!/bin/bash
set -euo pipefail
services=("nginx" "sshd" "docker")
failed=0
for svc in "${services[@]}"; do
if systemctl is-active --quiet "$svc"; then
printf " %-15s %s\n" "$svc" "OK"
else
printf " %-15s %s\n" "$svc" "FAILED"
(( failed++ ))
fi
done
exit "$failed"
Git multi-remote push
#!/bin/bash
set -euo pipefail
branch=$(git rev-parse --abbrev-ref HEAD)
for remote in $(git remote); do
echo "Pushing to $remote/$branch..."
git push "$remote" "$branch" &
done
wait
echo "All remotes updated"
See Also
-
Exit Codes — set -euo pipefail, script safety
-
Traps — signal handling and cleanup
-
Script Lifecycle — four-stage progression