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.$?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.
See Also¶
- [[ansible-getting-started]]
- [[managing-linux-services-systemd-ansible]]