Shell Scripting
Shell scripts are glue. They're not elegant programs — they're the thing you write at 11pm to automate the thing you just did by hand three times in a row. Once you get comfortable with the basics, a lot of tedious sysadmin work just disappears. This covers the parts you actually use.
The Shebang and Basic Structure
Always start with a shebang so the system knows which interpreter to use:
#!/bin/bash # This is a comment echo "Hello from a shell script"
Make it executable before running:
$ chmod +x myscript.sh $ ./myscript.sh
Variables
NAME="jason"
COUNT=42
echo "Hello, $NAME"
echo "Count is ${COUNT}" # braces help when adjacent to other text
echo "Value: ${COUNT}px"
No spaces around the =. This is the most common beginner mistake — NAME = "jason" tries to run a command called NAME.
Control Flow
If / Else
if [ -f "/etc/hostname" ]; then
echo "hostname file exists"
elif [ -d "/etc/hostname" ]; then
echo "that's a directory, weird"
else
echo "not found"
fi
Common test flags: -f (regular file), -d (directory), -e (exists), -z (string is empty), -n (string is not empty). For numbers use -eq, -gt, -lt.
For Loops
# Loop over a list
for COLOR in red green blue; do
echo "Color: $COLOR"
done
# Loop over files
for FILE in /var/log/*.log; do
echo "Processing $FILE"
done
# C-style loop
for ((i=0; i<10; i++)); do
echo "i = $i"
done
While Loop
COUNT=0
while [ $COUNT -lt 5 ]; do
echo "Count: $COUNT"
COUNT=$((COUNT + 1))
done
Functions
log() {
echo "$(date '+%Y-%m-%d %H:%M:%S') - $1"
}
backup() {
local SOURCE=$1
local DEST=$2
log "Backing up $SOURCE to $DEST"
tar -czf "$DEST/backup_$(date +%Y%m%d).tar.gz" "$SOURCE"
}
backup /home/jason /mnt/backups
Use local for variables inside functions — otherwise they're global and you'll create hard-to-find bugs.
Error Handling
By default, bash keeps going even when commands fail. That's almost never what you want in a script.
#!/bin/bash set -e # exit on error set -u # treat unset variables as errors set -o pipefail # catch errors in pipes too # Now this will stop the script instead of silently continuing: cp nonexistent_file /tmp/ echo "This won't run if cp failed"
The trap command lets you run cleanup code on exit:
TMPFILE=$(mktemp)
cleanup() {
rm -f "$TMPFILE"
echo "Cleaned up"
}
trap cleanup EXIT # runs on any exit, including errors
Working with Command Output
# Capture output into a variable
HOSTNAME=$(hostname)
DISK_USAGE=$(df -h / | awk 'NR==2 {print $5}')
echo "Running on $HOSTNAME"
echo "Root disk usage: $DISK_USAGE"
# Check exit status of last command
if grep -q "error" /var/log/syslog; then
echo "Errors found in syslog"
fi
A Real Script: Rotating Log Files
Something actually useful — rotate a log file by date and keep only the last 7:
#!/bin/bash
set -euo pipefail
LOG_DIR="/var/log/myapp"
LOG_FILE="$LOG_DIR/app.log"
MAX_KEEP=7
if [ ! -f "$LOG_FILE" ]; then
echo "No log file at $LOG_FILE"
exit 0
fi
# Rotate
TIMESTAMP=$(date +%Y%m%d_%H%M%S)
mv "$LOG_FILE" "$LOG_DIR/app_$TIMESTAMP.log"
touch "$LOG_FILE"
# Clean up old rotated logs, keep only MAX_KEEP newest
ls -t "$LOG_DIR"/app_*.log 2>/dev/null | tail -n +"$((MAX_KEEP + 1))" | xargs rm -f
echo "Rotated log. Kept last $MAX_KEEP archives."
Useful One-Liners to Know
# Run a command for every line in a file while IFS= read -r line; do echo "$line"; done < file.txt # Process all files in a directory safely (handles spaces in filenames) find /path -name "*.txt" -print0 | xargs -0 wc -l # Check if a script is already running [ -f /tmp/myscript.pid ] && kill -0 $(cat /tmp/myscript.pid) 2>/dev/null && exit 1 echo $$ > /tmp/myscript.pid
dispelled