--- title: "Bash Scripting Patterns for Sysadmins" domain: linux category: shell-scripting tags: [bash, scripting, automation, linux, shell] status: published created: 2026-03-08 updated: 2026-03-08 --- # 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 ```bash #!/usr/bin/env bash set -euo pipefail ``` 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 ```bash #!/usr/bin/env bash set -euo pipefail # ── Config ───────────────────────────────────────────────────────────────────── SCRIPT_NAME="$(basename "$0")" LOG_FILE="/var/log/myscript.log" # ─────────────────────────────────────────────────────────────────────────────── ``` ## Logging ```bash log() { echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*" | tee -a "$LOG_FILE" } log "Script started" log "Processing $1" ``` ## Error Handling ```bash # 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 ```bash 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 ```bash usage() { cat < 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 ```bash # 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 ```bash # 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 ```bash # 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:** ```bash 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:** ```bash 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 -e` exits on the first error — including in pipes.** Add `set -o pipefail` or you'll miss failures in `cmd1 | cmd2`. - **`$?` after `if` is almost always wrong.** Use `if command; then` not `command; 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](ansible-getting-started.md) - [managing-linux-services-systemd-ansible](../process-management/managing-linux-services-systemd-ansible.md)