Bash Grundkurs #6: Fehlerbehandlung und Debugging in Bash

Willkommen zum sechsten Artikel unserer technischen Wiki-Serie über Bash-Programmierung!

In den vorherigen Artikeln hast du gelernt, wie du Bash-Skripte erstellst, mit Variablen arbeitest und Kontrollstrukturen wie if-else und Schleifen verwendest. Heute lernen wir etwas sehr Wichtiges: Wie du Fehler in deinen Skripten findest und behandelst.

Warum ist das wichtig?

Stell dir vor, du hast ein Skript geschrieben, aber es funktioniert nicht wie erwartet. Oder noch schlimmer: Es läuft auf deinem Computer perfekt, aber wenn es jemand anders benutzt, treten Fehler auf. Solche Situationen sind normal in der Programmierung, und heute lernst du, wie du damit umgehst.

In diesem Artikel lernst du:

  • Wie du häufige Fehler erkennst und vermeidest
  • Wie du dein Skript Schritt für Schritt debuggst
  • Wie du Fehlermeldungen richtig verwendest
  • Wie du dein Skript robuster machst

Lass uns mit dem Grundlegendsten beginnen: Wie Bash mit Fehlern umgeht.

Grundlegende Fehlerbehandlung

Exit-Codes

In Bash zeigt jeder Befehl durch seinen Exit-Code an, ob er erfolgreich war oder nicht. Das ist wie ein Ampelsystem:

  • Exit-Code 0 bedeutet „Erfolg“ (grüne Ampel)
  • Jeder andere Exit-Code (1-255) bedeutet „Fehler“ (rote Ampel)
Exit-Codes überprüfen
Bash
#!/bin/bash

# Einen Befehl ausführen und Exit-Code prüfen
ls /existierender_ordner
if [ $? -eq 0 ]; then
    echo "Der Befehl war erfolgreich"
else
    echo "Der Befehl ist fehlgeschlagen"
fi

# Direkt im if-Statement prüfen
if ls /nicht_existierender_ordner; then
    echo "Ordner gefunden"
else
    echo "Ordner nicht gefunden"
fi
Eigene Exit-Codes verwenden
Bash
#!/bin/bash

pruefe_alter() {
    local alter=$1
    
    # Prüfe, ob eine Zahl eingegeben wurde
    if ! [[ $alter =~ ^[0-9]+$ ]]; then
        echo "Fehler: Bitte eine Zahl eingeben" >&2
        return 1
    fi
    
    # Prüfe, ob das Alter realistisch ist
    if [ $alter -lt 0 ] || [ $alter -gt 120 ]; then
        echo "Fehler: Alter muss zwischen 0 und 120 liegen" >&2
        return 2
    fi
    
    echo "Das eingegebene Alter ist gültig"
    return 0
}

# Funktion testen
pruefe_alter "25"     # Sollte erfolgreich sein
pruefe_alter "abc"    # Sollte Fehler 1 zurückgeben
pruefe_alter "150"    # Sollte Fehler 2 zurückgeben

Wichtig für Anfänger:

  • $? enthält immer den Exit-Code des letzten Befehls
  • Fehlermeldungen sollten auf stderr (>&2) ausgegeben werden
  • Verwende unterschiedliche Exit-Codes für verschiedene Fehlerarten
  • Dokumentiere die Bedeutung deiner Exit-Codes

Debugging-Techniken

Beim Debugging helfen dir verschiedene Werkzeuge, den Ablauf deines Skripts zu verfolgen und Fehler zu finden. Die wichtigsten Werkzeuge sind set -x und set -e.

Debug-Modus mit set -x
Bash
#!/bin/bash

# Debug-Modus einschalten
set -x

echo "Starte das Skript"
name="Max"
echo "Hallo $name"

# Debug-Modus ausschalten
set +x

echo "Debug-Modus ist jetzt aus"

Wenn du dieses Skript ausführst, siehst du jede Zeile, bevor sie ausgeführt wird. Das hilft dir zu verstehen, was dein Skript genau macht.

Fehler abfangen mit set -e
Bash
#!/bin/bash

# Skript beenden bei Fehlern
set -e

echo "Dieser Befehl funktioniert"
ls /existierender_ordner

echo "Dieser Befehl wird einen Fehler erzeugen"
ls /nicht_existierender_ordner

echo "Diese Zeile wird nie erreicht, weil das Skript vorher abbricht"
Debugging-Ausgaben einfügen
Bash
#!/bin/bash

