Advanced Bash Scripting

Advanced bash scripting builds on fundamentals to create production-quality scripts that handle errors gracefully, clean up after themselves, process large files efficiently, and are safe to run in automated environments. The difference between a beginner script and a production script is mostly in error handling: what happens when a command fails, what state is left behind, and how much information is logged for debugging.

Error handling and traps

nano /usr/local/bin/safe-deploy.sh

safe-deploy.sh — trap-based cleanup

#!/bin/bash
set -euo pipefail

TEMP_DIR=$(mktemp -d)    # Create unique temp directory
LOCK_FILE="/var/run/deploy.lock"

# Cleanup function — runs on ANY exit (success or failure):
cleanup() {
    local exit_code=$?
    rm -rf "${TEMP_DIR}"
    rm -f "${LOCK_FILE}"
    if [[ $exit_code -ne 0 ]]; then
        echo "FAILED: deploy script exited with code ${exit_code}" >&2
    fi
}

# Register cleanup to run on EXIT, INT (Ctrl+C), TERM:
trap cleanup EXIT INT TERM

# Prevent concurrent runs:
if [[ -f "${LOCK_FILE}" ]]; then
    echo "Another deploy is running (lock: ${LOCK_FILE})" >&2
    exit 1
fi
touch "${LOCK_FILE}"

echo "Deploying to ${TEMP_DIR}..."
# ... deployment commands here ...
echo "Deploy complete"

Arrays and string manipulation

# Bash arrays:
SERVERS=("web-01" "web-02" "db-01")

# Loop over array:
for server in "${SERVERS[@]}"; do
    echo "Checking ${server}..."
    ssh "${server}" uptime
done

# Array length:
echo "Server count: ${#SERVERS[@]}"

# String manipulation:
FILENAME="backup-2025-06-09.tar.gz"
echo "${FILENAME%%.*}"        # backup-2025-06-09 (remove longest suffix match .*)
echo "${FILENAME##*.}"        # gz (remove longest prefix match *.)
echo "${FILENAME/backup/archive}"  # archive-2025-06-09.tar.gz (replace)

DATE_STR="2025-06-09"
YEAR="${DATE_STR:0:4}"        # 2025 (substring: start, length)
MONTH="${DATE_STR:5:2}"       # 06

# Parse CSV output:
while IFS=',' read -r name size status; do
    echo "Name: ${name}, Size: ${size}, Status: ${status}"
done < servers.csv

File and log processing

nano /usr/local/bin/log-summary.sh

log-summary.sh — processing logs efficiently

#!/bin/bash
set -euo pipefail

LOG_FILE="${1:-/var/log/nginx/access.log}"
OUTPUT_FILE="/tmp/log-summary-$(date +%Y%m%d).txt"

# Check file exists and is readable:
if [[ ! -r "${LOG_FILE}" ]]; then
    echo "Cannot read log file: ${LOG_FILE}" >&2
    exit 1
fi

echo "Analyzing: ${LOG_FILE}" > "${OUTPUT_FILE}"
echo "Generated: $(date)" >> "${OUTPUT_FILE}"
echo "" >> "${OUTPUT_FILE}"

# Top 10 IP addresses:
echo "=== Top 10 Client IPs ===" >> "${OUTPUT_FILE}"
awk '{print $1}' "${LOG_FILE}" | sort | uniq -c | sort -rn | head -10 >> "${OUTPUT_FILE}"

# HTTP status code summary:
echo "=== Status Codes ===" >> "${OUTPUT_FILE}"
awk '{print $9}' "${LOG_FILE}" | sort | uniq -c | sort -rn >> "${OUTPUT_FILE}"

# 404 errors:
echo "=== 404 Not Found URLs ===" >> "${OUTPUT_FILE}"
awk '$9 == 404 {print $7}' "${LOG_FILE}" | sort | uniq -c | sort -rn | head -20 >> "${OUTPUT_FILE}"

cat "${OUTPUT_FILE}"

Script best practices

Production script checklist:
  set -euo pipefail     → fail fast on errors
  trap cleanup EXIT     → always clean up temp files and locks
  [[ $# -ne N ]]        → validate argument count
  [[ -f "${file}" ]]    → validate inputs before using them
  local variables       → scope variables inside functions
  Log with timestamps   → echo "[$(date)] ..." >> "${LOG_FILE}"
  Redirect errors       → echo "Error" >&2 (not stdout)
  Lock files            → prevent concurrent execution
  Dry-run mode          → add --dry-run flag that echoes commands instead of running
# Dry-run pattern (safe to test):
DRY_RUN=false
[[ "${1:-}" == "--dry-run" ]] && DRY_RUN=true

run_cmd() {
    if $DRY_RUN; then
        echo "[DRY-RUN] $*"
    else
        "$@"
    fi
}

# Usage:
run_cmd rm -rf /old/directory
run_cmd systemctl restart nginx

Conclusion

The trap cleanup EXIT pattern is the single most important advanced technique for production scripts: it ensures temp files and lock files are always removed regardless of how the script exits — success, error, or Ctrl+C. Without it, failed scripts leave lock files that block future runs and temp files that fill disk. Combine this with lock files (/var/run/scriptname.lock) to prevent cron jobs from running concurrently when a previous instance is still running.

Need help with Ubuntu administration?

Work directly with Muhammad Irfan Aslam for Ubuntu Server, Linux, cloud, Docker, DevOps, CI/CD, or infrastructure troubleshooting support.

Work with Muhammad Irfan Aslam