Shell Scripting
Shell Scripting Projects
35 hands-on projects progressing from basic shell scripts to expert-level automation systems for Raspberry Pi and other SBCs. Each project builds on previous skills.
Beginner Projects (1–8)
Project 1 — Hello, System!
Print a personalised system greeting with key info.
#!/bin/bash
echo "==============================="
echo " Welcome, $USER!"
echo "==============================="
echo "Hostname : $(hostname)"
echo "Date/Time: $(date '+%Y-%m-%d %H:%M:%S')"
echo "OS : $(lsb_release -ds 2>/dev/null || uname -o)"
echo "Kernel : $(uname -r)"
echo "Uptime : $(uptime -p)"
echo "IP : $(hostname -I | awk '{print $1}')"
echo "==============================="
Project 2 — Directory Organiser
Sort files in a folder into subfolders by extension.
#!/bin/bash
TARGET="${1:-.}"
cd "$TARGET" || exit 1
for FILE in *; do
[ -f "$FILE" ] || continue
EXT="${FILE##*.}"
[ "$EXT" = "$FILE" ] && EXT="no_extension"
mkdir -p "$EXT"
mv "$FILE" "$EXT/"
echo "Moved $FILE → $EXT/"
done
echo "Done organising $TARGET"
Usage: ./organise.sh ~/Downloads
Project 3 — Timestamped Backup
Backup a directory with a date-stamped archive.
#!/bin/bash
SOURCE="${1:?Usage: $0 <source_dir> [dest_dir]}"
DEST="${2:-$HOME/backups}"
TIMESTAMP=$(date +%Y%m%d_%H%M%S)
ARCHIVE="$DEST/backup_$(basename "$SOURCE")_$TIMESTAMP.tar.gz"
mkdir -p "$DEST"
tar -czf "$ARCHIVE" "$SOURCE"
SIZE=$(du -sh "$ARCHIVE" | cut -f1)
echo "Backup complete: $ARCHIVE ($SIZE)"
Usage: ./backup.sh /home/pi/projects
Project 4 — Simple Calculator
#!/bin/bash
echo -n "Enter first number : "; read A
echo -n "Enter operator (+,-,*,/) : "; read OP
echo -n "Enter second number: "; read B
case "$OP" in
+) echo "Result: $(echo "$A + $B" | bc)" ;;
-) echo "Result: $(echo "$A - $B" | bc)" ;;
\*) echo "Result: $(echo "$A * $B" | bc)" ;;
/) echo "Result: $(echo "scale=4; $A / $B" | bc)" ;;
*) echo "Unknown operator: $OP"; exit 1 ;;
esac
Project 5 — Command Checker
Check whether required tools are installed before a project setup.
#!/bin/bash
REQUIRED=(git python3 cmake make gcc openocd)
MISSING=()
for CMD in "${REQUIRED[@]}"; do
if command -v "$CMD" &>/dev/null; then
echo "[OK] $CMD ($(command -v "$CMD"))"
else
echo "[MISSING] $CMD"
MISSING+=("$CMD")
fi
done
if [ ${#MISSING[@]} -gt 0 ]; then
echo ""
echo "Install missing tools with:"
echo " sudo apt install -y ${MISSING[*]}"
exit 1
fi
echo "All tools available."
Project 6 — Countdown Timer
#!/bin/bash
SECONDS_LEFT="${1:?Usage: $0 <seconds>}"
while [ "$SECONDS_LEFT" -gt 0 ]; do
printf "\r Remaining: %02d:%02d " \
$((SECONDS_LEFT / 60)) $((SECONDS_LEFT % 60))
sleep 1
((SECONDS_LEFT--))
done
echo ""
echo "Time's up!"
# Play a beep if speakers are available
command -v aplay &>/dev/null && aplay /usr/share/sounds/alsa/Front_Left.wav 2>/dev/null
Project 7 — Large File Finder
Find and report files larger than a given size.
#!/bin/bash
DIR="${1:-$HOME}"
MIN_MB="${2:-100}"
echo "Searching $DIR for files larger than ${MIN_MB}MB..."
echo ""
find "$DIR" -type f -size +"${MIN_MB}M" -printf '%s %p\n' 2>/dev/null \
| sort -rn \
| awk '{printf "%-10s %s\n", int($1/1024/1024)"MB", $2}' \
| head -30
Project 8 — User Account Reporter
#!/bin/bash
echo "=== System User Report ==="
echo ""
echo "--- Login Users ---"
awk -F: '$3 >= 1000 && $3 < 65534 {print $1, "| Home:", $6, "| Shell:", $7}' /etc/passwd
echo ""
echo "--- Currently Logged In ---"
who
echo ""
echo "--- Last 10 Logins ---"
last | head -10
Intermediate Projects (9–18)
Project 9 — CPU Temperature Monitor (Raspberry Pi)
#!/bin/bash
LOG="${1:-/var/log/cpu_temp.log}"
INTERVAL="${2:-5}"
WARN_TEMP=70
CRIT_TEMP=80
log() { echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*" | tee -a "$LOG"; }
while true; do
RAW=$(cat /sys/class/thermal/thermal_zone0/temp 2>/dev/null)
if [ -z "$RAW" ]; then
log "ERROR: Cannot read CPU temperature."
sleep "$INTERVAL"; continue
fi
TEMP=$((RAW / 1000))
FRAC=$(( (RAW % 1000) / 100 ))
if [ "$TEMP" -ge "$CRIT_TEMP" ]; then STATUS="CRITICAL"
elif [ "$TEMP" -ge "$WARN_TEMP" ]; then STATUS="WARNING"
else STATUS="OK"
fi
log "CPU Temp: ${TEMP}.${FRAC}°C [$STATUS]"
sleep "$INTERVAL"
done
Usage: ./cpu_temp_monitor.sh /var/log/cpu_temp.log 10
Project 10 — Disk Space Alert
Send an alert when disk usage exceeds a threshold.
#!/bin/bash
THRESHOLD="${1:-80}"
LOG="/var/log/disk_alert.log"
check_disk() {
df -h | awk 'NR>1' | while read -r FS SIZE USED AVAIL USE MP; do
PCT="${USE%%%}"
if [ "$PCT" -ge "$THRESHOLD" ]; then
MSG="[ALERT] $MP is ${PCT}% full (${USED}/${SIZE})"
echo "[$(date '+%Y-%m-%d %H:%M:%S')] $MSG" | tee -a "$LOG"
# Uncomment to send email:
# echo "$MSG" | mail -s "Disk Alert on $(hostname)" admin@example.com
fi
done
}
while true; do
check_disk
sleep 300
done
Project 11 — System Health Report
Generate a full HTML system report.
#!/bin/bash
REPORT="/tmp/system_report_$(date +%Y%m%d_%H%M%S).html"
cat << EOF > "$REPORT"
<!DOCTYPE html><html><head>
<title>System Report — $(hostname)</title>
<style>body{font-family:monospace;background:#111;color:#0f0;padding:20px}
h2{color:#0af}pre{background:#222;padding:10px}</style>
</head><body>
<h1>System Report — $(hostname)</h1>
<p>Generated: $(date)</p>
<h2>CPU</h2><pre>$(lscpu | grep -E 'Model|Core|Thread|MHz')</pre>
<h2>Memory</h2><pre>$(free -h)</pre>
<h2>Disk</h2><pre>$(df -h)</pre>
<h2>Network</h2><pre>$(ip addr show | grep -E 'inet |link')</pre>
<h2>Top Processes</h2><pre>$(ps aux --sort=-%cpu | head -10)</pre>
<h2>Recent Logs</h2><pre>$(journalctl -n 20 --no-pager 2>/dev/null)</pre>
</body></html>
EOF
echo "Report saved: $REPORT"
command -v xdg-open &>/dev/null && xdg-open "$REPORT"
Project 12 — Auto Update Script
Keep a Raspberry Pi automatically updated and log changes.
#!/bin/bash
set -e
LOG="/var/log/auto_update.log"
exec >> "$LOG" 2>&1
echo "=== Update started: $(date) ==="
apt-get update -qq
UPGRADABLE=$(apt-get -s upgrade | grep "^Inst" | wc -l)
echo "Packages to upgrade: $UPGRADABLE"
if [ "$UPGRADABLE" -gt 0 ]; then
apt-get upgrade -y
echo "Upgrade complete."
if [ -f /var/run/reboot-required ]; then
echo "Reboot required — scheduling..."
shutdown -r +1 "Auto-update reboot"
fi
else
echo "System is up to date."
fi
apt-get autoremove -y
apt-get autoclean
echo "=== Update finished: $(date) ==="
Install as cron: 0 3 * * 0 sudo /home/pi/scripts/auto_update.sh
Project 13 — Log Rotator
Rotate log files older than N days.
#!/bin/bash
LOG_DIR="${1:?Usage: $0 <log_dir> [max_days]}"
MAX_DAYS="${2:-7}"
ARCHIVE_DIR="$LOG_DIR/archive"
mkdir -p "$ARCHIVE_DIR"
find "$LOG_DIR" -maxdepth 1 -name "*.log" -mtime "+$MAX_DAYS" | while read -r FILE; do
BASENAME=$(basename "$FILE")
DEST="$ARCHIVE_DIR/${BASENAME%.log}_$(date +%Y%m%d).log.gz"
gzip -c "$FILE" > "$DEST"
rm "$FILE"
echo "Rotated: $BASENAME → $(basename "$DEST")"
done
find "$ARCHIVE_DIR" -name "*.gz" -mtime +30 -delete
echo "Log rotation complete for $LOG_DIR"
Project 14 — Network Connectivity Monitor
Monitor ping to multiple hosts and log outages.
#!/bin/bash
HOSTS=("8.8.8.8" "1.1.1.1" "192.168.1.1")
LOG="/var/log/network_monitor.log"
INTERVAL=30
log() { echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*" | tee -a "$LOG"; }
declare -A LAST_STATE
for H in "${HOSTS[@]}"; do LAST_STATE[$H]="up"; done
while true; do
for HOST in "${HOSTS[@]}"; do
if ping -c 2 -W 2 "$HOST" &>/dev/null; then
if [ "${LAST_STATE[$HOST]}" = "down" ]; then
log "RECOVERED: $HOST is reachable again."
LAST_STATE[$HOST]="up"
fi
else
if [ "${LAST_STATE[$HOST]}" = "up" ]; then
log "ALERT: $HOST is unreachable!"
LAST_STATE[$HOST]="down"
fi
fi
done
sleep "$INTERVAL"
done
Project 15 — Batch File Renamer
Rename files in bulk with a pattern.
#!/bin/bash
# Usage: ./rename.sh <dir> <find_pattern> <replace_pattern>
DIR="${1:?Usage: $0 <dir> <find> <replace>}"
FIND="${2:?}"
REPLACE="${3:?}"
COUNT=0
find "$DIR" -maxdepth 1 -type f | while read -r FILE; do
BASE=$(basename "$FILE")
NEW="${BASE//$FIND/$REPLACE}"
if [ "$BASE" != "$NEW" ]; then
mv "$FILE" "$(dirname "$FILE")/$NEW"
echo "Renamed: $BASE → $NEW"
((COUNT++)) || true
fi
done
echo "Done."
Usage: ./rename.sh ./captures IMG Photo
Project 16 — Interactive Menu System
A reusable menu framework for SBC management scripts.
#!/bin/bash
TITLE="Raspberry Pi Manager"
show_menu() {
clear
echo "╔════════════════════════════╗"
printf "║ %-26s ║\n" "$TITLE"
echo "╠════════════════════════════╣"
echo "║ 1) System Info ║"
echo "║ 2) CPU Temperature ║"
echo "║ 3) Disk Usage ║"
echo "║ 4) Network Info ║"
echo "║ 5) List Services ║"
echo "║ 6) Reboot ║"
echo "║ q) Quit ║"
echo "╚════════════════════════════╝"
echo -n " Choice: "
}
while true; do
show_menu
read -r CHOICE
case "$CHOICE" in
1) uname -a; free -h; df -h ;;
2) cat /sys/class/thermal/thermal_zone0/temp | awk '{printf "%.1f°C\n", $1/1000}' ;;
3) df -h ;;
4) ip addr; ip route ;;
5) systemctl list-units --type=service --state=running ;;
6) read -p "Reboot now? [y/N] " C; [[ "$C" =~ ^[Yy] ]] && sudo reboot ;;
q|Q) echo "Bye!"; exit 0 ;;
*) echo "Invalid choice." ;;
esac
echo; read -p "Press Enter to continue..."
done
Project 17 — GPIO LED Blinker (Raspberry Pi)
Blink an LED using shell — no Python needed.
#!/bin/bash
# Requires: GPIO pin wired to LED
# On Raspberry Pi: GPIO 17 = physical pin 11
GPIO_PIN="${1:-17}"
BLINK_COUNT="${2:-10}"
DELAY="${3:-0.5}"
GPIO_PATH="/sys/class/gpio"
cleanup() {
echo "$GPIO_PIN" > "$GPIO_PATH/unexport" 2>/dev/null
echo "GPIO $GPIO_PIN released."
}
trap cleanup EXIT
# Export pin
echo "$GPIO_PIN" > "$GPIO_PATH/export"
sleep 0.1
echo "out" > "$GPIO_PATH/gpio${GPIO_PIN}/direction"
echo "Blinking GPIO $GPIO_PIN — $BLINK_COUNT times..."
for ((i=0; i<BLINK_COUNT; i++)); do
echo 1 > "$GPIO_PATH/gpio${GPIO_PIN}/value"
sleep "$DELAY"
echo 0 > "$GPIO_PATH/gpio${GPIO_PIN}/value"
sleep "$DELAY"
echo "Blink $((i+1))/$BLINK_COUNT"
done
Project 18 — Serial Port Logger
Log incoming serial data from ESP32 or sensor module.
#!/bin/bash
PORT="${1:-/dev/ttyUSB0}"
BAUD="${2:-115200}"
LOG="serial_$(date +%Y%m%d_%H%M%S).log"
if [ ! -c "$PORT" ]; then
echo "ERROR: $PORT not found."
echo "Available ports: $(ls /dev/ttyUSB* /dev/ttyACM* 2>/dev/null)"
exit 1
fi
stty -F "$PORT" "$BAUD" cs8 -cstopb -parenb raw
echo "Logging $PORT at $BAUD baud → $LOG"
echo "Press Ctrl+C to stop."
while IFS= read -r LINE; do
ENTRY="[$(date '+%Y-%m-%d %H:%M:%S')] $LINE"
echo "$ENTRY"
echo "$ENTRY" >> "$LOG"
done < "$PORT"
Advanced Projects (19–26)
Project 19 — Wi-Fi Reconnect Watchdog
Auto-reconnect Wi-Fi on drop. Useful for headless Raspberry Pi.
#!/bin/bash
INTERFACE="${1:-wlan0}"
CHECK_HOST="8.8.8.8"
LOG="/var/log/wifi_watchdog.log"
log() { echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*" | tee -a "$LOG"; }
while true; do
if ! ping -c 2 -W 3 "$CHECK_HOST" &>/dev/null; then
log "No internet. Restarting $INTERFACE..."
ip link set "$INTERFACE" down
sleep 2
ip link set "$INTERFACE" up
sleep 10
if ping -c 2 "$CHECK_HOST" &>/dev/null; then
log "Reconnected on $INTERFACE."
else
log "Reconnect failed. Restarting networking service..."
systemctl restart networking
fi
fi
sleep 60
done
Project 20 — Service Watchdog with Auto-Restart
#!/bin/bash
SERVICE="${1:?Usage: $0 <service_name>}"
MAX_RESTARTS=5
WINDOW=300
LOG="/var/log/${SERVICE}_watchdog.log"
log() { echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*" | tee -a "$LOG"; }
RESTARTS=0
WINDOW_START=$(date +%s)
while true; do
if ! systemctl is-active --quiet "$SERVICE"; then
NOW=$(date +%s)
if (( NOW - WINDOW_START > WINDOW )); then
RESTARTS=0
WINDOW_START=$NOW
fi
if (( RESTARTS >= MAX_RESTARTS )); then
log "CRITICAL: $SERVICE restarted $MAX_RESTARTS times in ${WINDOW}s. Giving up."
exit 1
fi
log "ALERT: $SERVICE is down. Restarting... (attempt $((RESTARTS+1)))"
systemctl start "$SERVICE"
((RESTARTS++))
sleep $((RESTARTS * 10))
fi
sleep 30
done
Project 21 — Sensor CSV Logger (I2C — Raspberry Pi)
Log BME280 temperature/humidity/pressure to CSV using i2cget.
#!/bin/bash
LOG="sensor_$(date +%Y%m%d).csv"
INTERVAL=10
if [ ! -f "$LOG" ]; then
echo "timestamp,temp_c,humidity_pct,pressure_hpa" > "$LOG"
fi
read_bme280() {
# Requires python3-smbus: sudo apt install python3-smbus
python3 - << 'PY'
import smbus2, bme280
bus = smbus2.SMBus(1)
calibration = bme280.load_calibration_params(bus, 0x76)
data = bme280.sample(bus, 0x76, calibration)
print(f"{data.temperature:.2f},{data.humidity:.2f},{data.pressure:.2f}")
PY
}
echo "Logging to $LOG every ${INTERVAL}s. Ctrl+C to stop."
while true; do
TS=$(date '+%Y-%m-%d %H:%M:%S')
READING=$(read_bme280 2>/dev/null)
if [ -n "$READING" ]; then
echo "$TS,$READING" | tee -a "$LOG"
else
echo "$TS,ERROR,ERROR,ERROR" >> "$LOG"
echo "[$TS] Sensor read failed."
fi
sleep "$INTERVAL"
done
Project 22 — SSH Batch Deployer
Deploy a script to multiple SBCs in parallel.
#!/bin/bash
SCRIPT="${1:?Usage: $0 <script.sh> <hosts_file>}"
HOSTS_FILE="${2:?}"
SSH_USER="${3:-pi}"
SSH_KEY="${4:-$HOME/.ssh/id_rsa}"
LOG="deploy_$(date +%Y%m%d_%H%M%S).log"
deploy_to() {
local HOST=$1
echo "[$(date '+%H:%M:%S')] Deploying to $HOST..." | tee -a "$LOG"
scp -i "$SSH_KEY" -o StrictHostKeyChecking=no "$SCRIPT" "$SSH_USER@$HOST:/tmp/deploy_script.sh" \
&& ssh -i "$SSH_KEY" -o StrictHostKeyChecking=no "$SSH_USER@$HOST" "bash /tmp/deploy_script.sh" \
&& echo "[OK] $HOST" >> "$LOG" \
|| echo "[FAIL] $HOST" >> "$LOG"
}
while IFS= read -r HOST; do
[[ -z "$HOST" || "$HOST" == \#* ]] && continue
deploy_to "$HOST" &
done < "$HOSTS_FILE"
wait
echo "Deployment complete. Results in $LOG"
grep -E "OK|FAIL" "$LOG"
Project 23 — Git-Based OTA Updater
Pull latest code and restart service on Raspberry Pi.
#!/bin/bash
set -e
REPO_DIR="${1:?Usage: $0 <repo_dir> <service_name>}"
SERVICE="${2:?}"
LOG="/var/log/ota_update.log"
log() { echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*" | tee -a "$LOG"; }
cd "$REPO_DIR"
git fetch origin
LOCAL=$(git rev-parse HEAD)
REMOTE=$(git rev-parse origin/main)
if [ "$LOCAL" = "$REMOTE" ]; then
log "Already up to date ($LOCAL)."
exit 0
fi
log "Update available: $LOCAL → $REMOTE"
git pull origin main
if [ -f "requirements.txt" ]; then
log "Installing Python dependencies..."
pip install -r requirements.txt -q
fi
log "Restarting $SERVICE..."
systemctl restart "$SERVICE"
systemctl is-active --quiet "$SERVICE" \
&& log "Update complete. $SERVICE is running." \
|| { log "ERROR: $SERVICE failed to start after update."; exit 1; }
Project 24 — Process Memory Monitor
Alert when a process exceeds a memory threshold.
#!/bin/bash
PROCESS="${1:?Usage: $0 <process_name> [limit_mb]}"
LIMIT_MB="${2:-500}"
INTERVAL=10
LOG="/var/log/mem_monitor.log"
log() { echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*" | tee -a "$LOG"; }
while true; do
PID=$(pgrep -x "$PROCESS" | head -1)
if [ -z "$PID" ]; then
log "WARN: $PROCESS is not running."
sleep "$INTERVAL"; continue
fi
MEM_KB=$(awk '/VmRSS/{print $2}' /proc/"$PID"/status 2>/dev/null)
MEM_MB=$((MEM_KB / 1024))
if [ "$MEM_MB" -ge "$LIMIT_MB" ]; then
log "ALERT: $PROCESS (PID $PID) using ${MEM_MB}MB — limit ${LIMIT_MB}MB"
else
log "OK: $PROCESS uses ${MEM_MB}MB"
fi
sleep "$INTERVAL"
done
Project 25 — Bandwidth Monitor
Track real-time network throughput on a Raspberry Pi.
#!/bin/bash
IFACE="${1:-eth0}"
INTERVAL="${2:-2}"
rx_bytes() { cat /sys/class/net/"$IFACE"/statistics/rx_bytes; }
tx_bytes() { cat /sys/class/net/"$IFACE"/statistics/tx_bytes; }
OLD_RX=$(rx_bytes); OLD_TX=$(tx_bytes)
echo "Monitoring $IFACE every ${INTERVAL}s (Ctrl+C to stop)"
printf "%-24s %12s %12s\n" "Timestamp" "Download" "Upload"
while true; do
sleep "$INTERVAL"
NEW_RX=$(rx_bytes); NEW_TX=$(tx_bytes)
RX_RATE=$(( (NEW_RX - OLD_RX) * 8 / INTERVAL / 1024 ))
TX_RATE=$(( (NEW_TX - OLD_TX) * 8 / INTERVAL / 1024 ))
printf "%-24s %9s kbps %9s kbps\n" \
"$(date '+%Y-%m-%d %H:%M:%S')" "$RX_RATE" "$TX_RATE"
OLD_RX=$NEW_RX; OLD_TX=$NEW_TX
done
Project 26 — SD Card Health Checker
Check SD card health on Raspberry Pi before it fails.
#!/bin/bash
DEVICE="${1:-/dev/mmcblk0}"
LOG="/var/log/sdcard_health.log"
log() { echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*" | tee -a "$LOG"; }
log "=== SD Card Health Check ==="
# Check filesystem errors
log "Running fsck (read-only)..."
fsck -n "$DEVICE" 2>&1 | tail -5 | while read -r L; do log "$L"; done
# Check for I/O errors in dmesg
IO_ERRORS=$(dmesg | grep -c "I/O error" 2>/dev/null)
log "I/O errors in dmesg: $IO_ERRORS"
# Disk usage
log "Disk usage:"
df -h / | tail -1 | awk '{log=$5; print " Used: "log}' || df -h /
# Bad blocks (WARNING: takes a long time on large cards)
# badblocks -v "$DEVICE"
log "Check complete."
Expert Projects (27–35)
Project 27 — First-Boot Provisioner (Raspberry Pi)
Run once on first boot to fully configure a fresh Pi.
#!/bin/bash
set -euo pipefail
MARKER="/etc/.provisioned"
LOG="/var/log/provision.log"
exec > >(tee -a "$LOG") 2>&1
[ -f "$MARKER" ] && { echo "Already provisioned. Skipping."; exit 0; }
echo "=== First Boot Provisioning: $(date) ==="
# Set hostname
NEW_HOST="${HOSTNAME_PREFIX:-pi}-$(date +%s | tail -c 4)"
hostnamectl set-hostname "$NEW_HOST"
sed -i "s/127.0.1.1.*/127.0.1.1\t$NEW_HOST/" /etc/hosts
echo "Hostname set: $NEW_HOST"
# Update system
apt-get update -qq && apt-get upgrade -y
# Install base tools
apt-get install -y git python3 python3-pip python3-venv \
i2c-tools build-essential vim htop curl
# Enable I2C and SPI
raspi-config nonint do_i2c 0
raspi-config nonint do_spi 0
# Create a project directory
mkdir -p /home/pi/projects
chown pi:pi /home/pi/projects
touch "$MARKER"
echo "=== Provisioning complete: $(date) ==="
reboot
Project 28 — Parallel SSH Command Runner
Run a command on a fleet of SBCs simultaneously.
#!/bin/bash
CMD="${1:?Usage: $0 \"<command>\" <hosts_file>}"
HOSTS_FILE="${2:?}"
SSH_USER="${3:-pi}"
SSH_KEY="${4:-$HOME/.ssh/id_rsa}"
TIMEOUT=15
run_on() {
local HOST=$1
RESULT=$(ssh -i "$SSH_KEY" -o ConnectTimeout="$TIMEOUT" \
-o StrictHostKeyChecking=no \
"$SSH_USER@$HOST" "$CMD" 2>&1)
printf "[%-20s] %s\n" "$HOST" "$RESULT"
}
export -f run_on
export SSH_KEY SSH_USER TIMEOUT CMD
grep -v '^\s*#' "$HOSTS_FILE" | grep -v '^\s*$' | \
xargs -P 10 -I{} bash -c 'run_on "$@"' _ {}
echo "Done."
Usage: ./fleet_cmd.sh "uname -r" hosts.txt
Project 29 — Automated ESP32 Flasher
Build and flash ESP32 firmware automatically, detecting the port.
#!/bin/bash
set -euo pipefail
TARGET="${1:-esp32}"
BAUD="${2:-460800}"
log() { echo -e "\033[0;32m[INFO]\033[0m $*"; }
err() { echo -e "\033[0;31m[ERROR]\033[0m $*" >&2; exit 1; }
detect_port() {
for P in /dev/ttyUSB* /dev/ttyACM*; do
[ -c "$P" ] && { echo "$P"; return; }
done
err "No serial device found. Is ESP32 connected?"
}
PORT=$(detect_port)
log "Detected device: $PORT"
log "Target chip: $TARGET"
log "Setting target..."
idf.py set-target "$TARGET"
log "Building..."
idf.py build
log "Flashing to $PORT at $BAUD baud..."
idf.py -p "$PORT" -b "$BAUD" flash
log "Flash complete. Starting monitor..."
idf.py -p "$PORT" monitor
Project 30 — Log Anomaly Detector
Parse logs and flag lines matching suspicious patterns.
#!/bin/bash
LOG_FILE="${1:-/var/log/syslog}"
RULES_FILE="${2:-/etc/anomaly_rules.txt}"
ALERT_LOG="/var/log/anomaly_alerts.log"
# Default rules if file doesn't exist
if [ ! -f "$RULES_FILE" ]; then
cat << 'EOF' > "$RULES_FILE"
segfault
kernel panic
Out of memory
authentication failure
Permission denied
connection refused
disk full
read-only file system
EOF
fi
log() { echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*" | tee -a "$ALERT_LOG"; }
PATTERN=$(paste -sd'|' "$RULES_FILE")
log "=== Scanning $LOG_FILE ==="
grep -iE "$PATTERN" "$LOG_FILE" | tail -100 | while IFS= read -r LINE; do
log "MATCH: $LINE"
done
TOTAL=$(grep -ciE "$PATTERN" "$LOG_FILE" 2>/dev/null || echo 0)
log "Total anomalous lines found: $TOTAL"
Project 31 — Camera Snapshot + Upload (Raspberry Pi)
Capture a frame and upload to a remote server.
#!/bin/bash
DEST_HOST="${1:?Usage: $0 <user@host:/path>}"
INTERVAL="${2:-60}"
SNAP_DIR="/tmp/snapshots"
mkdir -p "$SNAP_DIR"
log() { echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*"; }
capture() {
FILE="$SNAP_DIR/snap_$(date +%Y%m%d_%H%M%S).jpg"
# Use libcamera-still on Pi 5 / newer OS:
if command -v libcamera-still &>/dev/null; then
libcamera-still -o "$FILE" --nopreview -t 200 2>/dev/null
else
# Legacy raspistill:
raspistill -o "$FILE" -t 200 -n 2>/dev/null
fi
echo "$FILE"
}
while true; do
FILE=$(capture)
if [ -f "$FILE" ]; then
log "Captured: $(basename "$FILE")"
scp -o StrictHostKeyChecking=no "$FILE" "$DEST_HOST" \
&& { log "Uploaded OK."; rm "$FILE"; } \
|| log "Upload failed. File kept locally."
else
log "Capture failed."
fi
sleep "$INTERVAL"
done
Project 32 — dotfiles Installer
Set up a developer environment on any fresh Linux/SBC machine.
#!/bin/bash
set -e
DOTFILES_REPO="${1:-https://github.com/youruser/dotfiles.git}"
DOTFILES_DIR="$HOME/.dotfiles"
log() { echo -e "\033[0;34m[SETUP]\033[0m $*"; }
# Clone dotfiles
if [ ! -d "$DOTFILES_DIR" ]; then
log "Cloning dotfiles..."
git clone "$DOTFILES_REPO" "$DOTFILES_DIR"
fi
# Link config files
for FILE in bashrc vimrc tmux.conf gitconfig; do
SRC="$DOTFILES_DIR/$FILE"
DEST="$HOME/.$FILE"
[ -f "$SRC" ] || continue
[ -f "$DEST" ] && mv "$DEST" "${DEST}.bak"
ln -sf "$SRC" "$DEST"
log "Linked: $DEST → $SRC"
done
# Install packages
log "Installing dev packages..."
sudo apt-get install -y vim tmux git curl htop tree jq build-essential python3-pip
# Install Oh My Bash (optional)
if [ ! -d "$HOME/.oh-my-bash" ]; then
bash -c "$(curl -fsSL https://raw.githubusercontent.com/ohmybash/oh-my-bash/master/tools/install.sh)" \
--unattended || true
fi
log "Setup complete. Reload your shell: source ~/.bashrc"
Project 33 — GPIO Button Event Handler
Detect a GPIO button press and run an action (Raspberry Pi).
#!/bin/bash
# Wire: Button between GPIO 27 (pin 13) and GND
# Requires: internal pull-up is enabled via direction
GPIO_PIN="${1:-27}"
GPIO_PATH="/sys/class/gpio"
ACTION="${2:-echo BUTTON PRESSED}"
cleanup() { echo "$GPIO_PIN" > "$GPIO_PATH/unexport" 2>/dev/null; }
trap cleanup EXIT
echo "$GPIO_PIN" > "$GPIO_PATH/export" 2>/dev/null || true
sleep 0.1
echo "in" > "$GPIO_PATH/gpio${GPIO_PIN}/direction"
# Enable active-low (pull up)
echo 1 > "$GPIO_PATH/gpio${GPIO_PIN}/active_low" 2>/dev/null || true
echo "Waiting for button press on GPIO $GPIO_PIN..."
LAST=0
while true; do
VAL=$(cat "$GPIO_PATH/gpio${GPIO_PIN}/value")
if [ "$VAL" -eq 1 ] && [ "$LAST" -eq 0 ]; then
echo "[$(date '+%H:%M:%S')] Button pressed!"
eval "$ACTION"
fi
LAST=$VAL
sleep 0.05
done
Project 34 — Secure Encrypted Backup
Create an AES-encrypted backup and upload offsite.
#!/bin/bash
SOURCE="${1:?Usage: $0 <dir> <passphrase> [dest_host]}"
PASS="${2:?}"
DEST_HOST="${3:-}"
TIMESTAMP=$(date +%Y%m%d_%H%M%S)
ARCHIVE="/tmp/backup_${TIMESTAMP}.tar.gz.enc"
log() { echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*"; }
log "Creating encrypted backup of $SOURCE..."
tar -czf - "$SOURCE" | \
openssl enc -aes-256-cbc -salt -pbkdf2 -pass "pass:$PASS" \
-out "$ARCHIVE"
SIZE=$(du -sh "$ARCHIVE" | cut -f1)
log "Encrypted archive: $ARCHIVE ($SIZE)"
if [ -n "$DEST_HOST" ]; then
log "Uploading to $DEST_HOST..."
scp "$ARCHIVE" "$DEST_HOST"
rm "$ARCHIVE"
log "Upload complete. Local copy removed."
fi
# Verify decryption works
log "Verifying decryption..."
openssl enc -d -aes-256-cbc -salt -pbkdf2 -pass "pass:$PASS" \
-in "$ARCHIVE" 2>/dev/null | tar -tzf - > /dev/null \
&& log "Verification passed." \
|| log "WARNING: Verification failed!"
Project 35 — Full SBC Health Dashboard
Print a live terminal dashboard for any Raspberry Pi or SBC.
#!/bin/bash
IFACE="${1:-eth0}"
INTERVAL="${2:-2}"
# Save/restore terminal
tput smcup; tput civis
trap 'tput rmcup; tput cnorm; echo ""; exit 0' INT TERM
OLD_RX=$(cat /sys/class/net/"$IFACE"/statistics/rx_bytes 2>/dev/null || echo 0)
OLD_TX=$(cat /sys/class/net/"$IFACE"/statistics/tx_bytes 2>/dev/null || echo 0)
while true; do
tput cup 0 0
W=$(tput cols)
SEP=$(printf '%*s' "$W" '' | tr ' ' '─')
TEMP_RAW=$(cat /sys/class/thermal/thermal_zone0/temp 2>/dev/null || echo 0)
TEMP=$(awk "BEGIN{printf \"%.1f\", $TEMP_RAW/1000}")
CPU=$(top -bn1 | awk '/Cpu/ {printf "%.1f", 100-$8}')
MEM=$(free | awk '/Mem/{printf "%.1f", $3/$2*100}')
DISK=$(df / | awk 'NR==2{print $5}')
UPTIME=$(uptime -p | sed 's/up //')
IP=$(hostname -I | awk '{print $1}')
NEW_RX=$(cat /sys/class/net/"$IFACE"/statistics/rx_bytes 2>/dev/null || echo 0)
NEW_TX=$(cat /sys/class/net/"$IFACE"/statistics/tx_bytes 2>/dev/null || echo 0)
RX_K=$(( (NEW_RX - OLD_RX) * 8 / INTERVAL / 1024 ))
TX_K=$(( (NEW_TX - OLD_TX) * 8 / INTERVAL / 1024 ))
OLD_RX=$NEW_RX; OLD_TX=$NEW_TX
echo -e "\033[1;36m$(hostname) — SBC Health Dashboard\033[0m $(date '+%Y-%m-%d %H:%M:%S')"
echo "$SEP"
printf " %-14s \033[1;33m%s°C\033[0m\n" "CPU Temp:" "$TEMP"
printf " %-14s \033[1;32m%s%%\033[0m\n" "CPU Usage:" "$CPU"
printf " %-14s \033[1;32m%s%%\033[0m\n" "Memory:" "$MEM"
printf " %-14s \033[1;32m%s\033[0m\n" "Disk (/):" "$DISK"
printf " %-14s \033[1;37m%s\033[0m\n" "Uptime:" "$UPTIME"
printf " %-14s \033[1;37m%s\033[0m\n" "IP:" "$IP"
echo "$SEP"
printf " %-14s ↓ %s kbps ↑ %s kbps\n" "$IFACE:" "$RX_K" "$TX_K"
echo "$SEP"
echo " Top Processes:"
ps aux --sort=-%cpu --no-headers | head -5 | \
awk '{printf " %-20s CPU:%-6s MEM:%s%%\n", $11, $3, $4}'
echo "$SEP"
sleep "$INTERVAL"
done
Project Index
| # | Project | Level | Key Skills |
|---|---|---|---|
| 1 | Hello System | Beginner | echo, hostname, date |
| 2 | Directory Organiser | Beginner | for, mv, extension parsing |
| 3 | Timestamped Backup | Beginner | tar, date, mkdir |
| 4 | Calculator | Beginner | case, bc, read |
| 5 | Command Checker | Beginner | arrays, command -v |
| 6 | Countdown Timer | Beginner | while, printf, sleep |
| 7 | Large File Finder | Beginner | find, sort, awk |
| 8 | User Reporter | Beginner | awk, who, last |
| 9 | CPU Temp Monitor | Intermediate | /sys, while, logging |
| 10 | Disk Space Alert | Intermediate | df, awk, threshold |
| 11 | HTML System Report | Intermediate | heredoc, HTML, lscpu |
| 12 | Auto Updater | Intermediate | apt, cron, reboot |
| 13 | Log Rotator | Intermediate | find, gzip, mtime |
| 14 | Network Monitor | Intermediate | ping, assoc arrays |
| 15 | Batch Renamer | Intermediate | find, string replace |
| 16 | Menu System | Intermediate | case, tput, UI |
| 17 | GPIO LED Blinker | Intermediate | /sys/class/gpio, trap |
| 18 | Serial Logger | Intermediate | stty, read, /dev |
| 19 | Wi-Fi Watchdog | Advanced | ip, systemctl, ping |
| 20 | Service Watchdog | Advanced | systemctl, rate limiting |
| 21 | Sensor CSV Logger | Advanced | I2C, python3, CSV |
| 22 | SSH Batch Deployer | Advanced | scp, ssh, xargs -P |
| 23 | Git OTA Updater | Advanced | git, systemctl, pip |
| 24 | Memory Monitor | Advanced | /proc, pgrep |
| 25 | Bandwidth Monitor | Advanced | /sys/net, arithmetic |
| 26 | SD Card Checker | Advanced | fsck, dmesg, badblocks |
| 27 | First-Boot Provisioner | Expert | raspi-config, systemd |
| 28 | Fleet SSH Runner | Expert | xargs -P, ssh, export |
| 29 | ESP32 Flasher | Expert | idf.py, detect port |
| 30 | Log Anomaly Detector | Expert | grep -E, rules file |
| 31 | Camera + Upload | Expert | libcamera-still, scp |
| 32 | dotfiles Installer | Expert | symlinks, git, Oh-My-Bash |
| 33 | GPIO Button Handler | Expert | /sys/class/gpio, event loop |
| 34 | Encrypted Backup | Expert | openssl, aes-256, scp |
| 35 | SBC Dashboard | Expert | tput, /sys, live UI |