# Debug-Funktion erstellen
debug() {
    local nachricht="$1"
    # Nur ausgeben wenn DEBUG=1 gesetzt ist
    if [ "${DEBUG:-0}" -eq 1 ]; then
        echo "[DEBUG] $nachricht" >&2
    fi
}

# Beispielverwendung
DEBUG=1  # Debug-Ausgaben aktivieren

debug "Skript gestartet"
debug "Überprüfe Verzeichnis"
if [ -d "/tmp" ]; then
    debug "Verzeichnis /tmp existiert"
else
    debug "Verzeichnis /tmp fehlt"
fi

Wichtige Debug-Optionen:

  • set -x: Zeigt jeden Befehl vor der Ausführung
  • set -e: Beendet das Skript bei Fehlern
  • set -v: Zeigt jede Zeile beim Einlesen
  • set -u: Fehler bei undefinierten Variablen

Logging implementieren

Ein gutes Logging-System hilft dir, nachzuvollziehen, was dein Skript macht und was schiefgegangen ist. Lass uns ein einfaches, aber effektives Logging-System aufbauen.

Grundlegendes Logging
Bash
#!/bin/bash

# Logging-Funktion erstellen
log() {
    local level="$1"
    local nachricht="$2"
    local zeitstempel=$(date "+%Y-%m-%d %H:%M:%S")
    echo "[$zeitstempel] [$level] $nachricht" >> "skript.log"
}

# Verschiedene Log-Level
log_info() {
    log "INFO" "$1"
    echo "INFO: $1"
}

log_warning() {
    log "WARNUNG" "$1"
    echo "WARNUNG: $1" >&2
}

log_error() {
    log "FEHLER" "$1"
    echo "FEHLER: $1" >&2
}

# Beispielverwendung
log_info "Skript gestartet"
log_warning "Datei könnte veraltet sein"
log_error "Zugriff verweigert"
Erweitertes Logging mit Funktionen
Bash
#!/bin/bash

# Konfiguration
LOG_DATEI="anwendung.log"
LOG_LEVEL="INFO"  # Mögliche Werte: DEBUG, INFO, WARNING, ERROR

# Hilfsfunktion für Log-Level-Vergleich
ist_log_level_aktiv() {
    local level="$1"
    case "$LOG_LEVEL" in
        "DEBUG")   return 0 ;;
        "INFO")    [[ "$level" != "DEBUG" ]] && return 0 ;;
        "WARNING") [[ "$level" =~ ^(WARNING|ERROR)$ ]] && return 0 ;;
        "ERROR")   [[ "$level" == "ERROR" ]] && return 0 ;;
    esac
    return 1
}

# Erweiterte Logging-Funktion
log() {
    local level="$1"
    local nachricht="$2"
    
    # Prüfe, ob dieser Log-Level aktiv ist
    if ist_log_level_aktiv "$level"; then
        local zeitstempel=$(date "+%Y-%m-%d %H:%M:%S")
        local prozess_id="$$"
        echo "[$zeitstempel] [$level] (PID:$prozess_id) $nachricht" >> "$LOG_DATEI"
        
        # Fehler und Warnungen auch auf stderr ausgeben
        if [[ "$level" =~ ^(ERROR|WARNING)$ ]]; then
            echo "[$level] $nachricht" >&2
        fi
    fi
}

# Beispielverwendung mit Fehlerbehandlung
datei_verarbeiten() {
    local dateiname="$1"
    
    log "INFO" "Starte Verarbeitung von: $dateiname"
    
    if [ ! -f "$dateiname" ]; then
        log "ERROR" "Datei nicht gefunden: $dateiname"
        return 1
    fi
    
    if [ ! -r "$dateiname" ]; then
        log "ERROR" "Keine Leserechte für: $dateiname"
        return 2
    fi
    
    log "DEBUG" "Lese Dateiinhalt..."
    # Hier würde die eigentliche Verarbeitung stattfinden
    
    log "INFO" "Verarbeitung abgeschlossen"
    return 0
}
Log-Rotation implementieren
Bash
#!/bin/bash

# Log-Rotation-Funktion
rotate_logs() {
    local log_datei="$1"
    local max_groesse=$((1024 * 1024))  # 1MB
    
    if [ -f "$log_datei" ]; then
        # Prüfe Dateigröße
        local groesse=$(stat -f%z "$log_datei" 2>/dev/null || stat -c%s "$log_datei")
        
        if [ $groesse -gt $max_groesse ]; then
            log "INFO" "Starte Log-Rotation"
            # Alte Log-Datei umbenennen
            mv "$log_datei" "${log_datei}.$(date +%Y%m%d-%H%M%S)"
            # Neue Log-Datei erstellen
            touch "$log_datei"
            log "INFO" "Log-Rotation abgeschlossen"
        fi
    fi
}

