BASH SCRIPTING FUER SYSADMINS Terminal $ #!/bin/bash $ set -euo pipefail $ echo "automatisieren" Variablen NAME="value" $1, $2, $@ Arrays, Strings Logik if / elif / else for / while Funktionen, Traps Skripte Festplatten-Monitor Log-Bereinigung Backup, Audit Cron 0 2 * * * Geplant Automatisiert Vom ersten Skript zur vollstaendig automatisierten Serveradministration

Wenn Sie Linux-Server verwalten, ist Bash-Scripting keine Option — es ist eine Kernkompetenz. Jede sich wiederholende Aufgabe, die Sie manuell ausfuehren, ist ein Kandidat fuer die Automatisierung: Festplattenplatz pruefen, Logs rotieren, Backups erstellen, Benutzerkonten ueberpruefen, Dienste ueberwachen. Ein gut geschriebenes Bash-Skript fuehrt in Sekunden aus, was Sie Minuten (oder Stunden) an Klicken und Tippen kosten wuerde. Dieser Leitfaden bringt Ihnen Bash-Scripting von Grund auf bei, mit Fokus auf praktische Sysadmin-Aufgaben, die Sie sofort einsetzen koennen.

Am Ende dieses Artikels werden Sie wissen, wie man Skripte mit Variablen, Bedingungen, Schleifen und Funktionen schreibt. Sie werden Fehlerbehandlungsmuster verstehen, die verhindern, dass Skripte Schaden anrichten, wenn etwas schiefgeht. Und Sie werden vier vollstaendige, produktionsreife Skripte haben, die reale Sysadmin-Probleme loesen.


Warum Bash-Scripting fuer Sysadmins?

Bash ist die Standard-Shell auf praktisch jeder Linux-Distribution. Es ist vorinstalliert auf Ubuntu, Debian, CentOS, RHEL, Fedora, Arch und sogar macOS (obwohl macOS auf zsh als Standard-Interaktiv-Shell umgestiegen ist, bleibt Bash verfuegbar). Das bedeutet, Ihre Skripte laufen auf jedem Server ohne zusaetzliche Software zu installieren.

Das bietet Ihnen Bash-Scripting:

  • Automatisierung: Verwandeln Sie jede Abfolge von Terminal-Befehlen in ein wiederholbares Skript
  • Konsistenz: Skripte werden jedes Mal gleich ausgefuehrt und eliminieren menschliche Fehler
  • Geschwindigkeit: Ein Skript kann Tausende von Dateien oder Benutzern in Sekunden verarbeiten
  • Planung: Kombiniert mit cron laufen Skripte automatisch in jedem Intervall
  • Portabilitaet: Bash-Skripte laufen auf jedem Linux-System ohne Abhaengigkeiten
  • Nachvollziehbarkeit: Skripte dienen als Dokumentation dessen, was genau wann getan wurde

Wann Bash vs. Python verwenden: Bash ist ideal fuer Aufgaben, die bestehende Kommandozeilen-Tools verketten (Dateioperationen, Textverarbeitung, Dienstverwaltung). Wenn Ihre Aufgabe komplexe Datenstrukturen, API-Aufrufe oder die Verarbeitung von JSON/YAML erfordert, ziehen Sie stattdessen Python in Betracht.

Voraussetzungen

Bevor Sie beginnen, stellen Sie sicher, dass Sie Folgendes haben:

  • Ein Linux-System (Ubuntu 22.04 oder 24.04 empfohlen, aber jede Distribution funktioniert)
  • Terminal-Zugang mit einem regulaeren Benutzerkonto mit sudo-Berechtigungen
  • Einen Texteditor (nano fuer Anfaenger, vim oder VS Code fuer fortgeschrittene Benutzer)
  • Grundlegende Vertrautheit mit der Linux-Kommandozeile (Verzeichnisse navigieren, Dateien auflisten, Dateiinhalte lesen)

Ueberpruefen Sie Ihre Bash-Version:

bash --version

Sie sollten Version 4.0 oder neuer sehen. Ubuntu 22.04 wird mit Bash 5.1 ausgeliefert und Ubuntu 24.04 mit Bash 5.2.

Ihr erstes Skript

Jedes Bash-Skript beginnt mit einer Shebang-Zeile, die dem System mitteilt, welchen Interpreter es verwenden soll. Erstellen Sie eine Datei namens hallo.sh:

#!/bin/bash
# Mein erstes Bash-Skript
echo "Hallo, $(whoami)! Heute ist $(date +%A), der $(date +%d.\ %B\ %Y)."
echo "Sie verwenden Bash-Version: $BASH_VERSION"
echo "Ihr Home-Verzeichnis ist: $HOME"

Machen Sie es ausfuehrbar und fuehren Sie es aus:

chmod +x hallo.sh
./hallo.sh

Ausgabe:

Hallo, jc! Heute ist Montag, der 27. Januar 2026.
Sie verwenden Bash-Version: 5.2.21(1)-release
Ihr Home-Verzeichnis ist: /home/jc

