Practical Scripts
Practical Scripts#
Concepts#
This lesson brings together everything from the shell scripting module. No new concepts — just real-world application. Each script below is a practical tool you might actually use. Study them, run them, and modify them.
Lab#
Script 1: Backup Script#
A script that creates timestamped compressed backups of a directory.
cd ~/lab/scripts
cat > backup.sh << 'SCRIPT'
#!/bin/bash
set -euo pipefail
# --- Configuration ---
BACKUP_DIR="${HOME}/backups"
# --- Functions ---
die() { echo "ERROR: $1" >&2; exit 1; }
log() { echo "[$(date '+%Y-%m-%d %H:%M:%S')] $1"; }
usage() {
echo "Usage: $0 <source_directory>"
echo "Creates a timestamped backup in $BACKUP_DIR"
exit 1
}
# --- Validate ---
[[ $# -eq 1 ]] || usage
SOURCE="$1"
[[ -d "$SOURCE" ]] || die "Source directory does not exist: $SOURCE"
# --- Prepare ---
mkdir -p "$BACKUP_DIR"
BASENAME=$(basename "$SOURCE")
TIMESTAMP=$(date +%Y%m%d_%H%M%S)
BACKUP_FILE="${BACKUP_DIR}/${BASENAME}_${TIMESTAMP}.tar.gz"
# --- Execute ---
log "Starting backup of '$SOURCE'"
log "Destination: $BACKUP_FILE"
tar -czf "$BACKUP_FILE" -C "$(dirname "$SOURCE")" "$BASENAME"
SIZE=$(du -h "$BACKUP_FILE" | cut -f1)
log "Backup complete: $BACKUP_FILE ($SIZE)"
# --- Show recent backups ---
echo ""
echo "Recent backups:"
ls -lht "$BACKUP_DIR"/${BASENAME}_*.tar.gz 2>/dev/null | head -5
SCRIPT
chmod +x backup.sh
# Test it
mkdir -p ~/lab/testdata
echo "important file" > ~/lab/testdata/data.txt
echo "config" > ~/lab/testdata/settings.conf
./backup.sh ~/lab/testdata
ls -lh ~/backups/
Script 2: Log Analyzer#
A script that parses a log file and reports statistics.
cd ~/lab/scripts
# Create a sample log file
cat > sample.log << 'EOF'
2024-10-15 08:00:01 INFO Server started on port 8080
2024-10-15 08:00:05 INFO Database connected
2024-10-15 08:05:12 WARN Slow query: SELECT * FROM users (3.2s)
2024-10-15 08:10:33 ERROR Connection timeout to payment API
2024-10-15 08:10:34 ERROR Retry 1/3: payment API
2024-10-15 08:10:37 INFO Payment API reconnected
2024-10-15 08:15:00 INFO Processing batch job #42
2024-10-15 08:20:45 WARN High memory usage: 87%
2024-10-15 08:25:00 ERROR Disk space low: /var/log at 92%
2024-10-15 08:30:00 INFO Batch job #42 completed
2024-10-15 08:35:12 INFO Health check: OK
2024-10-15 08:40:00 WARN SSL certificate expires in 7 days
2024-10-15 08:45:33 ERROR Out of memory: worker process killed
2024-10-15 08:45:35 INFO Worker process restarted
2024-10-15 08:50:00 INFO Serving 1,247 active connections
EOF
cat > loganalyzer.sh << 'SCRIPT'
#!/bin/bash
set -euo pipefail
die() { echo "ERROR: $1" >&2; exit 1; }
[[ $# -eq 1 ]] || die "Usage: $0 <logfile>"
[[ -f "$1" ]] || die "File not found: $1"
LOGFILE="$1"
TOTAL=$(wc -l < "$LOGFILE")
echo "===== Log Analysis: $(basename "$LOGFILE") ====="
echo "Total entries: $TOTAL"
echo ""
# Count by level
echo "--- Counts by Level ---"
for level in INFO WARN ERROR; do
count=$(grep -c "$level" "$LOGFILE" || true)
pct=0
(( TOTAL > 0 )) && pct=$((count * 100 / TOTAL))
printf " %-8s %3d (%d%%)\n" "$level" "$count" "$pct"
done
echo ""
echo "--- Error Messages ---"
grep "ERROR" "$LOGFILE" | while IFS= read -r line; do
echo " ${line}"
done
echo ""
echo "--- Timeline ---"
first=$(head -1 "$LOGFILE" | cut -d' ' -f1,2)
last=$(tail -1 "$LOGFILE" | cut -d' ' -f1,2)
echo " First entry: $first"
echo " Last entry: $last"
SCRIPT
chmod +x loganalyzer.sh
./loganalyzer.sh sample.log
Script 3: Batch File Renamer#
A script that renames files in bulk based on a pattern.
cd ~/lab/scripts
cat > renamer.sh << 'SCRIPT'
#!/bin/bash
set -euo pipefail
die() { echo "ERROR: $1" >&2; exit 1; }
usage() {
cat << EOF
Usage: $0 [options] <directory>
Options:
-p PREFIX Add prefix to filenames
-s OLD NEW Replace OLD with NEW in filenames
-l Convert filenames to lowercase
-d Dry run (show what would happen, don't rename)
-h Show this help
Examples:
$0 -p "2024_" ./photos
$0 -s ".jpeg" ".jpg" ./photos
$0 -l ./documents
EOF
exit 0
}
# --- Parse arguments ---
DRY_RUN=false
MODE=""
PREFIX=""
OLD_STR=""
NEW_STR=""
while getopts "p:s:ldh" opt; do
case "$opt" in
p) MODE="prefix"; PREFIX="$OPTARG" ;;
s) MODE="substitute"; OLD_STR="$OPTARG"; NEW_STR="${!OPTIND}"; OPTIND=$((OPTIND + 1)) ;;
l) MODE="lowercase" ;;
d) DRY_RUN=true ;;
h) usage ;;
*) usage ;;
esac
done
shift $((OPTIND - 1))
[[ $# -eq 1 ]] || die "Missing directory argument. Use -h for help."
DIR="$1"
[[ -d "$DIR" ]] || die "Not a directory: $DIR"
[[ -n "$MODE" ]] || die "No operation specified. Use -h for help."
# --- Execute ---
$DRY_RUN && echo "=== DRY RUN ==="
count=0
for filepath in "$DIR"/*; do
[[ -f "$filepath" ]] || continue
filename=$(basename "$filepath")
dirname=$(dirname "$filepath")
case "$MODE" in
prefix) newname="${PREFIX}${filename}" ;;
substitute) newname="${filename//$OLD_STR/$NEW_STR}" ;;
lowercase) newname="${filename,,}" ;;
esac
if [[ "$filename" != "$newname" ]]; then
if $DRY_RUN; then
echo " Would rename: $filename → $newname"
else
mv "$filepath" "$dirname/$newname"
echo " Renamed: $filename → $newname"
fi
((count++))
fi
done
echo "Total: $count file(s) ${DRY_RUN:+would be }renamed."
SCRIPT
chmod +x renamer.sh
# Create test files
mkdir -p ~/lab/renametest
touch ~/lab/renametest/{"Photo 1.JPG","Photo 2.JPG","Document.TXT","notes.Md"}
# Dry run: lowercase
./renamer.sh -d -l ~/lab/renametest
# Actually do it
./renamer.sh -l ~/lab/renametest
ls ~/lab/renametest
# Clean up
rm -rf ~/lab/renametest
Script 4: System Health Report#
A comprehensive system information script.
cd ~/lab/scripts
cat > healthcheck.sh << 'SCRIPT'
#!/bin/bash
set -euo pipefail
WARN_CPU=80
WARN_MEM=80
WARN_DISK=80
bar() {
local pct=$1 width=30
local filled=$((pct * width / 100))
local empty=$((width - filled))
printf "[%s%s] %3d%%" \
"$(printf '#%.0s' $(seq 1 $filled 2>/dev/null))" \
"$(printf '-%.0s' $(seq 1 $empty 2>/dev/null))" \
"$pct"
}
echo "=============================="
echo " System Health Report"
echo " $(date '+%Y-%m-%d %H:%M:%S')"
echo "=============================="
echo ""
echo "--- System ---"
echo "Hostname: $(hostname)"
echo "OS: $(grep PRETTY_NAME /etc/os-release | cut -d= -f2 | tr -d '"')"
echo "Kernel: $(uname -r)"
echo "Uptime: $(uptime -p)"
echo ""
echo "--- CPU ---"
load=$(awk '{print $1}' /proc/loadavg)
cores=$(nproc)
load_pct=$(echo "$load $cores" | awk '{printf "%d", ($1/$2)*100}')
echo "Load average: $load (${cores} cores)"
echo "Usage: $(bar "$load_pct")"
(( load_pct >= WARN_CPU )) && echo " ⚠ CPU load is high!"
echo ""
echo "--- Memory ---"
mem_total=$(free -m | awk '/Mem:/ {print $2}')
mem_used=$(free -m | awk '/Mem:/ {print $3}')
mem_pct=$((mem_used * 100 / mem_total))
echo "Used: ${mem_used}M / ${mem_total}M"
echo "Usage: $(bar "$mem_pct")"
(( mem_pct >= WARN_MEM )) && echo " ⚠ Memory usage is high!"
echo ""
echo "--- Disk ---"
while read -r fs size used avail pct mount; do
pct_num=${pct%\%}
echo "$mount ($fs): $used used of $size"
echo "Usage: $(bar "$pct_num")"
(( pct_num >= WARN_DISK )) && echo " ⚠ Disk space is low!"
done < <(df -h --type=ext4 --type=xfs --type=btrfs --type=tmpfs 2>/dev/null | tail -n +2 | head -5)
echo ""
echo "--- Services ---"
for svc in ssh cron; do
if systemctl is-active --quiet "$svc" 2>/dev/null; then
echo " $svc: running"
else
echo " $svc: STOPPED"
fi
done
echo ""
echo "--- Recent Errors (last hour) ---"
errors=$(journalctl -p err --since "1 hour ago" --no-pager -q 2>/dev/null | wc -l)
echo " $errors error(s) in the last hour"
echo ""
echo "=============================="
SCRIPT
chmod +x healthcheck.sh
./healthcheck.sh
Clean Up#
# Keep the scripts directory for reference, or clean up:
# rm -rf ~/lab/scripts ~/lab/testdata ~/backups
Review#
1. What makes a script "production-ready"?
The safety header (set -euo pipefail), input validation, meaningful error messages to stderr, cleanup via trap, clear usage instructions, and exit codes that reflect success or failure.
2. How do you parse command-line options in a script?
Use getopts for simple option parsing. It handles -p value, -l, -d, etc. Use shift $((OPTIND - 1)) after parsing to access remaining positional arguments.
3. What is a "dry run" and why is it useful?
A dry run shows what the script would do without actually doing it. It lets users verify the script’s behavior before making changes. Especially important for destructive operations like file renaming or deletion.
4. How do you ensure temporary files are cleaned up even if the script fails?
Use trap 'rm -f "$tmpfile"' EXIT. The EXIT trap runs when the script exits for any reason — success, failure, or signal.
5. What is the purpose of a `die()` function?
A die() function provides a consistent way to report errors and exit. It prints the error message to stderr and exits with a non-zero code. Example: die() { echo "ERROR: $1" >&2; exit "${2:-1}"; }.
Previous: Functions and Error Handling | Next: Networking Concepts