Wichtige Aspekte beim Logging:

  • Verwende verschiedene Log-Level für unterschiedliche Wichtigkeitsstufen
  • Füge Zeitstempel zu den Log-Einträgen hinzu
  • Implementiere Log-Rotation, um die Dateigröße zu begrenzen
  • Speichere wichtige Informationen wie Prozess-ID und Kontext

Praktische Debugging-Strategien

Lass uns lernen, wie du systematisch Fehler in deinen Skripten finden und beheben kannst. Hier sind die wichtigsten Strategien mit praktischen Beispielen:

Schrittweise Fehlersuche
Bash
#!/bin/bash

# Debug-Modus für einen bestimmten Bereich aktivieren
debug_bereich() {
    echo "=== Debug-Bereich Start ==="
    set -x  # Debug-Modus an
    
    # Hier kommt der Code, den wir debuggen wollen
    local name="Max"
    echo "Verarbeite: $name"
    # Weitere Befehle...
    
    set +x  # Debug-Modus aus
    echo "=== Debug-Bereich Ende ==="
}

# Beispiel für schrittweise Fehlersuche
verarbeite_daten() {
    local datei="$1"
    
    echo "Schritt 1: Prüfe Datei"
    [ -f "$datei" ] || { echo "Fehler: Datei nicht gefunden"; return 1; }
    
    echo "Schritt 2: Prüfe Leserechte"
    [ -r "$datei" ] || { echo "Fehler: Keine Leserechte"; return 2; }
    
    echo "Schritt 3: Verarbeite Inhalt"
    while read -r zeile; do
        echo "Verarbeite: $zeile"
    done < "$datei"
}
Variablen überprüfen
Bash
#!/bin/bash

# Aktiviere Fehler bei undefinierten Variablen
set -u

debug_variablen() {
    local var1="Wert1"
    local var2
    
    echo "var1=${var1:-nicht_gesetzt}"
    echo "var2=${var2:-nicht_gesetzt}"
    
    # Prüfe ob Variable gesetzt ist
    if [ -z "${var2+x}" ]; then
        echo "var2 ist nicht gesetzt"
    fi
}

# Beispiel für Variablenprüfung in Funktionen
verarbeite_eingabe() {
    local eingabe="$1"
    
    # Prüfe ob Parameter übergeben wurde
    if [ -z "$eingabe" ]; then
        echo "Fehler: Keine Eingabe erhalten" >&2
        return 1
    fi
    
    # Zeige Variableninhalt für Debugging
    echo "DEBUG: eingabe=$eingabe" >&2
    
    # Verarbeitung...
}
Trap für Fehlerbehandlung
Bash
#!/bin/bash

# Fehlerbehandlung einrichten
fehler_aufgetreten() {
    local zeile=$1
    local befehl=$2
    echo "Fehler in Zeile $zeile: $befehl fehlgeschlagen" >&2
    # Aufräumarbeiten hier...
}

# Trap für Fehler einrichten
trap 'fehler_aufgetreten ${LINENO} "$BASH_COMMAND"' ERR

# Trap für sauberes Beenden
cleanup() {
    echo "Räume auf..."
    # Temporäre Dateien löschen, Verbindungen schließen, etc.
}

trap cleanup EXIT

# Beispielcode der Fehler erzeugen könnte
nicht_existierende_datei() {
    cat /nicht/vorhanden.txt
}

teste_fehler() {
    echo "Teste Fehlerbehandlung..."
    nicht_existierende_datei
    echo "Diese Zeile wird nicht erreicht"
}
Häufige Fehlerquellen und deren Vermeidung
Bash
#!/bin/bash

# 1. Leerzeichen bei Variablenzuweisungen
# Falsch
name = "Max"    # Erzeugt einen Fehler
# Richtig
name="Max"

# 2. Anführungszeichen bei Variablen
dateiname="Meine Datei.txt"
# Falsch
rm $dateiname   # Wird als zwei separate Argumente interpretiert
# Richtig
rm "$dateiname"

# 3. Pfade mit Leerzeichen
# Falsch
cd /pfad mit/leerzeichen
# Richtig
cd "/pfad mit/leerzeichen"

# 4. Prüfung auf leere Variablen
# Unsicher
if [ $variable = "wert" ]; then
# Sicher
if [ "$variable" = "wert" ]; then