Zentrale Konzepte:

  • #!/bin/bash — der Shebang teilt Linux mit, Bash zur Interpretation dieser Datei zu verwenden
  • chmod +x — setzt die Ausfuehrungsberechtigung, damit Sie das Skript direkt ausfuehren koennen
  • $(Befehl) — Befehlssubstitution: fuehrt den Befehl aus und fuegt seine Ausgabe ein
  • $VARIABLE — Variablenexpansion: fuegt den Wert der Variable ein

Best Practice: Verwenden Sie immer #!/bin/bash (nicht #!/bin/sh), wenn Ihr Skript Bash-spezifische Funktionen wie Arrays, [[ ]]-Tests oder {1..10}-Klammerexpansion nutzt. Wenn Sie maximale Portabilitaet zwischen verschiedenen Shells benoetigen, schreiben Sie POSIX-konforme sh-Skripte.

Variablen und Datentypen

Bash-Variablen speichern standardmaessig Zeichenketten. Es gibt keine separaten Integer-, Float- oder Boolean-Typen — alles ist eine Zeichenkette, die in arithmetischen Kontexten als Zahl interpretiert werden kann.

Variablen definieren

#!/bin/bash

# String-Variablen (keine Leerzeichen um das =)
HOSTNAME="webserver01"
BACKUP_DIR="/var/backups"
LOG_FILE="/var/log/myscript.log"

# Ganzzahl-Arithmetik mit $(( ))
MAX_RETRIES=5
CURRENT_RETRY=0
TOTAL=$((MAX_RETRIES - CURRENT_RETRY))

# Befehlssubstitution
CURRENT_DATE=$(date +%Y-%m-%d)
DISK_USAGE=$(df -h / | awk 'NR==2 {print $5}')
UPTIME=$(uptime -p)

echo "Server: $HOSTNAME"
echo "Backup-Verzeichnis: $BACKUP_DIR"
echo "Datum: $CURRENT_DATE"
echo "Festplattennutzung: $DISK_USAGE"
echo "Betriebszeit: $UPTIME"
echo "Verbleibende Versuche: $TOTAL"

Spezielle Variablen

#!/bin/bash

echo "Skriptname: $0"
echo "Erstes Argument: $1"
echo "Zweites Argument: $2"
echo "Alle Argumente: $@"
echo "Anzahl der Argumente: $#"
echo "Exit-Status des letzten Befehls: $?"
echo "Prozess-ID dieses Skripts: $$"
echo "Prozess-ID des letzten Hintergrundbefehls: $!"

Arrays

#!/bin/bash

# Indiziertes Array
SERVERS=("web01" "web02" "db01" "db02" "cache01")

echo "Erster Server: ${SERVERS[0]}"
echo "Alle Server: ${SERVERS[@]}"
echo "Anzahl der Server: ${#SERVERS[@]}"

# Array durchlaufen
for server in "${SERVERS[@]}"; do
    echo "Pruefe $server..."
done

# Assoziatives Array (Bash 4+)
declare -A SERVICE_PORTS
SERVICE_PORTS[http]=80
SERVICE_PORTS[https]=443
SERVICE_PORTS[ssh]=22
SERVICE_PORTS[mysql]=3306

for service in "${!SERVICE_PORTS[@]}"; do
    echo "$service -> Port ${SERVICE_PORTS[$service]}"
done

Haeufiger Fehler: Setzen Sie niemals Leerzeichen um das =-Zeichen bei der Zuweisung von Variablen. NAME="value" ist korrekt. NAME = "value" wird fehlschlagen, weil Bash NAME als Befehl interpretiert.

Bedingungen: if, elif, else

Bedingungen steuern den Ablauf Ihres Skripts basierend auf Testbedingungen.

Grundsyntax

#!/bin/bash

FILE="/etc/ssh/sshd_config"

if [[ -f "$FILE" ]]; then
    echo "$FILE existiert."
elif [[ -d "$FILE" ]]; then
    echo "$FILE ist ein Verzeichnis, keine Datei."
else
    echo "$FILE existiert nicht."
fi

Dateitest-Operatoren

#!/bin/bash

TARGET="/var/log/syslog"

# Dateiexistenz- und Typtests
[[ -e "$TARGET" ]] && echo "Existiert"
[[ -f "$TARGET" ]] && echo "Ist eine regulaere Datei"
[[ -d "$TARGET" ]] && echo "Ist ein Verzeichnis"
[[ -L "$TARGET" ]] && echo "Ist ein symbolischer Link"
[[ -r "$TARGET" ]] && echo "Ist lesbar"
[[ -w "$TARGET" ]] && echo "Ist beschreibbar"
[[ -x "$TARGET" ]] && echo "Ist ausfuehrbar"
[[ -s "$TARGET" ]] && echo "Hat eine Groesse groesser als Null"

Zeichenkettenvergleiche

#!/bin/bash

ENVIRONMENT="production"

if [[ "$ENVIRONMENT" == "production" ]]; then
    echo "Ausfuehrung im Produktionsmodus -- besondere Vorsicht!"
    VERBOSE=false
elif [[ "$ENVIRONMENT" == "staging" ]]; then
    echo "Ausfuehrung im Staging-Modus."
    VERBOSE=true
else
    echo "Unbekannte Umgebung: $ENVIRONMENT"
    exit 1
