ShBash · Lesson 7 of 8

Mini Project: Backup Script

A practical backup script that compresses directories, keeps the last N backups, and logs everything. Something you might actually use.

Bash
#!/usr/bin/env bash
# backup.sh — Simple directory backup script
set -euo pipefail  # exit on error, undefined vars, pipe failures

BACKUP_DIR="${HOME}/backups"
KEEP_LAST=5
LOG_FILE="${BACKUP_DIR}/backup.log"

# ─── Helpers ────────────────────────────────────────────────────────────────

log() {
    local msg="[$(date '+%Y-%m-%d %H:%M:%S')] $*"
    echo "$msg"
    echo "$msg" >> "$LOG_FILE"
}

die() {
    log "ERROR: $*"
    exit 1
}

usage() {
    cat << 'USAGE'
Usage: backup.sh <source-directory> [backup-name]
  source-directory  Directory to back up
  backup-name       Optional name (default: directory name)
USAGE
    exit 1
}

# ─── Validation ─────────────────────────────────────────────────────────────

[ $# -ge 1 ] || usage

SOURCE="${1%/}"    # remove trailing slash
NAME="${2:-$(basename "$SOURCE")}"    # use basename if name not provided

[ -d "$SOURCE" ] || die "Source directory not found: $SOURCE"
command -v tar &>/dev/null || die "tar is required but not installed"

# ─── Setup ──────────────────────────────────────────────────────────────────

mkdir -p "$BACKUP_DIR"

TIMESTAMP=$(date '+%Y%m%d_%H%M%S')
BACKUP_FILE="${BACKUP_DIR}/${NAME}_${TIMESTAMP}.tar.gz"

# ─── Backup ─────────────────────────────────────────────────────────────────

log "Starting backup of '$SOURCE'..."
log "Target: $BACKUP_FILE"

# Calculate source size
SOURCE_SIZE=$(du -sh "$SOURCE" 2>/dev/null | cut -f1)
log "Source size: $SOURCE_SIZE"

# Create compressed archive
if tar -czf "$BACKUP_FILE" -C "$(dirname "$SOURCE")" "$(basename "$SOURCE")"; then
    BACKUP_SIZE=$(du -sh "$BACKUP_FILE" | cut -f1)
    log "Backup created: $BACKUP_FILE ($BACKUP_SIZE)"
else
    die "tar failed"
fi

# ─── Cleanup old backups ────────────────────────────────────────────────────

# Count existing backups for this name
EXISTING=$(find "$BACKUP_DIR" -name "${NAME}_*.tar.gz" | sort)
COUNT=$(echo "$EXISTING" | grep -c "." 2>/dev/null || true)

if [ "$COUNT" -gt "$KEEP_LAST" ]; then
    DELETE_COUNT=$((COUNT - KEEP_LAST))
    log "Removing $DELETE_COUNT old backup(s) (keeping last $KEEP_LAST)..."
    echo "$EXISTING" | head -n "$DELETE_COUNT" | while IFS= read -r old; do
        log "  Removing: $(basename "$old")"
        rm -f "$old"
    done
fi

log "Backup complete!"
log "Backups for '$NAME':"
find "$BACKUP_DIR" -name "${NAME}_*.tar.gz" | sort | while IFS= read -r f; do
    log "  $(basename "$f") ($(du -sh "$f" | cut -f1))"
done
Bash
# Make it executable
chmod +x backup.sh

# Run it
./backup.sh ~/Documents
./backup.sh ~/projects my-code

# Schedule with cron (run daily at 2am):
crontab -e
# Add: 0 2 * * * /path/to/backup.sh /home/user/Documents
◆ Note
The set -euo pipefail at the top is a safety triple: -e exits on error, -u errors on undefined variables, -o pipefail catches errors in pipelines. Put it at the top of every Bash script you write.