| name | defensive-bash |
| description | Production-grade defensive Bash scripting for server automation, monitoring, and DevOps tasks. Emphasizes safety, error handling, idempotency, and logging. |
Defensive Bash Scripting for Server Automation
This skill provides expertise in writing safe, reliable, and maintainable Bash scripts for server administration, Docker automation, and Moodle operations.
Core Principles
1. Script Safety Headers
ALWAYS start scripts with:
#!/bin/bash
set -euo pipefail
IFS=$'\n\t'
Explanation:
set -e: Exit on any error
set -u: Exit on undefined variable
set -o pipefail: Fail if any command in a pipeline fails
IFS: Prevent word splitting issues
2. Error Handling
ALWAYS implement proper error handling:
error_exit() {
echo "ERROR: $1" >&2
echo "Line: ${BASH_LINENO[0]}, Function: ${FUNCNAME[1]}" >&2
exit "${2:-1}"
}
trap 'error_exit "Script failed at line $LINENO"' ERR
some_command || error_exit "Command failed" 1
3. Input Validation
ALWAYS validate inputs:
if [[ $# -lt 1 ]]; then
echo "Usage: $0 <argument>" >&2
exit 1
fi
if ! [[ "$1" =~ ^[0-9]+$ ]]; then
error_exit "Argument must be a number"
fi
if [[ ! -f "$CONFIG_FILE" ]]; then
error_exit "Config file not found: $CONFIG_FILE"
fi
4. Safe File Operations
ALWAYS use safe file handling:
readonly TMPDIR="$(mktemp -d)"
trap 'rm -rf "$TMPDIR"' EXIT
backup_file() {
local file="$1"
local backup="${file}.backup.$(date +%Y%m%d_%H%M%S)"
cp -a "$file" "$backup" || error_exit "Backup failed for $file"
echo "$backup"
}
atomic_write() {
local content="$1"
local target="$2"
local tmpfile="${target}.tmp.$$"
echo "$content" > "$tmpfile" || error_exit "Write failed"
mv "$tmpfile" "$target" || error_exit "Atomic move failed"
}
5. Logging
ALWAYS implement comprehensive logging:
readonly LOG_FILE="/var/log/$(basename "$0" .sh).log"
readonly LOG_LEVEL="${LOG_LEVEL:-INFO}"
log() {
local level="$1"
shift
local message="$*"
local timestamp
timestamp="$(date '+%Y-%m-%d %H:%M:%S')"
echo "[${timestamp}] [${level}] ${message}" | tee -a "$LOG_FILE"
}
log_info() { log "INFO" "$@"; }
log_warn() { log "WARN" "$@"; }
log_error() { log "ERROR" "$@"; }
log_debug() { [[ "$LOG_LEVEL" == "DEBUG" ]] && log "DEBUG" "$@"; }
6. Idempotency
ALWAYS make operations idempotent:
if [[ ! -d "$TARGET_DIR" ]]; then
mkdir -p "$TARGET_DIR"
log_info "Created directory: $TARGET_DIR"
else
log_debug "Directory already exists: $TARGET_DIR"
fi
restart_service() {
local service="$1"
if systemctl is-active --quiet "$service"; then
systemctl restart "$service"
log_info "Restarted service: $service"
else
systemctl start "$service"
log_info "Started service: $service"
fi
}
7. Signal Handling
ALWAYS handle signals gracefully:
cleanup() {
local exit_code=$?
log_info "Cleaning up (exit code: $exit_code)..."
[[ -d "$TMPDIR" ]] && rm -rf "$TMPDIR"
[[ -n "$LOCKFILE" ]] && rm -f "$LOCKFILE"
log_info "Cleanup complete"
exit "$exit_code"
}
trap cleanup EXIT
trap 'log_warn "Received SIGINT, exiting..."; exit 130' INT
trap 'log_warn "Received SIGTERM, exiting..."; exit 143' TERM
8. Locking Mechanism
ALWAYS prevent concurrent execution:
readonly LOCKFILE="/var/run/$(basename "$0" .sh).lock"
acquire_lock() {
if [[ -f "$LOCKFILE" ]]; then
local pid
pid=$(<"$LOCKFILE")
if kill -0 "$pid" 2>/dev/null; then
error_exit "Script already running (PID: $pid)"
else
log_warn "Removing stale lock file"
rm -f "$LOCKFILE"
fi
fi
echo $$ > "$LOCKFILE"
}
release_lock() {
rm -f "$LOCKFILE"
}
trap release_lock EXIT
acquire_lock
Docker-Specific Patterns
Safe Container Execution
docker_exec() {
local container="$1"
shift
local cmd="$*"
if ! docker ps --format '{{.Names}}' | grep -q "^${container}$"; then
error_exit "Container not running: $container"
fi
log_debug "Executing in $container: $cmd"
docker exec "$container" bash -c "$cmd" || {
error_exit "Command failed in container $container: $cmd"
}
}
wait_for_container() {
local container="$1"
local timeout="${2:-60}"
local elapsed=0
log_info "Waiting for container: $container"
while [[ $elapsed -lt $timeout ]]; do
if docker ps --filter "name=${container}" --filter "status=running" | grep -q "$container";
log_info
0
2
((elapsed += ))
error_exit
}
Service Health Checks
check_service() {
local service="$1"
local container="${2:-moodle-dev}"
log_debug "Checking service: $service in $container"
if docker_exec "$container" "systemctl is-active --quiet $service"; then
log_info "Service running: $service"
return 0
else
log_error "Service not running: $service"
return 1
fi
}
check_http() {
local url="$1"
local expected_code="${2:-200}"
log_debug "Checking HTTP: $url"
local response_code
response_code=$(curl -s -o /dev/null -w '%{http_code}' "$url" || echo "000")
if [[ "$response_code" == "$expected_code" ]]; then
log_info "HTTP check passed: $url ($response_code)"
0
log_error
1
}
Moodle-Specific Patterns
Multi-Version Moodle Operations
moodle_cli() {
local version="$1"
local script="$2"
shift 2
local args="$*"
local php_cmd moodle_dir
case "$version" in
"4.1")
php_cmd="php8.1"
moodle_dir="/opt/moodle-MOODLE_401_STABLE"
;;
"4.5")
php_cmd="php8.2"
moodle_dir="/opt/moodle-MOODLE_405_STABLE"
;;
"5.1")
php_cmd="php8.3"
moodle_dir="/opt/moodle-MOODLE_501_STABLE"
;;
"dh-prod")
php_cmd="php8.1"
moodle_dir="/workspace/moodle-dh-prod"
;;
*)
error_exit "Invalid Moodle version: $version"
;;
esac
local full_script="${moodle_dir}/admin/cli/${script}"
if [[ ! -f "$full_script" ]]; then
error_exit "Script not found: $full_script"
fi
log_info "Running Moodle $version: $script $args"
docker_exec moodle-dev
}
() {
versions=( )
version ;
log_info
moodle_cli || log_error
}
Best Practices Summary
- Always use
set -euo pipefail at script start
- Validate all inputs before using them
- Log all significant actions with timestamps
- Handle errors explicitly with meaningful messages
- Make operations idempotent when possible
- Clean up resources in trap handlers
- Use locks for critical sections
- Test before production use
- Document assumptions and requirements
- Version control all scripts
Common Anti-Patterns to Avoid
❌ Don't:
docker exec moodle-dev php script.php
file=$1
cat $file
command || true
rm -rf $DIR/*
✅ Do:
docker_exec moodle-dev "php script.php" || error_exit "PHP script failed"
file="$1"
cat "$file"
command || {
log_error "Command failed"
return 1
}
if [[ -z "$DIR" ]] || [[ ! -d "$DIR" ]]; then
error_exit "Invalid directory: $DIR"
fi
rm -rf "${DIR:?}/"*
Testing Scripts
Always test with:
shellcheck script.sh
bash -n script.sh
bash -x script.sh
./script.sh ""
./script.sh "../../etc/passwd"
./script.sh "$(printf '\0')"
Apply these patterns consistently for reliable, maintainable server automation scripts.