fi

# Zeichenketten-Pruefungen
[[ -z "$VAR" ]] && echo "VAR ist leer oder nicht gesetzt"
[[ -n "$VAR" ]] && echo "VAR ist nicht leer"

# Mustererkennung mit [[ ]]
if [[ "$HOSTNAME" == web* ]]; then
    echo "Dies ist ein Webserver."
fi

Numerische Vergleiche

#!/bin/bash

DISK_PERCENT=$(df / | awk 'NR==2 {print $5}' | tr -d '%')

if [[ "$DISK_PERCENT" -gt 90 ]]; then
    echo "KRITISCH: Festplattennutzung bei ${DISK_PERCENT}%"
elif [[ "$DISK_PERCENT" -gt 75 ]]; then
    echo "WARNUNG: Festplattennutzung bei ${DISK_PERCENT}%"
else
    echo "OK: Festplattennutzung bei ${DISK_PERCENT}%"
fi

Tipp: Verwenden Sie [[ ]] (doppelte Klammern) anstelle von [ ] (einfache Klammern). Doppelte Klammern sind eine Bash-Erweiterung, die Leerzeichen in Variablen besser handhabt, Mustererkennung mit == unterstuetzt und &&/|| innerhalb des Testausdrucks erlaubt.

Schleifen: for, while, until

Schleifen ermoelichen es Ihnen, Operationen ueber Dateien, Server, Benutzer oder beliebige Listen von Elementen zu wiederholen.

For-Schleife

#!/bin/bash

# Eine Liste durchlaufen
for PACKAGE in nginx curl wget htop; do
    echo "Installiere $PACKAGE..."
    sudo apt install -y "$PACKAGE" > /dev/null 2>&1
    echo "$PACKAGE installiert."
done

# Einen Bereich durchlaufen
for i in {1..10}; do
    echo "Iteration $i"
done

# C-Stil For-Schleife
for ((i = 0; i < 5; i++)); do
    echo "Zaehler: $i"
done

