Bash Scripting Patterns for Sysadmins¶
These are the patterns I reach for when writing bash scripts for server automation and maintenance tasks. Not a tutorial from scratch — this assumes you know basic bash syntax and want to write scripts that don't embarrass you later.
The Short Answer¶
Start every script with these two lines. set -e exits on error. set -u treats unset variables as errors. set -o pipefail catches errors in pipes. Together they prevent a lot of silent failures.
Script Header¶
#!/usr/bin/env bash
set -euo pipefail
# ── Config ─────────────────────────────────────────────────────────────────────
SCRIPT_NAME="$(basename "$0")"
LOG_FILE="/var/log/myscript.log"
# ───────────────────────────────────────────────────────────────────────────────
Logging¶
log() {
echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*" | tee -a "$LOG_FILE"
}
log "Script started"
log "Processing $1"
Error Handling¶
# Exit with a message
die() {
echo "ERROR: $*" >&2
exit 1
}
# Check a condition
[ -f "$CONFIG_FILE" ] || die "Config file not found: $CONFIG_FILE"
# Trap to run cleanup on exit
cleanup() {
log "Cleaning up temp files"
rm -f "$TMPFILE"
}
trap cleanup EXIT
Checking Dependencies¶
check_deps() {
local deps=("curl" "jq" "rsync")
for dep in "${deps[@]}"; do
command -v "$dep" &>/dev/null || die "Required dependency not found: $dep"
done
}
check_deps
Argument Parsing¶
usage() {
cat <<EOF
Usage: $SCRIPT_NAME [OPTIONS] <target>
Options:
-v, --verbose Enable verbose output
-n, --dry-run Show what would be done without doing it
-h, --help Show this help
EOF
exit 0
}
VERBOSE=false
DRY_RUN=false
while [[ $# -gt 0 ]]; do
case $1 in
-v|--verbose) VERBOSE=true; shift ;;
-n|--dry-run) DRY_RUN=true; shift ;;
-h|--help) usage ;;
--) shift; break ;;
-*) die "Unknown option: $1" ;;
*) TARGET="$1"; shift ;;
esac
done
[[ -z "${TARGET:-}" ]] && die "Target is required"
Running Commands¶
# Dry-run aware command execution
run() {
if $DRY_RUN; then
echo "DRY RUN: $*"
else
"$@"
fi
}
run rsync -av /source/ /dest/
run systemctl restart myservice
Working with Files and Directories¶
# Check existence before use
[[ -d "$DIR" ]] || mkdir -p "$DIR"
[[ -f "$FILE" ]] || die "Expected file not found: $FILE"
# Safe temp files
TMPFILE="$(mktemp)"
trap 'rm -f "$TMPFILE"' EXIT
# Loop over files
find /path/to/files -name "*.log" -mtime +30 | while read -r file; do
log "Processing: $file"
run gzip "$file"
done
String Operations¶
# Extract filename without extension
filename="${filepath##*/}" # basename
stem="${filename%.*}" # strip extension
# Check if string contains substring
if [[ "$output" == *"error"* ]]; then
die "Error detected in output"
fi
# Convert to lowercase
lower="${str,,}"
# Trim whitespace
trimmed="${str#"${str%%[![:space:]]*}"}"
Common Patterns¶
Backup with timestamp:
backup() {
local source="$1"
local dest="${2:-/backup}"
local timestamp
timestamp="$(date '+%Y%m%d_%H%M%S')"
local backup_path="${dest}/$(basename "$source")_${timestamp}.tar.gz"
log "Backing up $source to $backup_path"
run tar -czf "$backup_path" -C "$(dirname "$source")" "$(basename "$source")"
}
Retry on failure:
retry() {
local max_attempts="${1:-3}"
local delay="${2:-5}"
shift 2
local attempt=1
until "$@"; do
if ((attempt >= max_attempts)); then
die "Command failed after $max_attempts attempts: $*"
fi
log "Attempt $attempt failed, retrying in ${delay}s..."
sleep "$delay"
((attempt++))
done
}
retry 3 10 curl -f https://example.com/health
Gotchas & Notes¶
- Always quote variables.
"$var"not$var. Unquoted variables break on spaces and glob characters. - Use
[[not[for conditionals.[[is a bash built-in with fewer edge cases. set -eexits on the first error — including in pipes. Addset -o pipefailor you'll miss failures incmd1 | cmd2.set -ealso aborts on a false&&list.[ "$n" -eq 0 ] && exit 0exits the script with status 1 when$nis non-zero — the list evaluates false and errexit fires. In an alert script that is the run that had something to report, so it fails exactly when it matters. Use an explicitifblock, or append|| true. See Alert Script Never Emails.$?afterifis almost always wrong. Useif command; thennotcommand; if [[ $? -eq 0 ]]; then.- Bash isn't great for complex data. If your script needs real data structures or error handling beyond strings, consider Python.