← Back to Bash Course | Chapter 14: Best Practices & Real-World Scripting | Lesson 7 of 7

Capstone: A Real-World Backup/Cleanup Script

This final example combines many of the ideas from the course into one small but realistic script that backs up some files and cleans up old ones safely.

Setting Up the Script Header and Logging

The capstone script starts with strict mode and a small timestamped logger, both covered earlier in this course, establishing the safety net the rest of the script relies on.

Example: Setting Up the Script Header and Logging

bash
#!/bin/bash
set -euo pipefail

log() {
    echo "[$(date +'%H:%M:%S')] $1"
}

log "backup script starting"

Validating Preconditions Before Backing Up

Before doing anything, the script confirms the source directory exists and creates the backup destination idempotently with mkdir -p, so it fails fast and clearly if something is missing rather than partway through.

Example: Validating Preconditions Before Backing Up

bash
#!/bin/bash
set -euo pipefail

source_dir="source_data"
backup_dir="backups"

mkdir -p "$source_dir"
echo "sample" > "$source_dir/file1.txt"

if [[ ! -d $source_dir ]]; then
    echo "source directory missing, aborting" >&2
    exit 1
fi
mkdir -p "$backup_dir"
echo "preconditions satisfied: source and backup directories are ready"

rm -rf "$source_dir" "$backup_dir"

The Full Backup and Cleanup Script

This ties several concepts together: a main() entry point, a logging function, precondition checks, a timestamped backup archive, and cleanup of backups older than a retention count, all wrapped in strict mode with a clear final exit status.

Example: The Full Backup and Cleanup Script

bash
#!/bin/bash
set -euo pipefail

source_dir="source_data"
backup_dir="backups"
retain_count=3

log() {
    echo "[$(date +'%H:%M:%S')] $1"
}

validate() {
    if [[ ! -d $source_dir ]]; then
        log "ERROR: source directory '$source_dir' does not exist"
        exit 1
    fi
    mkdir -p "$backup_dir"
}

run_backup() {
    local stamp
    stamp=$(date +'%Y%m%d_%H%M%S')
    local archive="$backup_dir/backup_$stamp.tar"
    tar -cf "$archive" -C "$source_dir" .
    log "created backup: $archive"
}

cleanup_old_backups() {
    local count
    count=$(find "$backup_dir" -name 'backup_*.tar' | wc -l)
    if (( count > retain_count )); then
        find "$backup_dir" -name 'backup_*.tar' | sort | head -n "$((count - retain_count))" | xargs rm -f
        log "pruned old backups, kept the most recent $retain_count"
    else
        log "no pruning needed, $count backup(s) present"
    fi
}

main() {
    mkdir -p "$source_dir"
    echo "data" > "$source_dir/file.txt"

    validate
    run_backup
    sleep 1
    run_backup
    cleanup_old_backups

    log "backup script finished successfully"
    rm -rf "$source_dir" "$backup_dir"
}

main "$@"
Common Mistakes
  1. Writing a backup script without checking that the source directory actually exists first, which can produce a confusing empty backup.
  2. Deleting old backups without first confirming the new backup succeeded, risking losing all copies of the data at once.
  3. Skipping logging in a script meant to run unattended (e.g. via cron), making failures impossible to diagnose after the fact.
Chapter Summary
  • A well-built real-world script combines strict mode, functions with a main() entry point, logging, validation, and idempotent operations.
  • Validating inputs and preconditions before doing anything destructive prevents a whole class of avoidable failures.
  • Structuring backup/cleanup logic as small functions (backup, then cleanup) keeps each responsibility easy to reason about independently.
  • Even a small script benefits from the same discipline as a large one: check inputs, log progress, handle errors, and exit with a meaningful status.

Login to run this code

C/C++/Java/PHP execution requires a free account. Your code is saved — you'll land right back in the editor after logging in.