Study Projects

Automated Backup

A backup script must be safe: it should refuse to run twice at once, timestamp its archives, keep a rotation, and clean up after itself. This example shows every one of those habits.

#!/usr/bin/env bash
set -euo pipefail

readonly SRC="${1:?usage: backup.sh SOURCE_DIR [DEST_DIR]}"
readonly DEST="${2:-$HOME/backups}"
readonly KEEP=7
readonly STAMP="$(date +%Y%m%d-%H%M%S)"
readonly NAME="$(basename "$SRC")"

[[ -d "$SRC"  ]] || { echo "no such directory: $SRC"  >&2; exit 1; }
mkdir -p "$DEST"

# One archive per run; tar streams the tree.
archive="$DEST/${NAME}-${STAMP}.tar.gz"
tar -czf "$archive" -C "$(dirname "$SRC")" "$NAME"
echo "created $archive ($(du -h "$archive" | cut -f1))"

# Rotation: keep the newest N archives.
find "$DEST" -maxdepth 1 -name "${NAME}-*.tar.gz" -type f \
  | sort | head -n -"$KEEP" | xargs -r rm -f

echo "kept the last $KEEP archives"

System Report

A report script gathers facts, formats them, and writes a file that a human can scan. Note how it degrades gracefully when a tool is missing.

#!/usr/bin/env bash
set -euo pipefail

have() { command -v "$1" >/dev/null 2>&1; }
section() { printf '\n== %s ==\n' "$1"; }

section "Host"
echo "name : $(uname -n)"
echo "os   : $(uname -s) $(uname -r)"
echo "user : ${USER:-unknown}"
echo "uptime: $(uptime -p 2>/dev/null || uptime)"

section "CPU & Memory"
if have nproc; then echo "cores: $(nproc)"; fi
if [[ -r /proc/meminfo ]]; then
  awk '/MemTotal|MemAvailable/ { printf "%-16s %s %s\n", $1, $2, $3 }' /proc/meminfo
fi

section "Disk"
df -h --output=target,size,used,avail,pcent 2>/dev/null | head -10

section "Top processes"
ps -eo pid,comm,%cpu,%mem --sort=-%cpu 2>/dev/null | head -6

echo
echo "report finished at $(date)"

Bash has a built-in menu generator, select. Combined with a case, it makes a tidy admin tool in a few lines.

#!/usr/bin/env bash
set -euo pipefail

main() {
  PS3="choose an option: "          # the prompt select prints
  select opt in "Show date" "List files" "Disk usage" "Quit"; do
    case "$opt" in
      "Show date")  date ;;
      "List files") ls -lh ;;
      "Disk usage") df -h . ;;
      "Quit")       echo "bye"; break ;;
      *)            echo "invalid choice: $REPLY" >&2 ;;
    esac
  done
}

main "$@"

Service Health Check

A health check probes endpoints, retries with a bounded loop, and exits non-zero when something is down — exactly what a monitoring system expects.

#!/usr/bin/env bash
set -euo pipefail

readonly URL="${1:?usage: health.sh URL}"
readonly TRIES=5
readonly DELAY=2

for (( attempt = 1; attempt <= TRIES; attempt++ )); do
  if curl -sf --max-time 5 "$URL" >/dev/null; then
    echo "OK   $URL (attempt $attempt)"
    exit 0
  fi
  echo "FAIL $URL (attempt $attempt/$TRIES)" >&2
  (( attempt < TRIES )) && sleep "$DELAY"
done

echo "UNHEALTHY: $URL did not respond after $TRIES attempts" >&2
exit 1

Run it from cron or a systemd timer; a non-zero exit is all the alarm you need.

Project Bootstrap

A bootstrap script creates a reproducible skeleton. Make it idempotent — running it twice must not fail or duplicate work.

#!/usr/bin/env bash
set -euo pipefail

readonly ROOT="${1:-myapp}"
mkdir -p "$ROOT"/{src,tests,docs,scripts}

[[ -f "$ROOT/README.md" ]] || cat > "$ROOT/README.md" <<'EOF'
# myapp

Created by bootstrap.sh
EOF

# A git repository only if there is not one already.
if [[ ! -d "$ROOT/.git" ]]; then
  git -C "$ROOT" init -q
  printf 'created git repository in %s\n' "$ROOT"
fi

printf 'tree:\n'
find "$ROOT" -maxdepth 2 | sort

Next Steps

  • Add shellcheck to your editor and to CI.
  • Turn each project above into a testable function and cover it with bats.
  • Read other people's scripts — starting with your system's /etc/init.d and /usr/bin helpers.
  • Browse the curated links on the References & Tools page.