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))"
doneBash
# 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.
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.