# Dateien durchlaufen
for FILE in /var/log/*.log; do
    SIZE=$(stat -c%s "$FILE" 2>/dev/null || echo 0)
    echo "$FILE: $SIZE Bytes"
done

While-Schleife

#!/bin/bash

# Eine Datei Zeile fuer Zeile lesen
while IFS= read -r line; do
    echo "Verarbeite: $line"
done < /etc/hosts

# Zaehlerbasierte Schleife
COUNTER=0
MAX=5
while [[ $COUNTER -lt $MAX ]]; do
    echo "Versuch $((COUNTER + 1)) von $MAX"
    COUNTER=$((COUNTER + 1))
done

# Auf Verfuegbarkeit eines Dienstes warten
RETRIES=0
MAX_RETRIES=30
while ! curl -s http://localhost:8080/health > /dev/null 2>&1; do
    RETRIES=$((RETRIES + 1))
    if [[ $RETRIES -ge $MAX_RETRIES ]]; then
        echo "Dienst wurde innerhalb von $MAX_RETRIES Versuchen nicht gestartet."
        exit 1
    fi
    echo "Warte auf Dienst... (Versuch $RETRIES/$MAX_RETRIES)"
    sleep 2
done
echo "Dienst ist bereit!"

Until-Schleife

#!/bin/bash

# Until-Schleife -- laeuft bis die Bedingung wahr wird
COUNT=0
until [[ $COUNT -ge 5 ]]; do
    echo "Zaehler ist $COUNT"
    COUNT=$((COUNT + 1))
done

Funktionen

Funktionen ermoeglichen es Ihnen, Ihre Skripte in wiederverwendbare Bloecke zu organisieren. Das ist entscheidend fuer groessere Skripte, die mehrere zusammenhaengende Aufgaben ausfuehren.

#!/bin/bash

# Funktionsdefinition
log_message() {
    local LEVEL="$1"
    local MESSAGE="$2"
    local TIMESTAMP
    TIMESTAMP=$(date '+%Y-%m-%d %H:%M:%S')
    echo "[$TIMESTAMP] [$LEVEL] $MESSAGE"
}

# Funktion mit Rueckgabewert
check_service() {
    local SERVICE_NAME="$1"
    if systemctl is-active --quiet "$SERVICE_NAME"; then
        return 0  # Erfolg
    else
        return 1  # Fehlschlag
    fi
}

# Funktion die einen Wert ausgibt (mit Befehlssubstitution erfassen)
get_memory_usage() {
    free -m | awk 'NR==2 {printf "%.1f", $3/$2 * 100}'
}

# Verwendung der Funktionen
log_message "INFO" "Starte Systempruefung..."

SERVICES=("nginx" "ssh" "cron")
for svc in "${SERVICES[@]}"; do
    if check_service "$svc"; then
        log_message "INFO" "$svc laeuft."
    else
        log_message "WARN" "$svc laeuft NICHT!"
    fi
done

MEM_USAGE=$(get_memory_usage)
log_message "INFO" "Speicherauslastung: ${MEM_USAGE}%"

Wichtige Punkte zu Bash-Funktionen:

  • Verwenden Sie local, um funktionsbezogene Variablen zu deklarieren (verhindert die Verschmutzung des globalen Gueltigkeitsbereichs)
  • Funktionen verwenden return fuer Exit-Codes (0-255), nicht fuer die Rueckgabe von Daten
  • Um Daten zurueckzugeben, verwenden Sie echo innerhalb der Funktion und erfassen Sie mit $(Funktionsname)
  • Argumente werden mit $1, $2, $@ innerhalb der Funktion angesprochen

Ein-/Ausgabe und Umleitung

Das Verstaendnis der E/A-Umleitung ist wesentlich fuer das Schreiben von Skripten, die ihre Ausgabe protokollieren, Dateien verarbeiten und Fehler korrekt behandeln.

#!/bin/bash

# stdout in eine Datei umleiten (ueberschreiben)
echo "Log-Eintrag" > /tmp/output.log

# stdout in eine Datei umleiten (anhaengen)
echo "Weiterer Eintrag" >> /tmp/output.log

# stderr in eine Datei umleiten
command_that_fails 2> /tmp/error.log

# Sowohl stdout als auch stderr in dieselbe Datei umleiten
some_command > /tmp/all_output.log 2>&1

# Moderne Syntax (Bash 4+) fuer die Umleitung beider
some_command &> /tmp/all_output.log

# Ausgabe vollstaendig verwerfen
noisy_command > /dev/null 2>&1

# Here-Dokument (Heredoc) fuer mehrzeilige Eingabe
cat << 'EOF' > /tmp/config.txt
server {
    listen 80;
    server_name example.com;
    root /var/www/html;
}
EOF

# Ausgabe an einen anderen Befehl weiterleiten
ps aux | grep nginx | grep -v grep

# Tee: gleichzeitig in Datei UND stdout schreiben
echo "Systempruefung bestanden" | tee -a /var/log/checks.log

# Prozesssubstitution
diff <(ls /dir1) <(ls /dir2)

Benutzereingabe lesen

#!/bin/bash

read -p "Geben Sie den Servernamen ein: " SERVER_NAME
read -sp "Geben Sie das Passwort ein: " PASSWORD
echo ""  # Neue Zeile nach verdeckter Eingabe

read -p "Mit Deployment auf $SERVER_NAME fortfahren? (j/n): " CONFIRM
if [[ "$CONFIRM" != "j" ]]; then
    echo "Deployment abgebrochen."
    exit 0
fi

Fehlerbehandlung: set -euo pipefail und trap

Richtige Fehlerbehandlung trennt produktionsreife Skripte von fragilen. Ohne Fehlerbehandlung kann ein Skript stillschweigend auf halbem Weg fehlschlagen und Ihr System in einem inkonsistenten Zustand hinterlassen.

Der Strict-Mode-Header

#!/bin/bash
set -euo pipefail
IFS=$'\n\t'

Was jede Option bewirkt:

  • set -e — Beendet sofort, wenn ein Befehl einen Exit-Status ungleich Null zurueckgibt
  • set -u — Behandelt Referenzen auf nicht gesetzte Variablen als Fehler
  • set -o pipefail — Wenn ein Befehl in einer Pipeline fehlschlaegt, schlaegt die gesamte Pipeline fehl (nicht nur der letzte Befehl)
  • IFS=$'\n\t' — Setzt den internen Feldtrenner auf Zeilenumbruch und Tabulator (verhindert Wortaufteilung bei Leerzeichen in Dateinamen)

Trap fuer Aufraeumarbeiten verwenden

#!/bin/bash
set -euo pipefail

TEMP_DIR=""

cleanup() {
    local EXIT_CODE=$?
    if [[ -n "$TEMP_DIR" && -d "$TEMP_DIR" ]]; then
        rm -rf "$TEMP_DIR"
        echo "Temporaeres Verzeichnis aufgeraeumt: $TEMP_DIR"
    fi
    if [[ $EXIT_CODE -ne 0 ]]; then
        echo "Skript fehlgeschlagen mit Exit-Code: $EXIT_CODE"
    fi
    exit $EXIT_CODE
}

trap cleanup EXIT ERR

TEMP_DIR=$(mktemp -d)
echo "Arbeite in $TEMP_DIR"

# Ihre Skript-Logik hier...
# Wenn etwas fehlschlaegt, wird cleanup() automatisch ausgefuehrt
cp /some/important/file "$TEMP_DIR/"
process_data "$TEMP_DIR"

Erwartete Fehler behandeln

#!/bin/bash
set -euo pipefail

# Methode 1: || true verwenden, um einen Befehl fehlschlagen zu lassen
grep "pattern" /var/log/syslog || true

# Methode 2: if verwenden, um das Ergebnis zu pruefen
if grep -q "error" /var/log/syslog; then
    echo "Fehler in syslog gefunden"
else
    echo "Keine Fehler gefunden"
fi

# Methode 3: Exit-Code erfassen und pruefen
set +e  # Exit-bei-Fehler voruebergehend deaktivieren
risky_command
EXIT_CODE=$?
set -e  # Wieder aktivieren

if [[ $EXIT_CODE -ne 0 ]]; then
    echo "Befehl fehlgeschlagen mit Code $EXIT_CODE"
fi

Arbeiten mit Dateien und Verzeichnissen

Sysadmin-Skripte muessen haeufig Dateien erstellen, pruefen, verschieben und verarbeiten. Hier sind die gaengigsten Muster:

#!/bin/bash
set -euo pipefail

# Eine Verzeichnisstruktur erstellen
BACKUP_BASE="/var/backups/myapp"
BACKUP_DIR="${BACKUP_BASE}/$(date +%Y-%m-%d)"
mkdir -p "$BACKUP_DIR"

# Pruefen, ob eine Datei existiert, bevor darauf operiert wird
CONFIG_FILE="/etc/myapp/config.yml"
if [[ ! -f "$CONFIG_FILE" ]]; then
    echo "FEHLER: Konfigurationsdatei nicht gefunden: $CONFIG_FILE"
    exit 1
fi

# Dateien finden, die aelter als 30 Tage sind
find /var/log/myapp -name "*.log" -mtime +30 -type f -print

# Alte Dateien finden und loeschen (mit Bestaetigung)
find /tmp -name "*.tmp" -mtime +7 -type f -delete

# Dateigroesse in Bytes ermitteln
FILE_SIZE=$(stat -c%s "$CONFIG_FILE")
echo "Groesse der Konfigurationsdatei: $FILE_SIZE Bytes"

# Zeilen in einer Datei zaehlen
LINE_COUNT=$(wc -l < "$CONFIG_FILE")
echo "Konfiguration hat $LINE_COUNT Zeilen"

# Konfigurationsdatei lesen, Kommentare und leere Zeilen ueberspringen
while IFS='=' read -r key value; do
    # Kommentare und leere Zeilen ueberspringen
    [[ "$key" =~ ^#.*$ || -z "$key" ]] && continue
    echo "Config: $key = $value"
done < "$CONFIG_FILE"

# Sichere Erstellung temporaerer Dateien
TEMP_FILE=$(mktemp /tmp/myapp.XXXXXX)
echo "Verwende temporaere Datei: $TEMP_FILE"
# Temporaere Dateien immer aufraeumen (verwenden Sie trap wie zuvor gezeigt)

Praktische Sysadmin-Skripte

Hier sind vier vollstaendige, produktionsreife Skripte, die gaengige Sysadmin-Probleme loesen.

Skript 1: Festplattenplatz-Monitor mit E-Mail-Warnung

#!/bin/bash
set -euo pipefail

# Konfiguration
THRESHOLD=80
ALERT_EMAIL="admin@example.com"
LOG_FILE="/var/log/disk-monitor.log"

log() {
    echo "[$(date '+%Y-%m-%d %H:%M:%S')] $1" | tee -a "$LOG_FILE"
}

log "Starte Festplattenplatz-Pruefung..."

ALERT_TRIGGERED=false

while IFS= read -r line; do
    # Header-Zeile ueberspringen
    [[ "$line" == Filesystem* ]] && continue

    USAGE=$(echo "$line" | awk '{print $5}' | tr -d '%')
    MOUNT=$(echo "$line" | awk '{print $6}')
    FILESYSTEM=$(echo "$line" | awk '{print $1}')

    if [[ "$USAGE" -ge "$THRESHOLD" ]]; then
        ALERT_TRIGGERED=true
        log "WARNUNG: $MOUNT bei ${USAGE}% ($FILESYSTEM)"
    else
        log "OK: $MOUNT bei ${USAGE}%"
    fi
done < <(df -h --output=source,size,used,avail,pcent,target -x tmpfs -x devtmpfs)

if [[ "$ALERT_TRIGGERED" == true ]]; then
    SUBJECT="[ALARM] Festplattenplatz-Warnung auf $(hostname)"
    BODY="Ein oder mehrere Dateisysteme haben ${THRESHOLD}% Nutzung auf $(hostname) am $(date) ueberschritten.\n\n$(df -h)"
    echo -e "$BODY" | mail -s "$SUBJECT" "$ALERT_EMAIL" 2>/dev/null || \
        log "WARNUNG: E-Mail-Warnung konnte nicht gesendet werden"
fi

log "Festplattenplatz-Pruefung abgeschlossen."

Skript 2: Log-Bereinigung und Rotation

#!/bin/bash
set -euo pipefail

# Konfiguration
LOG_DIRS=("/var/log/myapp" "/var/log/nginx" "/home/*/logs")
MAX_AGE_DAYS=30
COMPRESS_AGE_DAYS=7
DRY_RUN=false