# 5. Numerische Vergleiche
# Falsch
if [ $zahl = 5 ]; then    # String-Vergleich
# Richtig
if [ $zahl -eq 5 ]; then  # Numerischer Vergleich

Übung

Erstelle ein Skript für die Datensicherung von Verzeichnissen, das folgende Anforderungen erfüllt:

  1. Das Skript soll:
    • Ein Verzeichnis sichern
    • Fehler erkennen und protokollieren
    • Eine ausführliche Log-Datei erstellen
    • Bei Problemen sauber aufräumen
  2. Implementiere:
    • Fehlerbehandlung für alle kritischen Operationen
    • Ausführliches Logging
    • Debugging-Möglichkeiten
    • Sauberes Aufräumen bei Abbruch
  3. Beachte:
    • Überprüfe alle Eingaben
    • Protokolliere jeden wichtigen Schritt
    • Gib sinnvolle Fehlermeldungen aus

Hier ist eine mögliche Lösung:

Bash
#!/bin/bash

# Konfiguration
BACKUP_DIR="/tmp/backups"
LOG_FILE="/var/log/backup.log"
DEBUG=0

# Logging-Funktion
log() {
    local level="$1"
    local message="$2"
    local timestamp=$(date "+%Y-%m-%d %H:%M:%S")
    echo "[$timestamp] [$level] $message" >> "$LOG_FILE"
    
    # Bei Debug-Modus oder Fehlern auch auf stderr ausgeben
    if [ "$DEBUG" = "1" ] || [ "$level" = "ERROR" ]; then
        echo "[$level] $message" >&2
    fi
}

# Aufräumfunktion
cleanup() {
    local exit_code=$?
    log "INFO" "Aufräumen nach Beendigung (Exit-Code: $exit_code)"
    # Temporäre Dateien löschen, falls vorhanden
    rm -f "/tmp/backup_temp_$$" 2>/dev/null
    exit $exit_code
}

# Fehlerbehandlung
error_handler() {
    local line_number=$1
    local command=$2
    log "ERROR" "Fehler in Zeile $line_number: $command fehlgeschlagen"
    exit 1
}

# Traps einrichten
trap cleanup EXIT
trap 'error_handler ${LINENO} "$BASH_COMMAND"' ERR

# Hauptfunktion
main() {
    local source_dir="$1"
    
    # Eingabevalidierung
    if [ -z "$source_dir" ]; then
        log "ERROR" "Kein Quellverzeichnis angegeben"
        return 1
    fi
    
    if [ ! -d "$source_dir" ]; then
        log "ERROR" "Quellverzeichnis existiert nicht: $source_dir"
        return 1
    fi
    
    # Backup-Verzeichnis erstellen
    log "INFO" "Erstelle Backup-Verzeichnis"
    mkdir -p "$BACKUP_DIR"
    
    # Backup erstellen
    local backup_file="$BACKUP_DIR/backup_$(date +%Y%m%d_%H%M%S).tar.gz"
    log "INFO" "Erstelle Backup: $backup_file"
    
    if tar -czf "$backup_file" "$source_dir" 2>/dev/null; then
        log "INFO" "Backup erfolgreich erstellt"
    else
        log "ERROR" "Backup fehlgeschlagen"
        return 1
    fi
}

# Skript starten
log "INFO" "Backup-Skript gestartet"
main "$1"

Fazit

In diesem sechsten Teil unseres Bash-Grundkurses hast du die wichtigen Konzepte der Fehlerbehandlung und des Debuggings kennengelernt. Du weißt nun, wie du systematisch Fehler in deinen Skripten findest und diese behebst. Das Logging hilft dir dabei, den Überblick über die Abläufe in deinen Skripten zu behalten, während die verschiedenen Debugging-Techniken dir ermöglichen, Probleme schnell zu lokalisieren und zu beheben.

Die praktischen Übungen haben dir gezeigt, wie wichtig eine gute Fehlerbehandlung für robuste Skripte ist. Besonders das Backup-Beispiel demonstriert, wie du Fehlerbehandlung, Logging und Debugging in einem realen Szenario kombinierst.

Im siebten und letzten Teil unserer Serie werden wir uns mit fortgeschrittenen Bash-Techniken beschäftigen. Du wirst lernen, wie du Arrays verwendest, mit regulären Ausdrücken arbeitest und komplexe Kommandozeilenargumente verarbeitest.

Bis dahin, happy scripting!

Kommentar verfassen