log() {
    echo "[$(date '+%Y-%m-%d %H:%M:%S')] $1"
}

# Argumente auswerten
while [[ $# -gt 0 ]]; do
    case "$1" in
        --dry-run) DRY_RUN=true; shift ;;
        --max-age) MAX_AGE_DAYS="$2"; shift 2 ;;
        *) echo "Unbekannte Option: $1"; exit 1 ;;
    esac
done

TOTAL_FREED=0

for DIR_PATTERN in "${LOG_DIRS[@]}"; do
    for DIR in $DIR_PATTERN; do
        [[ -d "$DIR" ]] || continue
        log "Verarbeite $DIR..."

        # Logs komprimieren, die aelter als COMPRESS_AGE_DAYS sind
        while IFS= read -r -d '' file; do
            if [[ "$DRY_RUN" == true ]]; then
                log "  [TROCKENLAUF] Wuerde komprimieren: $file"
            else
                gzip "$file"
                log "  Komprimiert: $file"
            fi
        done < <(find "$DIR" -name "*.log" -mtime +"$COMPRESS_AGE_DAYS" -type f -print0)

        # Komprimierte Logs loeschen, die aelter als MAX_AGE_DAYS sind
        while IFS= read -r -d '' file; do
            SIZE=$(stat -c%s "$file")
            TOTAL_FREED=$((TOTAL_FREED + SIZE))
            if [[ "$DRY_RUN" == true ]]; then
                log "  [TROCKENLAUF] Wuerde loeschen: $file ($SIZE Bytes)"
            else
                rm -f "$file"
                log "  Geloescht: $file ($SIZE Bytes)"
            fi
        done < <(find "$DIR" -name "*.gz" -mtime +"$MAX_AGE_DAYS" -type f -print0)
    done
done

FREED_MB=$((TOTAL_FREED / 1024 / 1024))
log "Insgesamt freigegebener Speicherplatz: ${FREED_MB} MB"

Skript 3: Automatisiertes Backup-Skript

#!/bin/bash
set -euo pipefail

# Konfiguration
BACKUP_SOURCE="/var/www /etc/nginx /etc/myapp"
BACKUP_DEST="/var/backups"
RETENTION_DAYS=14
TIMESTAMP=$(date +%Y%m%d_%H%M%S)
HOSTNAME=$(hostname)
BACKUP_FILE="${BACKUP_DEST}/${HOSTNAME}_backup_${TIMESTAMP}.tar.gz"
LOG_FILE="/var/log/backup.log"

log() {
    echo "[$(date '+%Y-%m-%d %H:%M:%S')] $1" | tee -a "$LOG_FILE"
}

cleanup() {
    local EXIT_CODE=$?
    if [[ $EXIT_CODE -ne 0 ]]; then
        log "FEHLER: Backup fehlgeschlagen mit Exit-Code $EXIT_CODE"
        rm -f "$BACKUP_FILE"
    fi
}
trap cleanup EXIT

# Sicherstellen, dass das Backup-Verzeichnis existiert
mkdir -p "$BACKUP_DEST"

log "Starte Backup von: $BACKUP_SOURCE"
log "Ziel: $BACKUP_FILE"

# Komprimierten Tarball erstellen
# shellcheck disable=SC2086
tar -czf "$BACKUP_FILE" $BACKUP_SOURCE 2>/dev/null

BACKUP_SIZE=$(stat -c%s "$BACKUP_FILE")
BACKUP_SIZE_MB=$((BACKUP_SIZE / 1024 / 1024))
log "Backup erstellt: ${BACKUP_SIZE_MB} MB"

# Backup ueberpruefen
if tar -tzf "$BACKUP_FILE" > /dev/null 2>&1; then
    log "Backup-Integritaetspruefung: BESTANDEN"
else
    log "FEHLER: Backup-Integritaetspruefung FEHLGESCHLAGEN"
    exit 1
fi

# Alte Backups entfernen
DELETED_COUNT=0
while IFS= read -r -d '' old_backup; do
    rm -f "$old_backup"
    DELETED_COUNT=$((DELETED_COUNT + 1))
    log "Altes Backup entfernt: $old_backup"
done < <(find "$BACKUP_DEST" -name "${HOSTNAME}_backup_*.tar.gz" -mtime +"$RETENTION_DAYS" -type f -print0)

log "Backup abgeschlossen. $DELETED_COUNT alte(s) Backup(s) entfernt."

Skript 4: Benutzerkonto-Audit

#!/bin/bash
set -euo pipefail

# Konfiguration
OUTPUT_FILE="/tmp/user_audit_$(date +%Y%m%d).txt"
MIN_UID=1000

log() {
    echo "$1" | tee -a "$OUTPUT_FILE"
}

# Vorherige Ausgabe loeschen
> "$OUTPUT_FILE"

log "============================================"
log "  AUDIT-BERICHT DER BENUTZERKONTEN"
log "  Host: $(hostname)"
log "  Datum: $(date '+%Y-%m-%d %H:%M:%S')"
log "============================================"
log ""

# Alle menschlichen Benutzer auflisten (UID >= 1000)
log "--- Menschliche Benutzerkonten (UID >= $MIN_UID) ---"
while IFS=: read -r username _ uid gid _ home shell; do
    if [[ $uid -ge $MIN_UID ]]; then
        LAST_LOGIN=$(lastlog -u "$username" 2>/dev/null | tail -1 | awk '{print $4, $5, $6, $7, $8, $9}')
        GROUPS=$(groups "$username" 2>/dev/null | cut -d: -f2)
        HAS_SUDO="Nein"
        if groups "$username" 2>/dev/null | grep -qw "sudo\|wheel"; then
            HAS_SUDO="Ja"
        fi
        log "  Benutzer: $username (UID: $uid)"
        log "    Home: $home"
        log "    Shell: $shell"
        log "    Sudo: $HAS_SUDO"
        log "    Gruppen:$GROUPS"
        log "    Letzte Anmeldung: $LAST_LOGIN"
        log ""
    fi
done < /etc/passwd

# Benutzer mit leeren Passwoertern pruefen
log "--- Benutzer mit leeren Passwoertern ---"
EMPTY_PASS=$(sudo awk -F: '($2 == "") {print $1}' /etc/shadow 2>/dev/null || true)
if [[ -n "$EMPTY_PASS" ]]; then
    log "  WARNUNG: $EMPTY_PASS"
else
    log "  Keine gefunden (gut)."
fi
log ""

# Benutzer mit UID 0 pruefen (Root-Aequivalente)
log "--- Benutzer mit UID 0 (Root-Berechtigungen) ---"
while IFS=: read -r username _ uid _; do
    if [[ $uid -eq 0 ]]; then
        log "  $username (UID: 0)"
    fi
done < /etc/passwd
log ""

# SSH-autorisierte Schluessel auflisten
log "--- Autorisierte SSH-Schluessel ---"
for HOME_DIR in /home/*; do
    USERNAME=$(basename "$HOME_DIR")
    AUTH_KEYS="$HOME_DIR/.ssh/authorized_keys"
    if [[ -f "$AUTH_KEYS" ]]; then
        KEY_COUNT=$(wc -l < "$AUTH_KEYS")
        log "  $USERNAME: $KEY_COUNT Schluessel in $AUTH_KEYS"
    fi
done
log ""

log "Bericht gespeichert in: $OUTPUT_FILE"

Planung mit Cron

Cron ist der Standard-Job-Planer unter Linux. Er fuehrt Skripte in definierten Intervallen ohne manuelles Eingreifen aus.

Die Crontab bearbeiten

# Crontab fuer den aktuellen Benutzer bearbeiten
crontab -e

# Crontab fuer einen bestimmten Benutzer bearbeiten (erfordert Root)
sudo crontab -u www-data -e

# Aktuelle Crontab-Eintraege auflisten
crontab -l

Cron-Syntax

# ┌───────────── Minute (0 - 59)
# │ ┌───────────── Stunde (0 - 23)
# │ │ ┌───────────── Tag des Monats (1 - 31)
# │ │ │ ┌───────────── Monat (1 - 12)
# │ │ │ │ ┌───────────── Wochentag (0 - 7, 0 und 7 = Sonntag)
# │ │ │ │ │
# * * * * * Befehl

Gaengige Cron-Zeitplaene

# Festplatten-Monitor stuendlich ausfuehren
0 * * * * /usr/local/bin/disk-monitor.sh >> /var/log/disk-monitor.log 2>&1

# Backup taeglich um 2 Uhr ausfuehren
0 2 * * * /usr/local/bin/backup.sh >> /var/log/backup.log 2>&1

# Log-Bereinigung jeden Sonntag um 3 Uhr ausfuehren
0 3 * * 0 /usr/local/bin/log-cleanup.sh >> /var/log/cleanup.log 2>&1

# Benutzer-Audit am 1. jeden Monats ausfuehren
0 9 1 * * /usr/local/bin/user-audit.sh >> /var/log/user-audit.log 2>&1

# Gesundheitspruefung alle 5 Minuten ausfuehren
*/5 * * * * /usr/local/bin/health-check.sh > /dev/null 2>&1

Wichtige Cron-Regeln: Verwenden Sie immer absolute Pfade in Cron-Jobs (keine relativen Pfade). Cron laeuft mit einer minimalen Umgebung, daher enthaelt PATH moeglicherweise nicht /usr/local/bin. Leiten Sie die Ausgabe immer in eine Logdatei oder /dev/null um, damit Cron nicht bei jeder Ausfuehrung eine E-Mail sendet.

Umgebungsvariablen in Cron verwenden

# Umgebungsvariablen am Anfang Ihrer Crontab setzen
SHELL=/bin/bash
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
MAILTO=admin@example.com

# Jetzt haben Ihre Jobs Zugriff auf gaengige Pfade
0 2 * * * /usr/local/bin/backup.sh

Bash-Operatoren Referenztabelle

KategorieOperatorBeschreibungBeispiel
Dateitests-fDatei existiert und ist regulaer[[ -f /etc/hosts ]]
-dVerzeichnis existiert[[ -d /var/log ]]
-eDatei oder Verzeichnis existiert[[ -e /tmp/lock ]]
-rDatei ist lesbar[[ -r config.yml ]]
-wDatei ist beschreibbar[[ -w /var/log/app.log ]]
-xDatei ist ausfuehrbar[[ -x script.sh ]]
-sDateigroesse groesser als Null[[ -s output.log ]]
-LIst ein symbolischer Link[[ -L /usr/bin/python ]]
Zeichenketten==Zeichenketten sind gleich[[ "$a" == "$b" ]]
!=Zeichenketten sind ungleich[[ "$a" != "$b" ]]
-zZeichenkette ist leer[[ -z "$var" ]]
-nZeichenkette ist nicht leer[[ -n "$var" ]]
=~Regex-Abgleich[[ "$s" =~ ^[0-9]+$ ]]
Numerisch-eqGleich[[ $a -eq $b ]]
-neUngleich[[ $a -ne $b ]]
-gtGroesser als[[ $a -gt $b ]]
-geGroesser oder gleich[[ $a -ge $b ]]
-ltKleiner als[[ $a -lt $b ]]
-leKleiner oder gleich[[ $a -le $b ]]
Logisch&&UND (innerhalb [[ ]])[[ $a -gt 0 && $a -lt 100 ]]
||ODER (innerhalb [[ ]])[[ $a -eq 0 || $a -eq 1 ]]
!NICHT[[ ! -f /tmp/lock ]]

Fehlerbehebung und Debugging

Wenn ein Skript nicht wie erwartet funktioniert, verwenden Sie diese Techniken, um das Problem zu finden.

Debugging mit set -x

#!/bin/bash
set -x  # Jeden Befehl vor der Ausfuehrung ausgeben

NAME="world"
echo "Hello, $NAME"
# Ausgabe:
# + NAME=world
# + echo 'Hello, world'
# Hello, world

Sie koennen das Debugging fuer bestimmte Abschnitte aktivieren:

#!/bin/bash

echo "Normale Ausgabe hier"

set -x  # Debug-Ausgabe starten
RESULT=$(some_complex_command)
process "$RESULT"
set +x  # Debug-Ausgabe stoppen

echo "Zurueck zur normalen Ausgabe"

Ein Skript im Debug-Modus ausfuehren

# Das gesamte Skript debuggen, ohne es zu aendern
bash -x ./myscript.sh

# Ausfuehrlicher Modus (Zeilen ausgeben, wie sie gelesen werden)
bash -v ./myscript.sh

# Kombiniert ausfuehrlich + Trace
bash -xv ./myscript.sh

ShellCheck: Statische Analyse

ShellCheck ist ein unverzichtbares Werkzeug, das gaengige Bash-Fehler, Anführungszeichenfehler und Portabilitaetsprobleme erkennt.

# ShellCheck installieren
sudo apt install -y shellcheck

# Ein Skript analysieren
shellcheck myscript.sh

# Beispielausgabe:
# In myscript.sh line 5:
# echo $VARIABLE
#      ^--------^ SC2086: Double quote to prevent globbing and word splitting.

Gaengige Debugging-Checkliste

  1. Shebang pruefen: Ist es #!/bin/bash (nicht #!/bin/sh)?
  2. Berechtigungen pruefen: Haben Sie chmod +x script.sh ausgefuehrt?
  3. Zeilenenden pruefen: Windows-Zeilenenden (\r\n) verursachen /bin/bash^M: bad interpreter. Beheben Sie es mit dos2unix script.sh.
  4. Variablen in Anfuehrungszeichen setzen: Verwenden Sie immer "$VARIABLE" statt $VARIABLE, um Leerzeichen und Sonderzeichen zu behandeln.
  5. Exit-Codes pruefen: Ueberpruefen Sie nach jedem kritischen Befehl $? oder verwenden Sie set -e.
  6. Mit festen Werten testen: Ersetzen Sie Variablen durch bekannte Werte, um das Problem zu isolieren.
  7. Cron-Umgebung pruefen: Wenn ein Skript manuell funktioniert, aber nicht in Cron, liegt das Problem meist am PATH oder fehlenden Umgebungsvariablen.
# Schnelles Skript zum Ausgeben der Cron-Umgebung
* * * * * env > /tmp/cron-env.txt 2>&1
# (nach der Pruefung entfernen)

Zusammenfassung

Bash-Scripting ist eine der wertvollsten Faehigkeiten, die ein Linux-Systemadministrator entwickeln kann. In diesem Leitfaden haben Sie gelernt, wie man:

  • Skripte mit korrekter Struktur schreibt (Shebang, Berechtigungen, Strict Mode)
  • Variablen, Arrays und Befehlssubstitution verwendet
  • Den Ablauf mit Bedingungen und Schleifen steuert
  • Code mit Funktionen organisiert
  • Fehler sicher mit set -euo pipefail und trap behandelt
  • Praktische Skripte fuer Festplattenueberwachung, Log-Bereinigung, Backups und Benutzer-Audits erstellt
  • Skripte mit Cron fuer vollautomatische Ausfuehrung plant
  • Skripte mit set -x und ShellCheck debuggt

Die vier praktischen Skripte in diesem Artikel sind Ausgangspunkte. Passen Sie sie an Ihre spezifische Umgebung an, fuegen Sie E-Mail-Benachrichtigungen hinzu, integrieren Sie sie in Ueberwachungssysteme und bauen Sie auf ihnen auf, wenn Ihre Anforderungen wachsen.

Fuer verwandte Themen zur Serveradministration schauen Sie sich unsere Linux Server Security Checklist: 20 Essential Steps an, um die Server zu sichern, auf denen Ihre Skripte laufen, und SSH Hardening: 12 Steps to Secure Your Linux Server, um den SSH-Zugang abzusichern, den Sie zur Verwaltung nutzen.