12 Debugging und Fehlerbehandlung in Shell-Skripten

12.1 Robuste Fehlerbehandlungsstrategien

Shell-Skripte werden häufig in kritischen Umgebungen eingesetzt, in denen Fehler schwerwiegende Folgen haben können. Ohne angemessene Fehlerbehandlung besteht die Gefahr, dass Skripte in unerwarteten Situationen unvorhersehbar reagieren oder kritische Ressourcen beschädigen. Eine robuste Fehlerbehandlung ist daher ein wesentlicher Bestandteil jedes professionellen Skripts.

12.1.1 Grundlegende Fehlerbehandlung mit Exit-Codes

In der Shell-Programmierung kommunizieren Programme ihren Erfolg oder Misserfolg durch Exit-Codes. Eine 0 signalisiert Erfolg, während Werte ungleich Null auf einen Fehler hindeuten. Diese Exit-Codes können und sollten in Skripten ausgewertet werden:

# Beispiel: Überprüfung eines Exit-Codes
if ! cp datei.txt backup/; then
    echo "Fehler beim Kopieren der Datei!" >&2
    exit 1
fi

Der Exit-Code des zuletzt ausgeführten Befehls ist in der Variablen $? gespeichert:

mkdir /tmp/testordner
if [ $? -ne 0 ]; then
    echo "Fehler beim Erstellen des Ordners" >&2
    exit 1
fi

12.1.2 Die set-Optionen für robustere Skripte

Die Bash bietet mehrere wertvolle Optionen über das set-Kommando, die dazu beitragen, Fehler früher zu erkennen und robustere Skripte zu schreiben:

#!/bin/bash
set -e          # Beendet das Skript sofort, wenn ein Befehl fehlschlägt
set -u          # Behandelt nicht definierte Variablen als Fehler
set -o pipefail # Gibt den Exit-Code des ersten fehlgeschlagenen Befehls in einer Pipe zurück

Diese drei Optionen können auch in Kurzform kombiniert werden:

#!/bin/bash
set -euo pipefail

Diese Kombination wird oft als “strikte Modus” bezeichnet und sollte für die meisten Skripte die Standardeinstellung sein.

12.1.3 Fehlerbehandlung mit trap

Das trap-Kommando ermöglicht es, Aktionen bei bestimmten Signalen oder Ereignissen auszuführen, insbesondere zum Aufräumen temporärer Ressourcen:

#!/bin/bash
set -euo pipefail

# Temporäre Datei erstellen
TEMP_FILE=$(mktemp)

# Aufräumfunktion definieren
cleanup() {
    echo "Aufräumen temporärer Ressourcen..." >&2
    rm -f "$TEMP_FILE"
}

# trap registrieren für mehrere Signale
trap cleanup EXIT SIGINT SIGTERM

# Hauptlogik des Skripts
echo "Arbeite mit temporärer Datei: $TEMP_FILE"
# ... weitere Befehle ...

Die trap-Anweisung sorgt dafür, dass die cleanup-Funktion beim Beenden des Skripts (EXIT) oder bei Abbruch durch den Benutzer (SIGINT durch Ctrl+C, SIGTERM) ausgeführt wird. Dadurch werden temporäre Ressourcen ordnungsgemäß freigegeben.

12.1.4 Benutzerdefinierte Fehlerbehandlungsfunktionen

Für komplexere Skripte empfiehlt sich die Implementierung einer einheitlichen Fehlerbehandlungsfunktion:

#!/bin/bash
set -euo pipefail

# Fehlerbehandlungsfunktion
error_exit() {
    local message="$1"
    local exit_code="${2:-1}"  # Default: 1
    
    echo "[FEHLER] $message" >&2
    # Optional: Logging in eine Datei
    echo "$(date '+%Y-%m-%d %H:%M:%S') - FEHLER: $message" >> /var/log/mein_skript.log
    
    exit "$exit_code"
}

# Verwendung
if ! ping -c 1 example.com > /dev/null 2>&1; then
    error_exit "Netzwerkverbindung nicht verfügbar" 10
fi

# Weiterer Code...

12.1.5 Validierung von Eingaben

Eine präventive Fehlerbehandlungsstrategie ist die Validierung aller Eingaben, bevor sie verwendet werden:

#!/bin/bash
set -euo pipefail

# Prüfen der Anzahl der Argumente
if [ "$#" -lt 2 ]; then
    echo "Verwendung: $0 QUELLE ZIEL" >&2
    exit 1
fi

QUELLE="$1"
ZIEL="$2"

# Validierung der Eingabedatei
if [ ! -f "$QUELLE" ]; then
    echo "Fehler: Die Quelldatei '$QUELLE' existiert nicht oder ist keine reguläre Datei." >&2
    exit 2
fi

# Validierung des Zielverzeichnisses
if [ ! -d "$(dirname "$ZIEL")" ]; then
    echo "Fehler: Das Zielverzeichnis '$(dirname "$ZIEL")' existiert nicht." >&2
    exit 3
fi

# Nach der Validierung kann die eigentliche Operation durchgeführt werden
cp "$QUELLE" "$ZIEL"

12.1.6 Protokollierung von Fehlern

Die systematische Protokollierung von Fehlern erleichtert die Diagnose und Behebung von Problemen:

#!/bin/bash
set -euo pipefail

LOG_FILE="/var/log/mein_skript.log"

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

# Verwendung
log "INFO" "Skript gestartet"

if ! some_command; then
    log "ERROR" "Befehl fehlgeschlagen: some_command"
    exit 1
fi

log "INFO" "Skript erfolgreich beendet"

12.1.7 Fehlerbehandlung in Funktionen

Bei modularen Skripten mit mehreren Funktionen kann die Fehlerbehandlung verfeinert werden:

#!/bin/bash
set -euo pipefail

# Funktion, die einen Fehler zurückgibt
function db_backup() {
    local db_name="$1"
    local backup_file="$2"
    
    if ! mysqldump "$db_name" > "$backup_file" 2>/dev/null; then
        echo "Datenbankbackup fehlgeschlagen" >&2
        return 1  # Fehlerstatus zurückgeben
    fi
    
    return 0  # Erfolg
}

# Aufruf mit Fehlerbehandlung
if ! db_backup "meine_db" "/backup/meine_db.sql"; then
    echo "Backup-Prozess fehlgeschlagen, breche ab" >&2
    exit 1
fi

12.1.8 Erweiterte Fehlerbehandlung mit Timeout

Für Befehle, die möglicherweise hängen bleiben, kann ein Timeout definiert werden:

#!/bin/bash
set -euo pipefail

# Befehl mit Timeout ausführen
if ! timeout 10s ping -c 4 example.com; then
    echo "Ping zu example.com fehlgeschlagen oder Timeout überschritten" >&2
    exit 1
fi

12.2 Logging und Nachverfolgung von Fehlern

Ein effektives Logging-System ist für die Diagnose und Behebung von Problemen in Shell-Skripten unverzichtbar, besonders in Produktionsumgebungen. Dieser Abschnitt konzentriert sich auf professionelle Logging-Methoden mit Fokus auf die Integration in die Systemprotokollierung.

12.2.1 Professionelles Logging mit dem logger-Befehl

Das logger-Kommando ist das zentrale Werkzeug für professionelles Logging in Shell-Skripten. Es integriert sich nahtlos in die System-Logging-Infrastruktur (syslog/rsyslog/journald) und bietet zahlreiche Vorteile gegenüber dem manuellen Schreiben in Dateien:

#!/bin/bash
set -euo pipefail

# Grundlegende Verwendung von logger
logger "Skript gestartet: $(basename "$0")"

# Mit Priorität (facility.level)
logger -p local0.info "Normale Informationsmeldung"
logger -p local0.warning "Warnung: Festplatte zu 85% gefüllt"
logger -p local0.error "Fehler: Verbindung zum Server fehlgeschlagen"

# Mit Tag für bessere Identifikation
logger -t "backup_script" "Backup-Vorgang gestartet"

# Logger mit Prozess-ID
logger -i -t "user_mgmt" "Benutzer hinzugefügt: $username"

# Strukturiertes Logging mit Metadaten
logger -t "database" --id="$transaction_id" "Datenbankabfrage abgeschlossen: $query_time ms"

Die wichtigsten Optionen des logger-Befehls:

Option Beschreibung
-p facility.level Setzt Facility und Level (z.B. local0.info)
-t tag Definiert ein Tag zur leichteren Filterung
-i Fügt die Prozess-ID hinzu
--id=ID Fügt eine benutzerdefinierte Nachrichten-ID hinzu (neuere Versionen)
-f file Sendet den Inhalt einer Datei
-n server Sendet Log an einen entfernten syslog-Server
--journald[=FIELD] Setzt journald-spezifische Felder (systemd-Systeme)

12.2.2 Integration in die Systemprotokollierung

Das Logging mit logger wird automatisch in die Systemprotokolle aufgenommen:

# Log-Meldungen in den Systemlogs finden
grep "backup_script" /var/log/syslog
journalctl -t "backup_script"  # Auf systemd-basierten Systemen

Um die Logs in spezifische Dateien zu leiten, kann die syslog-Konfiguration angepasst werden:

# /etc/rsyslog.d/10-custom-scripts.conf
local0.* /var/log/custom-scripts.log

Nach dieser Änderung muss der syslog-Dienst neu gestartet werden:

systemctl restart rsyslog

12.2.3 Strukturiertes Logging mit logger

Für systematisches Logging empfiehlt sich ein einheitliches Format:

#!/bin/bash
set -euo pipefail

# Zentrale Logging-Funktion
log() {
    local level=$1
    local message=$2
    local tag="${3:-$(basename "$0")}"

    # Syslog-Priorität basierend auf Level
    local priority="user.info"
    case $level in
        ERROR)   priority="user.err" ;;
        WARNING) priority="user.warning" ;;
        INFO)    priority="user.info" ;;
        DEBUG)   priority="user.debug" ;;
    esac

    # Logging an syslog mit aussagekräftigem Format
    logger -p "$priority" -t "$tag" "[$level] $message"
}

# Verwendung
log "INFO" "Skript gestartet mit Parameter: $*"
log "DEBUG" "Aktueller Speicherverbrauch: $(free -m | awk '/^Mem:/ {print $3}') MB"
log "WARNING" "Festplatte zu 90% voll" "disk_monitor"
log "ERROR" "Datei konnte nicht geöffnet werden: $filename" "file_handler"

12.2.4 Erweitertes Logging mit journald auf systemd-Systemen

Auf Systemen mit systemd bietet journald erweiterte Logging-Funktionen:

#!/bin/bash
set -euo pipefail

# Strukturiertes Logging mit journald
journald_log() {
    local level=$1
    local message=$2
    local tag="${3:-$(basename "$0")}"
    local code="${4:-}"  # Optionaler Fehlercode
    
    # Grundlegende Felder
    local cmd=(logger -t "$tag" --journald)
    
    # Statusfelder hinzufügen
    cmd+=(PRIORITY="$level" 
          CODE_FILE="$(caller | cut -d ' ' -f 3)" 
          CODE_LINE="$(caller | cut -d ' ' -f 1)")
    
    # Optionalen Fehlercode hinzufügen
    if [[ -n "$code" ]]; then
        cmd+=(CODE="$code")
    fi
    
    # Log senden
    "${cmd[@]}" "$message"
}

# Verwendung
journald_log 6 "Debug-Nachricht" "my_app"  # 6 = INFO
journald_log 3 "Kritischer Fehler" "my_app" "E1001"  # 3 = ERROR

Diese Logs können mit erweiterten journalctl-Filtern durchsucht werden:

# Nach Tag filtern
journalctl -t "my_app"

# Nach Priorität filtern
journalctl PRIORITY=3 -t "my_app"

# Nach Fehlercode filtern 
journalctl CODE="E1001"

# Nach Datei/Zeile filtern
journalctl CODE_FILE="mein_skript.sh"

12.2.5 Remote-Logging für verteilte Systeme

In verteilten Umgebungen ist das Senden von Logs an zentrale Server wichtig:

#!/bin/bash
set -euo pipefail

# Log an entfernten syslog-Server senden
remote_log() {
    local level=$1
    local message=$2
    local server=$3
    local port="${4:-514}"  # Standard-syslog-Port
    local tag="${5:-$(basename "$0")}"
    
    # Priorität basierend auf Level
    local priority="user.info"
    case $level in
        ERROR)   priority="user.err" ;;
        WARNING) priority="user.warning" ;;
        INFO)    priority="user.info" ;;
        DEBUG)   priority="user.debug" ;;
    esac
    
    # Log an entfernten Server senden
    logger -p "$priority" -t "$tag" -n "$server" -P "$port" "[$level] $message"
}

# Verwendung
remote_log "ERROR" "Kritischer Fehler im Backup-Prozess" "logs.example.com" "514" "backup"

12.2.6 Kombination von syslog mit JSON-Logging

Für moderne Log-Verarbeitungssysteme (wie ELK Stack oder Graylog) ist JSON-Logging sinnvoll:

#!/bin/bash
set -euo pipefail

# JSON-Logging über syslog
json_log() {
    local level=$1
    local message=$2
    local tag="${3:-$(basename "$0")}"
    local additional_fields="${4:-}"
    
    # Basis-JSON erstellen
    local json="{\"timestamp\":\"$(date -u +"%Y-%m-%dT%H:%M:%SZ")\",\"level\":\"$level\",\"message\":\"$message\""
    
    # Weitere Felder hinzufügen
    if [[ -n "$additional_fields" ]]; then
        # Annahme: additional_fields ist bereits im JSON-Format ohne { }
        json="$json,$additional_fields"
    fi
    
    # JSON schließen
    json="$json}"
    
    # An syslog senden
    logger -p "user.info" -t "$tag" "$json"
}

# Verwendung
json_log "INFO" "Backup gestartet" "backup_script" "\"size\":1024,\"files\":15,\"target\":\"/mnt/backup\""

12.2.7 Log-Rotation mit logrotate

Für eigene Log-Dateien sollte logrotate konfiguriert werden:

# /etc/logrotate.d/custom-scripts
/var/log/custom-scripts.log {
    daily
    rotate 14
    compress
    delaycompress
    missingok
    notifempty
    create 0640 syslog adm
    postrotate
        systemctl reload rsyslog >/dev/null 2>&1 || true
    endscript
}

12.2.8 Performance-Optimierung für umfangreiches Logging

Bei hohem Log-Volumen können Performance-Probleme auftreten:

#!/bin/bash
set -euo pipefail

# Batch-Logging für hohe Performance
declare -a LOG_QUEUE
LOG_QUEUE_MAX_SIZE=100

# Log zur Queue hinzufügen
queue_log() {
    LOG_QUEUE+=("$*")
    
    # Queue leeren, wenn maximale Größe erreicht
    if [[ ${#LOG_QUEUE[@]} -ge $LOG_QUEUE_MAX_SIZE ]]; then
        flush_log_queue
    fi
}

# Log-Queue an syslog senden
flush_log_queue() {
    if [[ ${#LOG_QUEUE[@]} -eq 0 ]]; then
        return
    fi
    
    # Alle Logs in einer Datei sammeln
    local temp_file
    temp_file=$(mktemp)
    
    printf "%s\n" "${LOG_QUEUE[@]}" > "$temp_file"
    
    # Datei an logger senden und Queue leeren
    logger -f "$temp_file"
    rm -f "$temp_file"
    LOG_QUEUE=()
}

# Beim Beenden sicherstellen, dass alle Logs gesendet werden
trap flush_log_queue EXIT

# Verwendung
for i in {1..1000}; do
    # Operationen ausführen
    result=$((i * 2))
    
    # Log zur Queue hinzufügen
    queue_log "Verarbeite Element $i: Ergebnis=$result"
done

# Am Ende der Verarbeitung Queue leeren
flush_log_queue

12.2.9 Tracing mit set -x in Kombination mit syslog

Für detailliertes Debugging kann Tracing mit syslog kombiniert werden:

#!/bin/bash

# Trace-Ausgabe in syslog umleiten
exec 19> >(logger -p local0.debug -t "${0##*/}[$$] TRACE")
BASH_XTRACEFD=19
PS4='+(${BASH_SOURCE}:${LINENO}): ${FUNCNAME[0]:+${FUNCNAME[0]}(): }'
set -x

# Ab hier wird jeder ausgeführte Befehl in syslog protokolliert
echo "Beispielbefehl"
for i in {1..3}; do
    echo "Schleifendurchlauf $i"
done

# Tracing wieder ausschalten für bestimmte Abschnitte
set +x
echo "Dieser Befehl wird nicht getracet"
set -x

12.2.10 Logging-Levels und bedingte Protokollierung

Für flexible Logging-Kontrolle ist ein Level-basierter Ansatz sinnvoll:

#!/bin/bash
set -euo pipefail

# Log-Level-Konfiguration
# 0=DEBUG, 1=INFO, 2=WARNING, 3=ERROR
LOG_LEVEL=${LOG_LEVEL:-1}  # Default: INFO

# Log-Funktion mit Level-Steuerung
log() {
    local level_name=$1
    local message=$2
    local tag="${3:-$(basename "$0")}"
    
    # Level in numerischen Wert konvertieren
    local level_num
    case $level_name in
        DEBUG)   level_num=0 ;;
        INFO)    level_num=1 ;;
        WARNING) level_num=2 ;;
        ERROR)   level_num=3 ;;
        *)       level_num=1 ;;  # Default: INFO
    esac
    
    # Nur loggen, wenn Level höher/gleich konfiguriertem Level
    if [[ $level_num -ge $LOG_LEVEL ]]; then
        # syslog-Priorität bestimmen
        local priority
        case $level_name in
            DEBUG)   priority="local0.debug" ;;
            INFO)    priority="local0.info" ;;
            WARNING) priority="local0.warning" ;;
            ERROR)   priority="local0.err" ;;
            *)       priority="local0.info" ;;
        esac
        
        logger -p "$priority" -t "$tag" "[$level_name] $message"
    fi
}

# Verwendung
log "DEBUG" "Detaillierte Debug-Information"  # Wird nur bei LOG_LEVEL=0 ausgegeben
log "INFO" "Normale Informationsmeldung"      # Wird bei LOG_LEVEL<=1 ausgegeben
log "WARNING" "Warnung: Ressourcen knapp"     # Wird bei LOG_LEVEL<=2 ausgegeben
log "ERROR" "Kritischer Fehler aufgetreten"   # Wird immer ausgegeben bei LOG_LEVEL<=3

12.2.11 In a Nutshell

Ein professionelles Logging-System für Shell-Skripte sollte:

  1. Das logger-Kommando als primäres Werkzeug für die Integration mit Systemprotokollen nutzen
  2. Strukturierte Log-Formate für einfachere Analyse verwenden
  3. Level-basierte Protokollierung für Flexibilität implementieren
  4. Die erweiterten Möglichkeiten von journald auf systemd-Systemen nutzen
  5. Performance-Optimierungen für umfangreiches Logging vorsehen
  6. Remote-Logging für verteilte Umgebungen unterstützen
  7. Mit dem System-Log-Management (logrotate, etc.) integriert sein

Durch den richtigen Einsatz von logger und die Integration in die Systemprotokollierung können Shell-Skripte nahtlos in die bestehende Log-Infrastruktur eingebunden werden, was die Fehlersuche und die Überwachung erheblich erleichtert.

12.3 Debugging-Techniken und -Tools

Trotz sorgfältiger Planung und robuster Fehlerbehandlung treten in Shell-Skripten immer wieder unerklärliche Fehler auf. Das systematische Debugging solcher Probleme erfordert spezifische Techniken und Werkzeuge, die in diesem Abschnitt ausführlich vorgestellt werden.

12.3.1 Eingebaute Bash-Debugging-Funktionen

Die Bash-Shell bietet mehrere integrierte Mechanismen zum Debugging von Skripten:

12.3.1.1 Trace-Modus mit set -x

Der Trace-Modus ist das grundlegendste und häufig verwendete Debugging-Werkzeug:

#!/bin/bash
set -x  # Aktiviert den Trace-Modus für das gesamte Skript
echo "Starte Operation"
for i in {1..3}; do
    echo "Durchlauf $i"
done
set +x  # Deaktiviert den Trace-Modus

Bei der Ausführung werden alle Befehle sowie deren Ergebnisse angezeigt, wobei ausgeführte Befehle durch ein Präfix (standardmäßig +) gekennzeichnet sind:

+ echo 'Starte Operation'
Starte Operation
+ for i in {1..3}
+ echo 'Durchlauf 1'
Durchlauf 1
+ for i in {1..3}
+ echo 'Durchlauf 2'
Durchlauf 2
...

Der Trace-Modus kann auch selektiv für bestimmte Abschnitte aktiviert werden:

function problematische_funktion() {
    set -x  # Tracing nur für diese Funktion aktivieren
    # Code der Funktion
    set +x  # Tracing wieder deaktivieren
}

12.3.1.2 Anpassung des Trace-Formats mit PS4

Die Ausgabe des Trace-Modus wird durch die Variable PS4 gesteuert:

#!/bin/bash
# Angepasstes Trace-Format mit Dateiname, Zeilennummer und Funktionsname
export PS4='+(${BASH_SOURCE}:${LINENO}): ${FUNCNAME[0]:+${FUNCNAME[0]}(): }'
set -x

function test_func() {
    echo "In der Testfunktion"
}

test_func

Die Ausgabe könnte dann so aussehen:

+(mein_skript.sh:6): test_func(): echo 'In der Testfunktion'
In der Testfunktion

12.3.1.3 Verbose-Modus mit set -v

Während set -x Befehle nach der Expansion von Variablen zeigt, gibt der Verbose-Modus mit set -v die Zeilen vor der Expansion aus:

#!/bin/bash
set -v  # Aktiviert den Verbose-Modus
NAME="Welt"
echo "Hallo $NAME"
set +v  # Deaktiviert den Verbose-Modus

Die Ausgabe zeigt die ursprünglichen Zeilen:

NAME="Welt"
echo "Hallo $NAME"
Hallo Welt

Beide Modi können kombiniert werden, um sowohl die ursprünglichen Befehle als auch deren expandierte Form zu sehen:

#!/bin/bash
set -vx  # Aktiviert Verbose- und Trace-Modus

12.3.2 Das bashdb-Debugging-Tool

Für komplexere Skripte bietet das externe Tool bashdb einen interaktiven Debugger, ähnlich wie gdb für C-Programme:

# Installation (auf Debian/Ubuntu-Systemen)
apt-get install bashdb

# Verwendung
bashdb mein_skript.sh parameter1 parameter2

bashdb bietet folgende Funktionen:

Ein typischer Debugging-Ablauf könnte so aussehen:

bashdb> break 25       # Breakpoint bei Zeile 25 setzen
bashdb> run            # Ausführung starten
bashdb> print $i       # Wert von Variable $i anzeigen
bashdb> step           # Eine Zeile ausführen
bashdb> next           # Über Funktionsaufrufe hinweg ausführen
bashdb> continue       # Bis zum nächsten Breakpoint ausführen

12.3.3 Shellcheck: Statische Codeanalyse

Der statische Codeanalyzer shellcheck ist ein unverzichtbares Werkzeug zur Identifizierung von potenziellen Problemen in Shell-Skripten, bevor sie ausgeführt werden:

# Installation (auf Debian/Ubuntu-Systemen)
apt-get install shellcheck

# Verwendung
shellcheck mein_skript.sh

shellcheck erkennt eine Vielzahl von Problemen, darunter:

Beispielausgabe von shellcheck:

In mein_skript.sh line 15:
if [ $count -eq 0 ]
     ^-- SC2086: Double quote to prevent globbing and word splitting.

In mein_skript.sh line 23:
rm -rf $TMP_DIR
        ^-- SC2115: Use "${TMP_DIR:?}" to ensure TMP_DIR is not empty.

Die von shellcheck erkannten Probleme sind mit Referenznummern versehen, die online nachgeschlagen werden können, um detaillierte Erklärungen und Lösungsvorschläge zu erhalten.

12.3.4 Debugging mit temporären Dateien

In Situationen, in denen andere Debugging-Methoden nicht anwendbar sind, kann die Verwendung temporärer Dateien zum Protokollieren von Zuständen hilfreich sein:

#!/bin/bash
set -euo pipefail

DEBUG_FILE="/tmp/debug_$(basename "$0").log"
debug() {
    echo "$(date '+%H:%M:%S.%N') [DEBUG] $*" >> "$DEBUG_FILE"
}

# Debug-Datei initialisieren
echo "=== Debug-Log gestartet $(date) ===" > "$DEBUG_FILE"

# Verwendung
debug "Skript gestartet mit Parametern: $*"
debug "Aktueller Wert von \$PATH: $PATH"

# In einer Schleife
for file in *.txt; do
    debug "Verarbeite Datei: $file"
    # Operationen an der Datei
    debug "Datei $file verarbeitet, Ergebnis: $?"
done

12.3.5 Remote-Debugging über SSH

Bei automatisierten Skripten auf entfernten Servern kann Debugging über SSH durchgeführt werden:

#!/bin/bash
set -euo pipefail

# Debug-Modus aktivieren, wenn DEBUG=1 gesetzt ist
if [ "${DEBUG:-0}" = "1" ]; then
    # Debug-Ausgabe an einen speziellen Socket senden
    exec 2> >(nc -U /tmp/debug_socket)
fi

# Rest des Skripts...

Auf einem anderen Terminal kann dann die Debug-Ausgabe überwacht werden:

socat UNIX-LISTEN:/tmp/debug_socket,fork STDOUT

12.3.6 Umgebungsvariablen für Debugging

Verschiedene Umgebungsvariablen beeinflussen das Verhalten von Bash und können für Debugging-Zwecke genutzt werden:

# Ausführlichere Fehlermeldungen für Befehlsexpansionen
export BASH_XTRACEFD=1

# Erweiterte Debugging-Informationen für die Variablensubstitution
export BASH_XTRACE=1

12.3.7 Profiling von Shell-Skripten

Für Performance-Optimierungen kann die Ausführungszeit verschiedener Teile eines Skripts gemessen werden:

#!/bin/bash
set -euo pipefail

start_time=$(date +%s.%N)

time_checkpoint() {
    local checkpoint_name=$1
    local current_time=$(date +%s.%N)
    local elapsed=$(echo "$current_time - $start_time" | bc)
    echo "TIMING: $checkpoint_name - $elapsed Sekunden" >&2
}

# Verwendung
time_checkpoint "Start"
sleep 1
time_checkpoint "Nach Sleep 1"
for i in {1..100}; do
    : # Noop
done
time_checkpoint "Nach Schleife"

Für detaillierteres Profiling kann das externe Tool bash-xtraceprofile verwendet werden:

# Bash-Skript mit Profiling ausführen
bash -x mein_skript.sh 2> xtrace.log

# Profiling-Informationen auswerten
bash-xtraceprofile < xtrace.log

12.3.8 Debugging von Variablen und Arrays

Bei komplexen Datenstrukturen ist das gezielte Debugging von Variablen wichtig:

#!/bin/bash
set -euo pipefail

# Funktion zum Anzeigen aller Variablen mit einem bestimmten Präfix
dump_vars() {
    local prefix=$1
    eval "$(declare -p | grep "^declare .* $prefix" | sed 's/^declare/echo/')"
}

# Funktion zum Anzeigen eines Arrays
dump_array() {
    local arr_name=$1
    local arr_ref="$arr_name[@]"
    local i=0
    
    echo "Array $arr_name:"
    for element in "${!arr_ref}"; do
        echo "  [$i] = '$element'"
        ((i++))
    done
}

# Beispiel-Verwendung
CONFIG_HOST="example.com"
CONFIG_PORT=8080
CONFIG_TIMEOUT=30

dump_vars "CONFIG_"

# Arrays debuggen
FILES=("file1.txt" "file2.txt" "file with spaces.txt")
dump_array "FILES"

12.3.9 Debugging von Prozessen und Signalen

Bei Skripten, die mehrere Prozesse starten oder mit Signalen arbeiten, ist die Nachverfolgung dieser Aspekte wichtig:

#!/bin/bash
set -euo pipefail

# PID des aktuellen Skripts speichern
echo "PID des Hauptskripts: $$" >&2

# Subshell mit eigener PID starten
(
    echo "PID der Subshell: $$" >&2
    sleep 10 &
    SLEEP_PID=$!
    echo "PID des Sleep-Prozesses: $SLEEP_PID" >&2
    
    # Prozesshierarchie anzeigen
    ps f -o pid,ppid,cmd --forest -p $$ $SLEEP_PID
) &

SUBSHELL_PID=$!
echo "PID der gestarteten Subshell: $SUBSHELL_PID" >&2

# Warten, damit die Ausgabe sichtbar ist
sleep 2

12.3.10 Debugging mit set-Optionen

Neben set -x und set -v bietet Bash weitere nützliche Debug-Optionen:

#!/bin/bash
set -euo pipefail

# Debugging-Optionen aktivieren
set -o errtrace  # ERR-Trap auch in Funktionen auslösen
set -o functrace # DEBUG und RETURN-Traps auch in Funktionen auslösen

# DEBUG-Trap für schrittweises Debugging
trap 'echo "DEBUG: $BASH_COMMAND (Zeile $LINENO)"' DEBUG

# Funktion definieren
function test_func() {
    echo "In test_func"
    return 0
}

# Hauptcode
echo "Hauptcode beginnt"
test_func
echo "Hauptcode endet"

# DEBUG-Trap wieder deaktivieren
trap - DEBUG

12.4 Fallstricke und häufige Fehler im Shell-Scripting

Selbst erfahrene Entwickler stoßen beim Shell-Scripting immer wieder auf typische Probleme und Fallstricke. Die Kenntnis dieser häufigen Fehler und ihrer Vermeidungsstrategien ist entscheidend für die Entwicklung robuster Skripte. Dieser Abschnitt beleuchtet die wichtigsten Problemfelder und zeigt Best Practices zu ihrer Umgehung.

12.4.1 Unzureichende Anführungszeichen

Fehlende oder falsch platzierte Anführungszeichen sind eine der häufigsten Fehlerquellen in Shell-Skripten:

# Problematisch: Fehlende Anführungszeichen
FILE_PATH=/path/with spaces/file.txt
cd $FILE_PATH   # Wird als mehrere Argumente interpretiert

# Korrekt: Variablen in doppelte Anführungszeichen setzen
cd "$FILE_PATH"

Besonders bei Dateinamen mit Leerzeichen, Sonderzeichen oder Platzhaltern ist die korrekte Anführung essentiell:

# Problematisch: Dateien mit Leerzeichen werden falsch behandelt
for file in *.txt; do
    cp $file /backup/
done

# Korrekt: Variablen in Anführungszeichen setzen
for file in *.txt; do
    cp "$file" /backup/
done

Auch bei der Verkettung von Variablen sind Anführungszeichen wichtig:

# Problematisch: Unkontrollierte Expansion
OPTS="-l -a"
ls $OPTS /tmp   # Wird als separate Argumente behandelt

# Korrekt: Erhalt der Argumentstruktur
ls "$OPTS" /tmp  # Falsch: übergibt "-l -a" als einzelnes Argument
ls $OPTS /tmp    # Korrekt in diesem speziellen Fall

# Bei Arrays korrekt:
OPTS=(-l -a)
ls "${OPTS[@]}" /tmp  # Übergibt die Array-Elemente als separate Argumente

12.4.2 Unsachgemäße Variablenexpansion

Die Art der Variablenexpansion hat erhebliche Auswirkungen auf die Sicherheit und Robustheit von Skripten:

# Problematisch: Ungeschützte Variable
rm -rf $DIRECTORY/temp

# Korrekt: Schützen gegen leere Variablen
rm -rf "${DIRECTORY:?}/temp"  # Bricht ab, wenn DIRECTORY leer ist

# Problematisch: Ungeprüfte Standardwerte
echo ${LOGFILE:-/var/log/app.log}

# Korrekt: Prüfen, ob der Standardwert gültig ist
LOGFILE=${LOGFILE:-/var/log/app.log}
if [ ! -d "$(dirname "$LOGFILE")" ]; then
    echo "Fehler: Verzeichnis für Log-Datei existiert nicht" >&2
    exit 1
fi

Besondere Vorsicht ist bei der Expansion von Variablen in arithmetischen Kontexten geboten:

# Problematisch: Ungültiger Zahlenwert führt zu Fehlern
COUNT="nicht-numerisch"
RESULT=$((COUNT + 1))  # Führt zu einem Fehler

# Korrekt: Validierung vor Verwendung
if [[ "$COUNT" =~ ^[0-9]+$ ]]; then
    RESULT=$((COUNT + 1))
else
    echo "Fehler: COUNT muss numerisch sein" >&2
    exit 1
fi

12.4.3 Fehlerhafte Bedingungsprüfungen

Die verschiedenen Test-Konstrukte in Bash haben unterschiedliche Semantiken und Fallstricke:

# Problematisch: [ ] vs [[ ]] Unterschiede nicht beachtet
if [ $VAR == "test" ]; then  # Kann zu Syntaxfehlern führen, wenn VAR leer ist
    echo "Übereinstimmung gefunden"
fi

# Korrekt: [[ ]] für erweiterte Tests und bessere Fehlertoleranz verwenden
if [[ $VAR == "test" ]]; then  # Funktioniert auch, wenn VAR leer ist
    echo "Übereinstimmung gefunden"
fi

Auch die Überprüfung auf Dateiexistenz hat typische Fallstricke:

# Problematisch: Keine Prüfung auf Existenz vor Verwendung
cat $CONFIG_FILE

# Korrekt: Existenz und Typ prüfen
if [ ! -f "$CONFIG_FILE" ]; then
    echo "Fehler: Konfigurationsdatei '$CONFIG_FILE' nicht gefunden" >&2
    exit 1
fi
cat "$CONFIG_FILE"

12.4.4 Unzureichende Fehlerbehandlung

Viele Shell-Skripte scheitern aufgrund mangelhafter Fehlerbehandlung:

# Problematisch: Keine Fehlerprüfung nach kritischen Operationen
cp wichtige_datei.txt /backup/
rm wichtige_datei.txt       # Löscht Original ohne zu prüfen, ob Backup erstellt wurde

# Korrekt: Exit-Code prüfen und entsprechend reagieren
if cp wichtige_datei.txt /backup/; then
    rm wichtige_datei.txt
else
    echo "Fehler: Backup fehlgeschlagen, Original bleibt erhalten" >&2
    exit 1
fi

Die Aktivierung des strikten Modus kann viele dieser Probleme verhindern:

#!/bin/bash
set -euo pipefail  # Strikte Fehlerbehandlung aktivieren

# Dieser Befehl würde das Skript bei Fehler beenden
not_existing_command  # Wird mit Exit-Code 127 abbrechen

# Dieser Code wird nie erreicht
echo "Fertig"

12.4.5 Probleme mit Pfaden und Arbeitsverzeichnissen

Relative Pfade und die Annahme eines bestimmten Arbeitsverzeichnisses sind häufige Fehlerquellen:

# Problematisch: Annahme über das aktuelle Arbeitsverzeichnis
cd /tmp
./setup.sh        # Fehler, wenn setup.sh nicht in /tmp liegt

# Korrekt: Absoluter Pfad zum Skript bestimmen und verwenden
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
cd /tmp
"$SCRIPT_DIR/setup.sh"

Bei Skripten, die das Arbeitsverzeichnis ändern, sollte dies explizit dokumentiert und behandelt werden:

# Problematisch: Unerwartete Auswirkungen auf nachfolgende Befehle
cd /tmp
rm *.log  # Löscht Log-Dateien im falschen Verzeichnis, wenn cd fehlschlägt

# Korrekt: In Subshell arbeiten, um das Arbeitsverzeichnis zu isolieren
(
    cd /tmp || { echo "Fehler: Konnte nicht nach /tmp wechseln" >&2; exit 1; }
    rm *.log
)
# Arbeitsverzeichnis bleibt unverändert, unabhängig vom Erfolg der Befehle in der Subshell

12.4.6 Unbeabsichtigte Globbing-Expansion

Die automatische Expansion von Platzhaltern (Globbing) kann zu unerwarteten Ergebnissen führen:

# Problematisch: Unbeabsichtigte Expansion
rm -rf /pfad/zu/verzeichnis/*    # Könnte bei fehlerhaftem Pfad gefährlich sein

# Korrekt: Prüfen, ob Expansion erfolgt
shopt -s nullglob  # Leere Liste, wenn keine Übereinstimmung
FILES=(/pfad/zu/verzeichnis/*)
if [ ${#FILES[@]} -gt 0 ]; then
    rm -rf "${FILES[@]}"
else
    echo "Warnung: Keine Dateien gefunden" >&2
fi

Besondere Vorsicht ist bei der Verarbeitung von Benutzereingaben geboten:

# Problematisch: Benutzereingabe wird als Globbing-Muster interpretiert
echo "Welche Datei soll gelöscht werden?"
read filename
rm $filename  # Gefährlich, wenn filename Platzhalter enthält

# Korrekt: Benutzereingabe als literalen String behandeln
rm "$filename"

12.4.7 Probleme mit Umgebungsvariablen und Subshells

Die Vererbung von Variablen zwischen Haupt-Shell und Subshells führt oft zu subtilen Fehlern:

# Problematisch: Variable in Subshell geändert, aber nicht im Hauptskript verfügbar
result=$(cd /tmp && VAR="neuer Wert" && echo "OK")
echo $VAR  # Gibt nichts aus, da VAR nur in der Subshell existiert

# Korrekt: Wert aus der Subshell explizit zurückgeben
VAR=$(cd /tmp && echo "neuer Wert")
echo $VAR  # Gibt "neuer Wert" aus

Bei der Verwendung von Pipes ist zu beachten, dass jeder Teil in einer separaten Subshell ausgeführt wird:

# Problematisch: Variablenänderungen in einer Pipeline gehen verloren
echo "Zeile 1
Zeile 2" | while read line; do
    count=$((count + 1))
done
echo "Anzahl Zeilen: $count"  # count ist außerhalb der while-Schleife nicht verändert

# Korrekt: Process Substitution verwenden, um dieses Problem zu umgehen
count=0
while read line; do
    count=$((count + 1))
done < <(echo "Zeile 1
Zeile 2")
echo "Anzahl Zeilen: $count"  # Gibt "Anzahl Zeilen: 2" aus

12.4.8 Race Conditions und Concurrency-Probleme

Shell-Skripte, die parallel Prozesse ausführen, können zu schwer diagnostizierbaren Race Conditions führen:

# Problematisch: Mehrere Prozesse schreiben in dieselbe Datei
for i in {1..10}; do
    (echo "Prozess $i schreibt" >> output.log) &
done
wait

# Korrekt: Synchronisation mit Datei-Locks
for i in {1..10}; do
    (
        # flock sorgt für exklusiven Zugriff
        flock -x 200
        echo "Prozess $i schreibt" >> output.log
    ) 200>/tmp/lock_file &
done
wait

Bei der Verwendung temporärer Dateien ist die Einzigartigkeit des Dateinamens sicherzustellen:

# Problematisch: Vorhersehbarer temporärer Dateiname
TEMP_FILE="/tmp/myapp_temp.txt"
echo "Daten" > "$TEMP_FILE"  # Kollision, wenn mehrere Instanzen laufen

# Korrekt: Sicheres Erstellen temporärer Dateien mit mktemp
TEMP_FILE=$(mktemp)
echo "Daten" > "$TEMP_FILE"
# Aufräumen nicht vergessen
trap 'rm -f "$TEMP_FILE"' EXIT

12.4.9 Probleme bei der Verarbeitung von Text und Daten

Die Textverarbeitung in Shell-Skripten ist anfällig für Fehler, insbesondere bei Sonderzeichen:

# Problematisch: Fehlinterpretation von Daten mit Sonderzeichen
DATA="Zeile mit
Zeilenumbruch"
echo $DATA  # Gibt "Zeile mit Zeilenumbruch" ohne Zeilenumbruch aus

# Korrekt: Erhalt von Zeilenumbrüchen und Struktur
echo "$DATA"  # Gibt die Daten mit Zeilenumbruch aus

Bei der Verarbeitung von CSV-Dateien oder anderen strukturierten Daten sind spezialisierte Tools vorzuziehen:

# Problematisch: Naives Parsen von CSV mit Bash
while IFS=, read -r name alter stadt; do
    echo "Name: $name, Alter: $alter"
done < daten.csv  # Versagt bei Kommas innerhalb von Feldern oder Zeilenumbrüchen

# Besser: Spezialisierte Tools verwenden
awk -F, '{print "Name: " $1 ", Alter: " $2}' daten.csv  # Für einfache Fälle
# Für komplexere Fälle: Python, Perl oder andere Sprachen mit CSV-Parsern

12.4.10 Ungenaue Zeiterfassung und -berechnung

Die Zeitmessung und -berechnung in Bash kann ungenau sein:

# Problematisch: Ungenauigkeit bei der Zeitmessung
start_time=$(date +%s)
sleep 0.1  # Befehl mit kurzer Laufzeit
end_time=$(date +%s)
duration=$((end_time - start_time))  # Könnte 0 sein bei kurzen Operationen

# Besser: Genauere Zeitmessung mit Nanosekunden
start_time=$(date +%s.%N)
sleep 0.1
end_time=$(date +%s.%N)
duration=$(echo "$end_time - $start_time" | bc)  # Erfordert bc für Fließkommaberechnung

Bei der Berechnung von Datumsintervallen oder der Manipulation von Zeitstempeln stoßen Shell-Skripte schnell an ihre Grenzen:

# Problematisch: Einfache Datumsberechnungen
today=$(date +%Y-%m-%d)
# Wie addiert man 30 Tage in reiner Bash?

# Besser: date-Befehl mit spezifischen Optionen oder externe Tools nutzen
today=$(date +%Y-%m-%d)
in_30_days=$(date -d "$today + 30 days" +%Y-%m-%d)  # GNU date Syntax
# Oder mit BSD/macOS:
# in_30_days=$(date -v+30d +%Y-%m-%d)

12.4.11 Überraschende Zeichenkettenvergleiche

Die Art des Zeichenkettenvergleichs kann zu unerwarteten Ergebnissen führen:

# Problematisch: Unerwartete Sortierreihenfolge
if [ "10" > "2" ]; then
    echo "10 ist größer als 2"
else
    echo "10 ist NICHT größer als 2"
fi
# Gibt "10 ist NICHT größer als 2" aus, da es sich um lexikographischen Vergleich handelt

# Korrekt für numerischen Vergleich:
if [ 10 -gt 2 ]; then
    echo "10 ist größer als 2"
fi

Bei Vergleichen mit regulären Ausdrücken ist die Syntax besonders wichtig:

# Problematisch: Falsche Syntax für reguläre Ausdrücke
if [ "$string" =~ ^[0-9]+$ ]; then  # Syntax-Fehler in [ ]
    echo "Numerischer String"
fi

# Korrekt: Doppelte eckige Klammern für reguläre Ausdrücke
if [[ "$string" =~ ^[0-9]+$ ]]; then
    echo "Numerischer String"
fi

12.4.12 Falsche Annahmen über externe Befehle

Die Verfügbarkeit und das Verhalten externer Befehle können zwischen Systemen variieren:

# Problematisch: Annahme über die Verfügbarkeit eines Befehls
sort -V versionsliste.txt  # -V Option ist nicht auf allen Systemen verfügbar

# Korrekt: Verfügbarkeit prüfen oder Alternative bereitstellen
if sort --version 2>&1 | grep -q "GNU coreutils"; then
    # GNU sort mit -V Option ist verfügbar
    sort -V versionsliste.txt
else
    # Alternative Implementierung
    cat versionsliste.txt | awk '{ ... }'
fi

Auch die Interpretation von Befehlsoptionen kann variieren:

# Problematisch: Unterschiedliche Interpretation von Optionen
echo "Text" | grep -q "Text"  # -q ist auf einigen älteren Systemen nicht verfügbar

# Korrekt: Kompatiblere Alternative verwenden
if echo "Text" | grep "Text" > /dev/null 2>&1; then
    echo "Gefunden"
fi

12.5 Fehlerinformationen interpretieren

Das Verständnis und die korrekte Interpretation von Fehlermeldungen ist ein entscheidender Aspekt des Debugging-Prozesses. Shell-Skripte können verschiedene Arten von Fehlern erzeugen, deren Meldungen oft kryptisch erscheinen. Dieser Abschnitt vermittelt die notwendigen Kenntnisse, um Fehlermeldungen effektiv zu analysieren und die zugrundeliegenden Probleme zu identifizieren.

12.5.1 Grundlegende Fehlertypen in Shell-Skripten

Shell-Fehler lassen sich in mehrere Hauptkategorien einteilen:

  1. Syntaxfehler: Fehler in der Skriptstruktur oder Befehlssyntax
  2. Laufzeitfehler: Fehler, die während der Ausführung auftreten
  3. Logische Fehler: Skript läuft ohne Fehlermeldung, liefert aber falsche Ergebnisse
  4. Umgebungsfehler: Probleme mit externen Ressourcen oder Systemkonfigurationen

12.5.2 Interpretation von Syntax-Fehlermeldungen

Syntaxfehler werden meist beim Parsen des Skripts erkannt und mit einer spezifischen Zeilennummer gemeldet:

$ bash fehler.sh
fehler.sh: line 7: syntax error near unexpected token `)'
fehler.sh: line 7: `if (( $count > 10 ); then'

Diese Fehlermeldung deutet auf ein Problem mit der Syntax der arithmetischen Auswertung hin. Der Fehler liegt in der zusätzlichen Verwendung des Semikolons innerhalb der (( ))-Konstruktion. Die korrekte Schreibweise wäre:

if (( $count > 10 )); then

Typische Syntaxfehler umfassen:

  1. Fehlende oder falsch platzierte Klammern:

    fehler.sh: line 5: unexpected EOF while looking for matching `"'
  2. Unvollständige Bedingungsausdrücke:

    fehler.sh: line 12: [: missing `]'
  3. Falsche Befehlsverkettung:

    fehler.sh: line 8: syntax error near unexpected token `||'

12.5.3 Analyse von Exit-Codes

Exit-Codes sind numerische Werte, die nach der Ausführung eines Befehls zurückgegeben werden und über den Erfolg oder Misserfolg informieren:

$ command_that_fails
$ echo $?
1

Die wichtigsten Exit-Codes und ihre Bedeutung:

Exit-Code Bedeutung
0 Erfolgreiche Ausführung
1 Allgemeiner Fehler oder unspezifischer Fehler
2 Fehlerhafte Verwendung von Shell-Builtin-Befehlen
126 Befehl kann nicht ausgeführt werden (Berechtigungsproblem)
127 Befehl nicht gefunden
128 + n Fatales Signal n empfangen (z.B. 130 = SIGINT, 143 = SIGTERM)

Eine systematische Analyse der Exit-Codes erfordert deren gezielte Erfassung:

#!/bin/bash
set -euo pipefail

analyze_exit_code() {
    local exit_code=$1
    local command=$2
    
    case $exit_code in
        0)   echo "Befehl '$command' erfolgreich ausgeführt" ;;
        1)   echo "Befehl '$command' fehlgeschlagen: Allgemeiner Fehler" ;;
        2)   echo "Befehl '$command' fehlgeschlagen: Falsche Verwendung" ;;
        126) echo "Befehl '$command' fehlgeschlagen: Keine Ausführungsberechtigung" ;;
        127) echo "Befehl '$command' fehlgeschlagen: Befehl nicht gefunden" ;;
        *)   
            if [ $exit_code -gt 128 ] && [ $exit_code -lt 165 ]; then
                sig=$((exit_code - 128))
                echo "Befehl '$command' fehlgeschlagen: Signal $sig empfangen"
            else
                echo "Befehl '$command' fehlgeschlagen: Unbekannter Exit-Code $exit_code"
            fi
            ;;
    esac
}

# Verwendung
command_to_test="ls /nicht/existierender/pfad"
$command_to_test || analyze_exit_code $? "$command_to_test"

12.5.4 Verstehen von Standardfehlerausgaben

Die Standardfehlerausgabe (stderr) enthält wertvolle Informationen zur Diagnose von Problemen:

$ ls /nicht/existierender/pfad
ls: cannot access '/nicht/existierender/pfad': No such file or directory

Bei komplexen Befehlen oder Skripten kann die Fehlerausgabe umfangreich sein. Durch gezieltes Umleiten und Filtern lassen sich relevante Informationen extrahieren:

#!/bin/bash
set -euo pipefail

# Fehlerausgabe in Variable speichern
error_output=$(ls /nicht/existierender/pfad 2>&1)

# Fehlerausgabe analysieren
if echo "$error_output" | grep -q "No such file or directory"; then
    echo "DIAGNOSE: Das angegebene Verzeichnis existiert nicht."
    echo "LÖSUNG: Überprüfen Sie den Pfad oder erstellen Sie das Verzeichnis."
elif echo "$error_output" | grep -q "Permission denied"; then
    echo "DIAGNOSE: Keine ausreichenden Berechtigungen für das Verzeichnis."
    echo "LÖSUNG: Überprüfen Sie die Zugriffsrechte mit 'ls -la'."
else
    echo "UNBEKANNTER FEHLER: $error_output"
fi

12.5.5 Interpretation von Laufzeitfehlern

Laufzeitfehler treten während der Ausführung auf und werden oft durch unerwartete Bedingungen verursacht:

$ ./script.sh
./script.sh: line 15: cd: /tmp/nicht_vorhanden: No such file or directory
./script.sh: line 16: /tmp/nicht_vorhanden/config.txt: No such file or directory

Diese Fehlermeldung zeigt ein typisches Muster: Ein Befehl schlägt fehl (hier cd), aber das Skript wird fortgesetzt und versucht, mit der fehlgeschlagenen Operation zu arbeiten, was zu Folgeproblemen führt.

Häufige Muster bei Laufzeitfehlern:

  1. Fehlende Ressourcen:

    ./script.sh: line 8: /usr/bin/spezial_tool: No such file or directory
  2. Unzureichende Berechtigungen:

    ./script.sh: line 12: /etc/secure_file: Permission denied
  3. Fehlende/unpassende Argumente:

    ./script.sh: line 17: [: too many arguments

12.5.6 Spezielle Bash-Fehlermeldungen analysieren

Die Bash erzeugt spezifische Fehlermeldungen, die Hinweise auf das zugrundeliegende Problem geben:

$ echo ${UNDEFINED_VAR:?}
bash: UNDEFINED_VAR: parameter null or not set

Diese Meldung zeigt, dass die Variable UNDEFINED_VAR nicht definiert ist, was durch den :?-Operator explizit geprüft wird.

Weitere typische Bash-Fehlermeldungen:

  1. Befehlsersetzungsfehler:

    $ echo $(nicht_existierender_befehl)
    bash: nicht_existierender_befehl: command not found
  2. Expansion von Arrays:

    $ echo ${array[nicht_numerischer_index]}
    bash: array: bad array subscript
  3. Mathematische Fehler:

    $ echo $((10 / 0))
    bash: division by 0 (error token is "0")

12.5.7 Dekodieren von kryptischen Fehlermeldungen

Einige Fehlermeldungen erscheinen zunächst kryptisch, enthalten aber wertvolle Informationen:

$ ./script.sh
./script.sh: line 23: fg: no job control

Diese Meldung zeigt, dass das Skript versucht, den fg-Befehl zu verwenden, der Jobkontrolle erfordert – was in einem Skript standardmäßig nicht aktiviert ist.

Ein weiteres Beispiel:

$ ./script.sh
./script.sh: line 42: warning: here-document at line 38 delimited by end-of-file (wanted `EOF')

Diese Meldung weist auf ein unvollständiges Here-Document hin, bei dem der Abschlussbezeichner fehlt oder nicht korrekt positioniert ist.

12.5.8 Identifizieren und Verstehen von Umgebungsfehlern

Umgebungsfehler treten auf, wenn das Skript mit der Systemumgebung interagiert und sind oft schwieriger zu diagnostizieren:

#!/bin/bash

# Beispiel zur Analyse von Umgebungsfehlern
analyze_environment_error() {
    local command=$1
    local error_output=$2
    
    # Typische Umgebungsfehler erkennen
    if echo "$error_output" | grep -q "No space left on device"; then
        echo "UMGEBUNGSFEHLER: Kein Speicherplatz mehr verfügbar."
        echo "DIAGNOSE: Überprüfen Sie freien Speicherplatz mit 'df -h'."
    elif echo "$error_output" | grep -q "Too many open files"; then
        echo "UMGEBUNGSFEHLER: Zu viele offene Dateien."
        echo "DIAGNOSE: Überprüfen Sie die Datei-Limits mit 'ulimit -a'."
    elif echo "$error_output" | grep -q "Cannot allocate memory"; then
        echo "UMGEBUNGSFEHLER: Nicht genügend Arbeitsspeicher."
        echo "DIAGNOSE: Überprüfen Sie den Speicherverbrauch mit 'free -m'."
    else
        echo "UNBEKANNTER UMGEBUNGSFEHLER: $error_output"
    fi
}

# Verwendung
command_to_test="tar -czf /backup.tar.gz /home"
error_output=$(eval "$command_to_test" 2>&1) || analyze_environment_error "$command_to_test" "$error_output"

12.5.9 Fehler durch fehlerhafte Zeichencodierung

Probleme mit Zeichencodierungen führen oft zu schwer nachvollziehbaren Fehlern, besonders bei der Verarbeitung internationaler Texte:

$ ./script.sh input_file_with_utf8.txt
./script.sh: line 27: $'\303\274': command not found

Diese Fehlermeldung kann auftreten, wenn ein UTF-8-kodierter Umlaut (hier ‘ü’) als Befehl interpretiert wird. Solche Probleme können durch gezielte Prüfung der Eingabedaten erkannt werden:

#!/bin/bash
set -euo pipefail

# Prüfen auf nicht-ASCII-Zeichen in einer Datei
check_encoding() {
    local file=$1
    if grep -P "[\x80-\xFF]" "$file" > /dev/null; then
        echo "WARNUNG: Datei enthält nicht-ASCII-Zeichen, die Probleme verursachen könnten."
        echo "         Überprüfen Sie die Zeichencodierung oder konvertieren Sie mit iconv."
    fi
}

# Beispiel
input_file="input.txt"
check_encoding "$input_file"

12.5.10 Interpretation von Fehlern in komplexen Pipelines

Fehler in Pipelines sind besonders tückisch, da der Exit-Code standardmäßig nur vom letzten Befehl zurückgegeben wird:

#!/bin/bash
set -euo pipefail  # pipefail aktivieren, um Fehler in Pipelines zu erkennen

# Beispiel für eine Pipeline mit Fehler
grep "muster" nicht_existierende_datei.txt | sort | uniq > ergebnis.txt

# Ohne pipefail würde das Skript hier weiterlaufen, da sort und uniq erfolgreich waren
# Mit pipefail wird der Fehler von grep erkannt und das Skript beendet

Zur detaillierten Analyse komplexer Pipelines kann jeder Schritt einzeln überprüft werden:

#!/bin/bash
set -euo pipefail

debug_pipeline() {
    echo "Debugging Pipeline: $*"
    
    # Temporäre Dateien für Zwischenergebnisse erstellen
    local temp_dir=$(mktemp -d)
    trap 'rm -rf "$temp_dir"' EXIT
    
    # Pipeline in einzelne Befehle zerlegen und schrittweise ausführen
    local commands=("$@")
    local input="/dev/null"
    local output=""
    
    for ((i=0; i<${#commands[@]}; i++)); do
        output="$temp_dir/step_$i.out"
        echo "Schritt $i: ${commands[$i]}"
        
        # Befehl ausführen und Exit-Code prüfen
        if ! eval "${commands[$i]} < $input > $output 2> $temp_dir/step_$i.err"; then
            echo "FEHLER bei Schritt $i: ${commands[$i]}"
            echo "Exit-Code: $?"
            echo "Fehlerausgabe:"
            cat "$temp_dir/step_$i.err"
            return 1
        fi
        
        echo "Zwischenergebnis (erste 5 Zeilen):"
        head -n 5 "$output"
        echo "---"
        
        input="$output"
    done
    
    echo "Pipeline erfolgreich abgeschlossen."
}

# Beispiel
debug_pipeline "grep 'Muster' datei.txt" "sort" "uniq"

12.5.11 Häufige Exit-Codes externer Programme verstehen

Viele Standard-Unix-Tools folgen Konventionen bezüglich ihrer Exit-Codes:

Programm Exit-Code Bedeutung
grep 0 Muster gefunden
1 Kein Muster gefunden
2 Fehler (z.B. Datei nicht gefunden)
ssh 0 Erfolgreiche Verbindung/Befehl
255 Verbindungsfehler (z.B. Host nicht erreichbar)
curl 0 Erfolgreicher Download
22 HTTP-Fehler (z.B. 404 Not Found)
28 Timeout

Ein Beispiel zur systematischen Interpretation von grep-Exit-Codes:

#!/bin/bash
set -euo pipefail

interpret_grep_result() {
    local pattern=$1
    local file=$2
    local exit_code=$3
    
    case $exit_code in
        0)  echo "Muster '$pattern' wurde in '$file' gefunden." ;;
        1)  echo "Muster '$pattern' wurde in '$file' NICHT gefunden." ;;
        2)  echo "FEHLER: Beim Durchsuchen von '$file' nach Muster '$pattern'."
            echo "        Überprüfen Sie, ob die Datei existiert und lesbar ist."
            ;;
        *)  echo "UNBEKANNTER FEHLER: Exit-Code $exit_code bei grep." ;;
    esac
}

# Anwendung
pattern="wichtiger_eintrag"
file="/var/log/system.log"

grep "$pattern" "$file" > /dev/null || interpret_grep_result "$pattern" "$file" $?

12.6 Testen von Shell-Skripten

Das systematische Testen von Shell-Skripten wird oft vernachlässigt, ist jedoch entscheidend für die Zuverlässigkeit und Wartbarkeit. In diesem Abschnitt werden verschiedene Testmethoden und -frameworks vorgestellt, die dazu beitragen, robuste und fehlerfreie Shell-Skripte zu entwickeln.

12.6.1 Grundlegende Testprinzipien für Shell-Skripte

Auch für Shell-Skripte gelten die allgemeinen Prinzipien des Softwaretestens:

  1. Automatisierbarkeit: Tests sollten ohne manuelle Eingriffe ausführbar sein
  2. Wiederholbarkeit: Gleiche Eingaben sollten stets zu gleichen Ergebnissen führen
  3. Isolation: Tests sollten voneinander unabhängig sein
  4. Selbstüberprüfung: Tests sollten selbständig feststellen, ob sie bestanden wurden
  5. Determinismus: Tests sollten immer gleich ablaufen und keine zufälligen Elemente enthalten

12.6.2 Manuelle Testmethoden

Für einfache Skripte können manuelle Testmethoden ausreichen:

#!/bin/bash
# test_runner.sh - Einfacher manueller Test-Runner

set -euo pipefail

# Zu testendes Skript
SCRIPT="./mein_skript.sh"

# Testfälle definieren
echo "=== Testfall 1: Normaler Betrieb ==="
$SCRIPT parameter1 parameter2
echo "Exit-Code: $?"

echo "=== Testfall 2: Fehlender Parameter ==="
$SCRIPT parameter1
echo "Exit-Code: $?"

echo "=== Testfall 3: Ungültiger Parameter ==="
$SCRIPT --ungueltige-option
echo "Exit-Code: $?"

Solche manuellen Tests sind jedoch wenig strukturiert und schwer zu validieren. Für systematischere Tests eignen sich dedizierte Testframeworks.

12.6.3 Einführung in BATS (Bash Automated Testing System)

BATS ist eines der bekanntesten Testframeworks für Bash und ermöglicht die Erstellung strukturierter automatisierter Tests:

#!/usr/bin/env bats
# test_mein_skript.bats

setup() {
    # Wird vor jedem Test ausgeführt
    export TEMP_DIR=$(mktemp -d)
    export PATH="$BATS_TEST_DIRNAME:$PATH"
}

teardown() {
    # Wird nach jedem Test ausgeführt
    rm -rf "$TEMP_DIR"
}

@test "Skript sollte bei korrekten Parametern 0 zurückgeben" {
    run mein_skript.sh parameter1 parameter2
    
    # Überprüfungen
    [ "$status" -eq 0 ]
    [ "${lines[0]}" = "Erwartete Ausgabe" ]
}

@test "Skript sollte bei fehlenden Parametern 1 zurückgeben" {
    run mein_skript.sh
    
    [ "$status" -eq 1 ]
    [[ "${output}" =~ "Fehler: Parameter fehlen" ]]
}

Installation und Ausführung von BATS:

# Installation (auf Debian/Ubuntu)
apt-get install bats

# Alternative: Installation aus GitHub
git clone https://github.com/bats-core/bats-core.git
cd bats-core
./install.sh /usr/local

# Tests ausführen
bats test_mein_skript.bats

12.6.4 Erstellung aussagekräftiger BATS-Tests

Strukturierte Tests mit BATS sollten verschiedene Aspekte des Skripts abdecken:

#!/usr/bin/env bats

load "test_helper"  # Lädt gemeinsame Hilfsfunktionen

# Funktionstest
@test "add_numbers sollte Zahlen korrekt addieren" {
    # Source das zu testende Skript, wenn es Funktionen enthält
    source "$BATS_TEST_DIRNAME/../lib/math_functions.sh"
    
    result=$(add_numbers 5 7)
    [ "$result" -eq 12 ]
}

# Integrationstest
@test "process_file sollte CSV-Datei korrekt verarbeiten" {
    # Testdatei erstellen
    echo "Name,Alter,Stadt
Anna,28,Berlin
Boris,42,Hamburg" > "$TEMP_DIR/test.csv"
    
    run "$BATS_TEST_DIRNAME/../bin/process_file.sh" "$TEMP_DIR/test.csv"
    
    [ "$status" -eq 0 ]
    [ -f "$TEMP_DIR/test.csv.processed" ]
    [[ "$(cat "$TEMP_DIR/test.csv.processed")" =~ "2 Datensätze verarbeitet" ]]
}

# Fehlertest
@test "Skript sollte nicht-existierende Datei korrekt behandeln" {
    run "$BATS_TEST_DIRNAME/../bin/process_file.sh" "/nicht/existierend.csv"
    
    [ "$status" -eq 2 ]
    [[ "${output}" =~ "Fehler: Datei nicht gefunden" ]]
}

BATS bietet weitere nützliche Funktionen:

12.6.5 Erweiterte Testhelfer für BATS

Für komplexere Tests können spezielle Helfer verwendet werden:

#!/usr/bin/env bash
# test_helper.bash

# Temporäres Testverzeichnis erstellen
setup_test_environment() {
    export TEST_DIR=$(mktemp -d)
    export TEST_INPUT="$TEST_DIR/input"
    export TEST_OUTPUT="$TEST_DIR/output"
    
    mkdir -p "$TEST_INPUT" "$TEST_OUTPUT"
}

# Testdaten generieren
generate_test_data() {
    local size=$1
    local file="$TEST_INPUT/data.txt"
    
    for ((i=1; i<=size; i++)); do
        echo "Testzeile $i mit zufälligem Inhalt $RANDOM" >> "$file"
    done
}

# Ausgaben validieren
assert_output_contains() {
    local pattern=$1
    local message=${2:-"Ausgabe sollte '$pattern' enthalten"}
    
    [[ "$output" =~ $pattern ]] || {
        echo "Fehler: $message"
        echo "Tatsächliche Ausgabe:"
        echo "$output"
        return 1
    }
}

# Dateien validieren
assert_file_contains() {
    local file=$1
    local pattern=$2
    local message=${3:-"Datei '$file' sollte '$pattern' enthalten"}
    
    [[ -f "$file" ]] || {
        echo "Fehler: Datei '$file' existiert nicht"
        return 1
    }
    
    grep -q "$pattern" "$file" || {
        echo "Fehler: $message"
        echo "Dateiinhalt:"
        cat "$file"
        return 1
    }
}

Einbindung in BATS-Tests:

#!/usr/bin/env bats

load "test_helper"

setup() {
    setup_test_environment
}

teardown() {
    rm -rf "$TEST_DIR"
}

@test "Skript sollte große Dateien verarbeiten können" {
    generate_test_data 1000
    
    run "$BATS_TEST_DIRNAME/../bin/process_large_file.sh" "$TEST_INPUT/data.txt"
    
    [ "$status" -eq 0 ]
    assert_output_contains "1000 Zeilen verarbeitet"
    assert_file_contains "$TEST_OUTPUT/summary.txt" "Verarbeitung abgeschlossen"
}

12.6.6 ShellSpec: Eine alternative Testumgebung

ShellSpec ist ein BDD-ähnliches (Behavior-Driven Development) Testframework für Shell-Skripte:

# Installation
git clone https://github.com/shellspec/shellspec.git
cd shellspec
./install.sh

# Initialisierung eines Testprojekts
shellspec --init

Ein ShellSpec-Testbeispiel:

# spec/mein_skript_spec.sh
Describe 'mein_skript.sh'
  Include ./mein_skript.sh
  
  Describe 'parse_arguments()'
    It 'sollte Standardwerte setzen, wenn keine Argumente übergeben werden'
      When call parse_arguments
      The variable CONFIG_FILE should equal '/etc/default.conf'
      The variable VERBOSE should equal 'false'
    End
    
    It 'sollte -c Option korrekt verarbeiten'
      When call parse_arguments -c /custom/config.conf
      The variable CONFIG_FILE should equal '/custom/config.conf'
    End
  End
  
  Describe 'process_file()'
    setup() { touch "$SHELLSPEC_TMPDIR/test.txt"; }
    
    It 'sollte eine Erfolgsausgabe für existierende Dateien erzeugen'
      When call process_file "$SHELLSPEC_TMPDIR/test.txt"
      The output should include 'Verarbeitung erfolgreich'
      The status should be 0
    End
    
    It 'sollte bei nicht existierenden Dateien einen Fehler ausgeben'
      When call process_file "$SHELLSPEC_TMPDIR/nicht_existierend.txt"
      The stderr should include 'Fehler'
      The status should be 1
    End
  End
End

12.6.7 Mocken von externen Befehlen und Abhängigkeiten

Ein kritischer Aspekt beim Testen von Shell-Skripten ist das Mocken externer Befehle, um kontrollierte Testbedingungen zu schaffen:

#!/usr/bin/env bats

setup() {
    # Temporäres Verzeichnis für Mock-Befehle
    export MOCK_BIN_DIR=$(mktemp -d)
    export PATH="$MOCK_BIN_DIR:$PATH"
    
    # Mock für den 'curl' Befehl erstellen
    cat > "$MOCK_BIN_DIR/curl" << 'EOF'
#!/bin/bash
echo "HTTP/1.1 200 OK"
echo ""
echo "Mocked API Response"
EOF
    chmod +x "$MOCK_BIN_DIR/curl"
}

teardown() {
    rm -rf "$MOCK_BIN_DIR"
}

@test "api_request sollte Daten erfolgreich abrufen" {
    source "$BATS_TEST_DIRNAME/../lib/api_functions.sh"
    
    result=$(api_request "https://example.com/api")
    
    [[ "$result" =~ "Mocked API Response" ]]
}

Für komplexere Mock-Anforderungen ist ein strukturierterer Ansatz sinnvoll:

#!/bin/bash
# mock_helper.sh

# Mock-Verzeichnis einrichten
setup_mocks() {
    export ORIGINAL_PATH="$PATH"
    export MOCK_DIR=$(mktemp -d)
    export PATH="$MOCK_DIR:$PATH"
}

# Mock-Verzeichnis aufräumen
teardown_mocks() {
    export PATH="$ORIGINAL_PATH"
    rm -rf "$MOCK_DIR"
}

# Mock für einen Befehl erstellen
create_mock() {
    local command=$1
    local exit_code=${2:-0}
    local stdout=${3:-""}
    local stderr=${4:-""}
    
    cat > "$MOCK_DIR/$command" << EOF
#!/bin/bash
echo "$stdout"
echo "$stderr" >&2
exit $exit_code
EOF
    chmod +x "$MOCK_DIR/$command"
}

# Mock für einen Befehl, der Argumente prüft
create_mock_with_args() {
    local command=$1
    local args_pattern=$2
    local success_stdout=${3:-""}
    local success_exit=${4:-0}
    local failure_stderr=${5:-"Ungültige Argumente"}
    local failure_exit=${6:-1}
    
    cat > "$MOCK_DIR/$command" << EOF
#!/bin/bash
if [[ "\$*" =~ $args_pattern ]]; then
    echo "$success_stdout"
    exit $success_exit
else
    echo "$failure_stderr" >&2
    exit $failure_exit
fi
EOF
    chmod +x "$MOCK_DIR/$command"
}

Anwendung in BATS-Tests:

#!/usr/bin/env bats

load "mock_helper"

setup() {
    setup_mocks
}

teardown() {
    teardown_mocks
}

@test "backup-Skript sollte rsync mit korrekten Optionen aufrufen" {
    # Mock für rsync erstellen, der die Argumente überprüft
    create_mock_with_args "rsync" "-av --delete .* /backup" "Erfolgreiches Backup"
    
    run ./backup.sh /quelle /backup
    
    [ "$status" -eq 0 ]
    [[ "${output}" =~ "Backup abgeschlossen" ]]
}

12.6.8 Testabdeckung für Shell-Skripte

Das Messen der Testabdeckung für Shell-Skripte ist mit speziellen Tools möglich:

# Installation von kcov (auf Debian/Ubuntu)
apt-get install kcov

# Ausführen von Tests mit Abdeckungsmessung
kcov --include-pattern=.sh coverage_report bats test_mein_skript.bats

# Ergebnis als HTML-Report in coverage_report/

Die kcov-Ausgabe zeigt: - Welche Zeilen des Skripts ausgeführt wurden - Welche Zweige durchlaufen wurden - Eine Gesamtabdeckungsrate in Prozent

12.6.9 Eigenständige Shell-Skript-Unit-Tests

Für einfachere Umgebungen kann ein eigenständiger Ansatz für Unit-Tests entwickelt werden:

#!/bin/bash
# unit_test_framework.sh

set -euo pipefail

# Globale Testvariablen
TEST_COUNT=0
PASS_COUNT=0
FAIL_COUNT=0

# Farbcodes für die Ausgabe
GREEN='\033[0;32m'
RED='\033[0;31m'
NC='\033[0m' # No Color

# Testfunktion, die einen Ausdruck bewertet
assert() {
    local expression=$1
    local message=${2:-"Assertion fehlgeschlagen"}
    
    ((TEST_COUNT++))
    
    if eval "$expression"; then
        printf "${GREEN}PASS${NC}: $message\n"
        ((PASS_COUNT++))
    else
        printf "${RED}FAIL${NC}: $message\n"
        printf "      Expression: $expression\n"
        ((FAIL_COUNT++))
    fi
}

# Gleichheit prüfen
assert_equals() {
    local actual=$1
    local expected=$2
    local message=${3:-"Erwartete '$expected', erhalten '$actual'"}
    
    assert "[ \"$actual\" = \"$expected\" ]" "$message"
}

# Ausgabe testen
assert_output() {
    local command=$1
    local expected_pattern=$2
    local message=${3:-"Ausgabe sollte '$expected_pattern' enthalten"}
    
    local output
    output=$(eval "$command" 2>&1)
    
    assert "[[ \"$output\" =~ \"$expected_pattern\" ]]" "$message (Ausgabe: $output)"
}

# Testgruppe starten
describe() {
    echo "=== $1 ==="
}

# Zusammenfassung drucken
summarize() {
    echo "------------------------"
    echo "Tests: $TEST_COUNT, Bestanden: $PASS_COUNT, Fehlgeschlagen: $FAIL_COUNT"
    
    if [ "$FAIL_COUNT" -eq 0 ]; then
        printf "${GREEN}Alle Tests bestanden!${NC}\n"
        exit 0
    else
        printf "${RED}Es gibt fehlgeschlagene Tests.${NC}\n"
        exit 1
    fi
}

Verwendung des Frameworks:

#!/bin/bash
# test_math_functions.sh

source "./unit_test_framework.sh"
source "./math_functions.sh"  # Die zu testenden Funktionen

describe "Funktion add_numbers"
assert_equals "$(add_numbers 5 7)" "12" "5 + 7 sollte 12 ergeben"
assert_equals "$(add_numbers -3 3)" "0" "-3 + 3 sollte 0 ergeben"
assert_equals "$(add_numbers 0 0)" "0" "0 + 0 sollte 0 ergeben"

describe "Funktion multiply_numbers"
assert_equals "$(multiply_numbers 4 3)" "12" "4 * 3 sollte 12 ergeben"
assert_equals "$(multiply_numbers -2 5)" "-10" "-2 * 5 sollte -10 ergeben"
assert_equals "$(multiply_numbers 0 100)" "0" "0 * 100 sollte 0 ergeben"

describe "Fehlerbehandlung"
assert_output "add_numbers abc 5" "Fehler: Ungültige Zahl"
assert_output "multiply_numbers 5" "Fehler: Zwei Zahlen erforderlich"

summarize

12.6.10 Integrationstests für Shell-Skripte

Für umfassendere Tests, die die Interaktion mit realen Dateien, Netzwerkressourcen oder Datenbanken prüfen:

#!/bin/bash
# integration_test.sh

set -euo pipefail

# Testumgebung aufsetzen
setup() {
    echo "Testumgebung wird eingerichtet..."
    export TEST_DIR=$(mktemp -d)
    export TEST_DB="${TEST_DIR}/test.db"
    export CONFIG_FILE="${TEST_DIR}/config.ini"
    
    # Testdatenbank initialisieren
    sqlite3 "$TEST_DB" << EOF
CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT, email TEXT);
INSERT INTO users (name, email) VALUES ('Test User', 'test@example.com');
EOF
    
    # Testkonfiguration erstellen
    cat > "$CONFIG_FILE" << EOF
[Database]
path = $TEST_DB

[Settings]
verbose = true
log_level = debug
EOF
}

# Testumgebung aufräumen
teardown() {
    echo "Testumgebung wird aufgeräumt..."
    rm -rf "$TEST_DIR"
}

# Integrationstests ausführen
run_tests() {
    echo "Starte Integrationstests..."
    
    # Test 1: Benutzer auflisten
    echo "Test 1: Benutzer auflisten"
    output=$(./user_manager.sh --config "$CONFIG_FILE" list)
    if [[ "$output" =~ "Test User" ]] && [[ "$output" =~ "test@example.com" ]]; then
        echo "Test 1 bestanden"
    else
        echo "Test 1 fehlgeschlagen. Ausgabe: $output"
        exit 1
    fi
    
    # Test 2: Neuen Benutzer hinzufügen
    echo "Test 2: Benutzer hinzufügen"
    ./user_manager.sh --config "$CONFIG_FILE" add "New User" "new@example.com"
    output=$(./user_manager.sh --config "$CONFIG_FILE" list)
    if [[ "$output" =~ "New User" ]] && [[ "$output" =~ "new@example.com" ]]; then
        echo "Test 2 bestanden"
    else
        echo "Test 2 fehlgeschlagen. Ausgabe: $output"
        exit 1
    fi
    
    # Test 3: Benutzer löschen
    echo "Test 3: Benutzer löschen"
    ./user_manager.sh --config "$CONFIG_FILE" delete "New User"
    output=$(./user_manager.sh --config "$CONFIG_FILE" list)
    if [[ "$output" =~ "New User" ]]; then
        echo "Test 3 fehlgeschlagen. Ausgabe: $output"
        exit 1
    else
        echo "Test 3 bestanden"
    fi
    
    echo "Alle Integrationstests bestanden!"
}

# Hauptfunktion
main() {
    trap teardown EXIT
    
    setup
    run_tests
}

main

12.6.11 Property-basiertes Testen für Shell-Skripte

Property-basiertes Testen, bei dem zufällige Eingaben generiert werden, um allgemeine Eigenschaften zu testen:

#!/bin/bash
# property_test.sh

set -euo pipefail

# Zufällige Zahlen generieren
generate_random_int() {
    local min=${1:-0}
    local max=${2:-100}
    echo $((RANDOM % (max - min + 1) + min))
}

# Zufällige Strings generieren
generate_random_string() {
    local length=${1:-10}
    cat /dev/urandom | tr -dc 'a-zA-Z0-9' | fold -w "$length" | head -n 1
}

# Property: add(a,b) = add(b,a) (Kommutativität)
test_add_commutative() {
    echo "Teste Kommutativität der Addition..."
    
    for ((i=1; i<=100; i++)); do
        a=$(generate_random_int -100 100)
        b=$(generate_random_int -100 100)
        
        result1=$(./math_functions.sh add "$a" "$b")
        result2=$(./math_functions.sh add "$b" "$a")
        
        if [ "$result1" -ne "$result2" ]; then
            echo "FEHLER: add($a,$b) = $result1, aber add($b,$a) = $result2"
            exit 1
        fi
    done
    
    echo "Kommutativitäts-Property bestanden!"
}

# Property: len(a + b) = len(a) + len(b)
test_string_concat() {
    echo "Teste String-Verkettung..."
    
    for ((i=1; i<=100; i++)); do
        a=$(generate_random_string $(generate_random_int 1 20))
        b=$(generate_random_string $(generate_random_int 1 20))
        
        concat=$(./string_functions.sh concat "$a" "$b")
        
        len_a=${#a}
        len_b=${#b}
        len_concat=${#concat}
        
        if [ "$len_concat" -ne $((len_a + len_b)) ]; then
            echo "FEHLER: Länge von '$a'+'$b' ist $len_concat, sollte aber $((len_a + len_b)) sein"
            exit 1
        fi
    done
    
    echo "String-Verkettungs-Property bestanden!"
}

# Alle Property-Tests ausführen
run_property_tests() {
    test_add_commutative
    test_string_concat
    
    echo "Alle Property-Tests bestanden!"
}

run_property_tests

12.6.12 Kontinuierliche Integration für Shell-Skripte

Einbindung von Shell-Skript-Tests in CI/CD-Pipelines (GitHub Actions Beispiel):

# .github/workflows/test.yml
name: Test Shell Scripts

on: [push, pull_request]

jobs:
  test:
    runs-on: ubuntu-latest
    
    steps:
    - uses: actions/checkout@v2
    
    - name: Install dependencies
      run: |
        sudo apt-get update
        sudo apt-get install -y bats shellcheck kcov
    
    - name: Lint with shellcheck
      run: |
        shellcheck scripts/*.sh
    
    - name: Run unit tests
      run: |
        bats tests/*.bats
    
    - name: Run integration tests
      run: |
        ./tests/integration_test.sh
    
    - name: Generate coverage report
      run: |
        kcov --include-pattern=.sh coverage_report bats tests/*.bats
    
    - name: Upload coverage report
      uses: actions/upload-artifact@v2
      with:
        name: coverage-report
        path: coverage_report

12.6.13 Best Practices für Shell-Skript-Tests

Abschließend einige bewährte Praktiken für effektive Tests:

  1. Test-First-Entwicklung praktizieren:

  2. Testbarkeitsprinzipien anwenden:

  3. Testumgebung isolieren:

    # Testumgebung in einem Docker-Container
    docker run --rm -v "$PWD:/code" -w /code bash:latest ./run_tests.sh
  4. Verschiedene Teststufen einsetzen:

  5. Tests als Dokumentation verwenden:

12.6.14 In a Nutshell

Das Testen von Shell-Skripten umfasst:

  1. Strukturierte Testmethoden - Von manuellen Tests bis zu automatisierten Frameworks wie BATS oder ShellSpec
  2. Mockingstrategien - Simulieren externer Abhängigkeiten für kontrollierte Testumgebungen
  3. Testabdeckungsmessung - Identifizieren nicht getesteter Codepfade mit Tools wie kcov
  4. Eigenständige Testframeworks - Entwicklung angepasster Lösungen für spezifische Anforderungen
  5. Integrationstests - Überprüfen der Zusammenarbeit mehrerer Komponenten
  6. Property-basiertes Testen - Überprüfen allgemeiner Eigenschaften mit zufälligen Eingaben
  7. CI/CD-Integration - Automatische Ausführung von Tests bei Code-Änderungen

Mit diesen Methoden können Shell-Skripte ebenso gründlich getestet werden wie Software in “höheren” Programmiersprachen, was zu robusteren, zuverlässigeren und wartbareren Skripten führt.

12.7 Defensive Programmierung und Fehlerprävention

Defensive Programmierung ist ein Ansatz, bei dem Skripte so geschrieben werden, dass sie auch unter unerwarteten Bedingungen zuverlässig funktionieren. Im Kontext von Shell-Scripting ist dieser Ansatz besonders wichtig, da Shell-Skripte oft systemkritische Operationen ausführen und in verschiedenen Umgebungen eingesetzt werden.

12.7.1 Prinzipien der defensiven Programmierung

Die Grundprinzipien der defensiven Programmierung für Shell-Skripte umfassen:

  1. Misstrauen gegenüber Eingaben: Alle externen Eingaben als potenziell fehlerhaft betrachten
  2. Konstante Validierung: Annahmen über den Zustand des Systems kontinuierlich überprüfen
  3. Vorbereitung auf das Schlimmste: Mechanismen für unerwartete Fehler implementieren
  4. Frühes Scheitern: Probleme so früh wie möglich erkennen und behandeln
  5. Minimierung des Schadenpotenzials: Bei Fehlern sicherstellen, dass keine kritischen Ressourcen beschädigt werden

12.7.2 Der strikte Modus als Grundlage

Die Aktivierung des strikten Modus ist der erste Schritt zur defensiven Programmierung:

#!/bin/bash
set -euo pipefail

# -e: Bricht das Skript ab, wenn ein Befehl fehlschlägt
# -u: Behandelt undefinierte Variablen als Fehler
# -o pipefail: Gibt den Exit-Code des ersten fehlgeschlagenen Befehls in einer Pipe zurück

Diese Optionen können auch einzeln aktiviert oder deaktiviert werden, um spezifische Verhaltensweisen zu kontrollieren:

set -e  # Aktivieren von exit-on-error
command_that_might_fail || true  # Verhindern, dass dieser Befehl das Skript beendet
set +e  # Deaktivieren von exit-on-error für einen bestimmten Abschnitt

12.7.3 Validierung von Eingaben

Jede Eingabe sollte validiert werden, bevor sie verwendet wird:

#!/bin/bash
set -euo pipefail

validate_number() {
    local input=$1
    local name=$2
    
    if ! [[ "$input" =~ ^[0-9]+$ ]]; then
        echo "Fehler: $name muss eine positive Ganzzahl sein." >&2
        exit 1
    fi
}

validate_file_path() {
    local path=$1
    local should_exist=${2:-true}
    
    # Prüfen auf ungültige Zeichen
    if [[ "$path" =~ [[:cntrl:]] || "$path" =~ [\\:*?\"<>|] ]]; then
        echo "Fehler: Pfad '$path' enthält ungültige Zeichen." >&2
        exit 1
    fi
    
    # Prüfen auf Existenz, falls erforderlich
    if [[ "$should_exist" == "true" && ! -e "$path" ]]; then
        echo "Fehler: Pfad '$path' existiert nicht." >&2
        exit 1
    fi
}

# Anwendung
if [ "$#" -lt 2 ]; then
    echo "Verwendung: $0 <zahl> <dateipfad>" >&2
    exit 1
fi

number="$1"
file_path="$2"

validate_number "$number" "Erstes Argument"
validate_file_path "$file_path"

# Nach erfolgreicher Validierung kann mit den Argumenten gearbeitet werden

12.7.4 Sichere Variablenexpansion

Die Art der Variablenexpansion ist entscheidend für robuste Skripte:

#!/bin/bash
set -euo pipefail

# Unsicher: Kann zu Fehlern führen, wenn Variablen leer sind
rm -rf $DIRECTORY/temp

# Sicher: Fehlschlag, wenn Variable leer ist
rm -rf "${DIRECTORY:?}/temp"

# Alternativ: Standardwert setzen
rm -rf "${DIRECTORY:-/tmp}/temp"

# Überprüfen, ob Variable gesetzt und nicht leer ist
if [ -z "${DIRECTORY:-}" ]; then
    echo "Fehler: DIRECTORY ist nicht gesetzt oder leer." >&2
    exit 1
fi

Für komplexere Validierungen empfiehlt sich eine spezielle Funktion:

#!/bin/bash
set -euo pipefail

# Funktion zur Überprüfung erforderlicher Variablen
require_env() {
    local var_name=$1
    local var_value=${!var_name:-}
    local message=${2:-"Erforderliche Umgebungsvariable '$var_name' ist nicht gesetzt."}
    
    if [ -z "$var_value" ]; then
        echo "Fehler: $message" >&2
        exit 1
    fi
}

# Anwendung
require_env "DATABASE_URL" "Datenbank-URL fehlt. Bitte in .env-Datei konfigurieren."
require_env "API_KEY"

12.7.5 Sichere Dateioperationen

Bei Dateioperationen ist besondere Vorsicht geboten:

#!/bin/bash
set -euo pipefail

# Sicheres Löschen von Dateien
safe_remove() {
    local path=$1
    
    # Prüfen, ob Pfad existiert
    if [ ! -e "$path" ]; then
        echo "Warnung: '$path' existiert nicht." >&2
        return 0
    fi
    
    # Prüfen, ob Pfad ein symbolischer Link ist
    if [ -L "$path" ]; then
        # Nur den Link löschen, nicht das Ziel
        rm "$path"
        return $?
    fi
    
    # Sicherstellen, dass keine kritischen Systemverzeichnisse gelöscht werden
    case "$path" in
        /|/bin|/boot|/dev|/etc|/home|/lib|/lib64|/proc|/root|/sbin|/sys|/usr|/var)
            echo "Fehler: Versuch, kritisches Systemverzeichnis '$path' zu löschen." >&2
            return 1
            ;;
    esac
    
    # Recursive-Flag nur für Verzeichnisse verwenden
    if [ -d "$path" ] && [ ! -L "$path" ]; then
        rm -rf "$path"
    else
        rm -f "$path"
    fi
}

# Sicheres Erstellen temporärer Dateien
create_secure_temp() {
    local prefix=${1:-"tmp"}
    local temp_dir=${2:-"/tmp"}
    
    # Sicherstellen, dass Zielverzeichnis existiert und beschreibbar ist
    if [ ! -d "$temp_dir" ] || [ ! -w "$temp_dir" ]; then
        echo "Fehler: '$temp_dir' existiert nicht oder ist nicht beschreibbar." >&2
        return 1
    fi
    
    # Sichere temporäre Datei mit mktemp erstellen
    local temp_file
    temp_file=$(mktemp "${temp_dir}/${prefix}.XXXXXXXX") || {
        echo "Fehler: Konnte keine temporäre Datei erstellen." >&2
        return 1
    }
    
    # Berechtigungen einschränken
    chmod 600 "$temp_file"
    
    echo "$temp_file"
}

12.7.6 Atomare Dateioperationen

Um Race-Conditions zu vermeiden, sollten Dateioperationen atomar sein:

#!/bin/bash
set -euo pipefail

# Atomares Schreiben in eine Datei
atomic_write() {
    local target_file=$1
    local content=$2
    
    local temp_file
    temp_file=$(mktemp "${target_file}.XXXXXXXX") || {
        echo "Fehler: Konnte keine temporäre Datei erstellen." >&2
        return 1
    }
    
    echo "$content" > "$temp_file"
    
    # Atomares Umbenennen (ersetzt die Datei atomisch, falls sie existiert)
    mv -f "$temp_file" "$target_file"
}

# Atomares Aktualisieren einer Konfigurationsdatei
update_config() {
    local config_file=$1
    local key=$2
    local value=$3
    
    # Sicherstellen, dass die Datei existiert
    touch "$config_file"
    
    local temp_file
    temp_file=$(mktemp "${config_file}.XXXXXXXX") || {
        echo "Fehler: Konnte keine temporäre Datei erstellen." >&2
        return 1
    }
    
    # Konfiguration aktualisieren
    if grep -q "^${key}=" "$config_file"; then
        # Schlüssel ersetzen
        sed "s|^${key}=.*|${key}=${value}|" "$config_file" > "$temp_file"
    else
        # Schlüssel hinzufügen
        cat "$config_file" > "$temp_file"
        echo "${key}=${value}" >> "$temp_file"
    fi
    
    # Atomares Ersetzen
    mv -f "$temp_file" "$config_file"
}

12.7.7 Vermeidung von Race-Conditions mit Locking

Für Skripte, die parallel laufen könnten, sind Locking-Mechanismen wichtig:

#!/bin/bash
set -euo pipefail

# Pfad zur Lock-Datei
LOCK_FILE="/var/run/mein_skript.lock"

# Lock-Funktion
acquire_lock() {
    local wait=${1:-0}  # Optional: Maximale Wartezeit in Sekunden
    local start_time
    start_time=$(date +%s)
    
    # Versuchen, Lock zu erwerben
    while ! mkdir "$LOCK_FILE" 2>/dev/null; do
        # Prüfen, ob Lock-Datei von einem nicht mehr laufenden Prozess gehalten wird
        if [ -f "${LOCK_FILE}/pid" ]; then
            local pid
            pid=$(cat "${LOCK_FILE}/pid")
            if ! ps -p "$pid" > /dev/null; then
                echo "Warnung: Verwaistes Lock gefunden, wird zurückgesetzt." >&2
                rm -rf "$LOCK_FILE"
                continue
            fi
        fi
        
        # Prüfen, ob Wartezeit überschritten wurde
        if [ "$wait" -gt 0 ]; then
            local current_time
            current_time=$(date +%s)
            if [ $((current_time - start_time)) -ge "$wait" ]; then
                echo "Fehler: Timeout beim Warten auf Lock." >&2
                return 1
            fi
            sleep 1
        else
            echo "Fehler: Konnte Lock nicht erwerben. Ein anderer Prozess verwendet es bereits." >&2
            return 1
        fi
    done
    
    # PID in Lock-Datei schreiben
    echo $$ > "${LOCK_FILE}/pid"
    
    # Automatisches Freigeben des Locks beim Beenden
    trap release_lock EXIT
    
    return 0
}

# Lock freigeben
release_lock() {
    rm -rf "$LOCK_FILE"
}

# Anwendung
if ! acquire_lock 30; then
    exit 1
fi

echo "Lock erworben, führe kritische Operation aus..."
sleep 5  # Simulation einer zeitintensiven Operation
echo "Operation abgeschlossen."

Ein alternativer Ansatz mit flock:

#!/bin/bash
set -euo pipefail

# Pfad zur Lock-Datei
LOCK_FILE="/var/run/mein_skript.lock"

# Funktion zur Ausführung mit Lock
run_with_lock() {
    local timeout=${1:-30}  # Timeout in Sekunden
    
    # Lock-Datei erstellen, falls sie nicht existiert
    touch "$LOCK_FILE"
    
    # Versuchen, Lock zu erwerben
    if ! flock -w "$timeout" -x 200; then
        echo "Fehler: Konnte Lock nicht innerhalb von $timeout Sekunden erwerben." >&2
        return 1
    fi
    
    # Hier wird der Code ausgeführt, während das Lock gehalten wird
    echo "Lock erworben, führe kritische Operation aus..."
    sleep 5  # Simulation einer zeitintensiven Operation
    echo "Operation abgeschlossen."
    
    # Lock wird automatisch freigegeben, wenn der Dateideskriptor geschlossen wird
    
    return 0
} 200>"$LOCK_FILE"  # Dateideskriptor 200 an die Lock-Datei binden

# Anwendung
if ! run_with_lock 60; then
    exit 1
fi

12.7.8 Vermeidung von Command Injection

Command Injection ist ein häufiges Sicherheitsproblem in Shell-Skripten:

#!/bin/bash
set -euo pipefail

# Unsicher: Erlaubt Command Injection
function unsafe_command() {
    local user_input=$1
    # NIEMALS SO MACHEN:
    eval "find . -name $user_input"  # Angreifer könnte "; rm -rf /" eingeben
}

# Sicher: Verwendung von Argumenten anstelle von String-Verkettung
function safe_command() {
    local user_input=$1
    # Sicherer Ansatz:
    find . -name "$user_input"
}

# Noch sicherer: Validierung der Eingabe vor Verwendung
function validated_command() {
    local user_input=$1
    
    # Strenge Validierung der Eingabe
    if ! [[ "$user_input" =~ ^[a-zA-Z0-9._-]+$ ]]; then
        echo "Fehler: Ungültige Eingabe. Nur Buchstaben, Zahlen, Punkte, Unterstriche und Bindestriche sind erlaubt." >&2
        return 1
    fi
    
    find . -name "$user_input"
}

12.7.9 Umgang mit dynamischer Codeausführung

Wenn dynamische Codeausführung erforderlich ist, sollten strenge Sicherheitsmaßnahmen implementiert werden:

#!/bin/bash
set -euo pipefail

# Sicherere Alternative zu eval
safe_eval() {
    local code=$1
    
    # Prüfen auf potenziell gefährliche Befehle
    local dangerous_commands=("rm" "mkfs" "dd" "wget" "curl" "> /dev" ">/dev" "> /proc" ">/proc")
    
    for cmd in "${dangerous_commands[@]}"; do
        if [[ "$code" == *"$cmd"* ]]; then
            echo "Fehler: Potenziell gefährlicher Befehl '$cmd' erkannt." >&2
            return 1
        fi
    done
    
    # In Subshell ausführen, um globale Umgebung nicht zu beeinflussen
    (
        # Einschränken der verfügbaren Befehle
        PATH="/usr/bin:/bin"
        # Umgebungsvariablen einschränken
        unset LD_LIBRARY_PATH LD_PRELOAD
        # Code ausführen
        eval "$code"
    )
}

# Beispiel für sichere Ausführung von Benutzereingaben
execute_user_command() {
    local command=$1
    
    # Whitelist erlaubter Befehle
    case "$command" in
        "list_files") safe_eval "ls -la" ;;
        "show_date") safe_eval "date" ;;
        "disk_usage") safe_eval "df -h" ;;
        *) echo "Fehler: Unbekannter Befehl '$command'." >&2; return 1 ;;
    esac
}

12.7.10 Präventive Fehlerbehandlung

Präventive Fehlerbehandlung antizipiert mögliche Fehler und verhindert sie proaktiv:

#!/bin/bash
set -euo pipefail

# Funktion zum Prüfen der verfügbaren Ressourcen
check_resources() {
    # Überprüfen des verfügbaren Festplattenspeichers
    local available_space
    available_space=$(df -k /var/log | awk 'NR==2 {print $4}')
    
    if [ "$available_space" -lt 102400 ]; then  # Weniger als 100 MB
        echo "Warnung: Wenig Festplattenspeicher verfügbar." >&2
        # Alte Logs bereinigen, um Platz zu schaffen
        find /var/log -name "*.log.??" -mtime +30 -delete
    fi
    
    # Überprüfen der Anzahl offener Dateideskriptoren
    local max_fds
    max_fds=$(ulimit -n)
    local current_fds
    current_fds=$(lsof -p $$ | wc -l)
    
    if [ "$current_fds" -gt $((max_fds * 80 / 100)) ]; then
        echo "Warnung: Hohe Anzahl offener Dateideskriptoren." >&2
        # Kritische Operationen verschieben oder Ressourcen freigeben
    fi
}

# Funktion zum Prüfen von Netzwerkverbindungen
check_connectivity() {
    local host=$1
    local port=$2
    local timeout=${3:-5}
    
    # Prüfen, ob Netzwerkverbindung verfügbar ist
    if ! timeout "$timeout" bash -c "echo > /dev/tcp/$host/$port" 2>/dev/null; then
        echo "Fehler: Keine Verbindung zu $host:$port möglich." >&2
        return 1
    fi
    
    return 0
}

12.7.11 Transaktionale Operationen

Für komplexe Änderungen ist ein transaktionaler Ansatz sinnvoll:

#!/bin/bash
set -euo pipefail

# Funktion zur transaktionalen Ausführung von Operationen
transaction() {
    local operation_name=$1
    shift
    local operations=("$@")
    
    echo "Starte Transaktion: $operation_name"
    
    # Checkpoint-Verzeichnis für Rollback
    local checkpoint_dir
    checkpoint_dir=$(mktemp -d)
    trap 'rm -rf "$checkpoint_dir"' EXIT
    
    # Liste der erfolgten Änderungen für Rollback
    local changes_file="$checkpoint_dir/changes.log"
    touch "$changes_file"
    
    # Durchführen der Operationen
    local failed=false
    for ((i=0; i<${#operations[@]}; i++)); do
        local op="${operations[$i]}"
        echo "  Operation $((i+1))/${#operations[@]}: $op"
        
        if ! eval "$op"; then
            echo "Fehler bei Operation $((i+1)): $op" >&2
            failed=true
            break
        fi
    done
    
    # Rollback bei Fehler
    if $failed; then
        echo "Fehler aufgetreten, führe Rollback durch..." >&2
        
        # Operationen in umgekehrter Reihenfolge rückgängig machen
        if [ -s "$changes_file" ]; then
            local rollback_ops
            rollback_ops=$(tac "$changes_file")
            
            while IFS= read -r rollback_op; do
                echo "  Rollback: $rollback_op" >&2
                eval "$rollback_op" || echo "Warnung: Rollback-Operation fehlgeschlagen: $rollback_op" >&2
            done <<< "$rollback_ops"
        fi
        
        echo "Rollback abgeschlossen." >&2
        return 1
    fi
    
    echo "Transaktion erfolgreich abgeschlossen: $operation_name"
    return 0
}

# Beispiel für transaktionale Dateioperationen
copy_with_backup() {
    local source=$1
    local target=$2
    
    # Backup-Operation mit Rollback-Information
    local backup_file="$target.bak"
    if [ -e "$target" ]; then
        cp -a "$target" "$backup_file" && echo "if [ -e \"$backup_file\" ]; then mv \"$backup_file\" \"$target\"; else rm -f \"$target\"; fi" >> "$changes_file"
    else
        echo "rm -f \"$target\"" >> "$changes_file"
    fi
    
    # Kopieroperation
    cp -a "$source" "$target"
}

# Anwendung
transaction "System-Update" \
    "copy_with_backup config.new /etc/app/config.json" \
    "systemctl restart app-service" \
    "update-database-schema /etc/app/config.json"

12.7.12 Verteidigungstiefe durch Redundanz

Das Prinzip der Verteidigungstiefe implementiert mehrere Schutzschichten:

#!/bin/bash
set -euo pipefail

# Funktion mit mehreren Validierungsebenen
process_user_data() {
    local user_data=$1
    
    # Ebene 1: Grundlegende Eingabevalidierung
    if [ -z "$user_data" ]; then
        echo "Fehler: Leere Eingabe nicht erlaubt." >&2
        return 1
    fi
    
    # Ebene 2: Formatvalidierung
    if ! [[ "$user_data" =~ ^[a-zA-Z0-9_.-]+$ ]]; then
        echo "Fehler: Ungültiges Format. Nur Buchstaben, Zahlen und bestimmte Sonderzeichen erlaubt." >&2
        return 1
    fi
    
    # Ebene 3: Inhaltliche Validierung
    if [[ "$user_data" =~ ^(admin|root|system)$ ]]; then
        echo "Fehler: Reservierte Namen nicht erlaubt." >&2
        return 1
    fi
    
    # Ebene 4: Längenvalidierung
    if [ ${#user_data} -gt 32 ]; then
        echo "Fehler: Maximale Länge (32 Zeichen) überschritten." >&2
        return 1
    fi
    
    # Ebene 5: Kontextvalidierung (z.B. Existenzprüfung)
    if [ -e "/var/data/$user_data" ]; then
        echo "Fehler: Datensatz existiert bereits." >&2
        return 1
    fi
    
    # Nach erfolgreicher Validierung: Verarbeitung
    echo "Verarbeite Daten: $user_data"
    # ...weitere Verarbeitung...
    
    return 0
}

12.7.13 Logging als präventive Maßnahme

Umfassendes Logging kann zur Fehlerprävention beitragen:

#!/bin/bash
set -euo pipefail

# Konfiguration
LOG_FILE="/var/log/mein_skript.log"
LOG_LEVEL=${LOG_LEVEL:-"INFO"}  # Mögliche Werte: DEBUG, INFO, WARN, ERROR

# Log-Funktion mit detaillierten Kontext-Informationen
log() {
    local level=$1
    local message=$2
    local timestamp
    timestamp=$(date '+%Y-%m-%d %H:%M:%S')
    local caller_info
    caller_info=$(caller 0)
    local line_number
    line_number=$(echo "$caller_info" | awk '{print $1}')
    local function_name
    function_name=$(echo "$caller_info" | awk '{print $2}')
    
    # Log-Level numerisch umsetzen für Vergleich
    local level_num=0
    case $level in
        DEBUG) level_num=0 ;;
        INFO)  level_num=1 ;;
        WARN)  level_num=2 ;;
        ERROR) level_num=3 ;;
    esac
    
    local current_level_num=1  # Standard: INFO
    case $LOG_LEVEL in
        DEBUG) current_level_num=0 ;;
        INFO)  current_level_num=1 ;;
        WARN)  current_level_num=2 ;;
        ERROR) current_level_num=3 ;;
    esac
    
    # Nur loggen, wenn Level gleich oder höher als konfigurierter Level
    if [ "$level_num" -ge "$current_level_num" ]; then
        local log_message="$timestamp [$level] [$function_name:$line_number] $message"
        
        # In Datei loggen, wenn möglich
        if [ -w "$(dirname "$LOG_FILE")" ]; then
            echo "$log_message" >> "$LOG_FILE"
        fi
        
        # Je nach Level auf verschiedene Ausgabeströme ausgeben
        case $level in
            ERROR) echo "$log_message" >&2 ;;
            WARN)  echo "$log_message" >&2 ;;
            *)     echo "$log_message" ;;
        esac
    fi
}

# Beispiel-Verwendung
critical_operation() {
    log "INFO" "Starte kritische Operation"
    
    # Vorprüfungen loggen
    log "DEBUG" "Prüfe Ressourcen: Speicher=$(free -m | awk '/^Mem:/ {print $4}')MB frei"
    
    # Warnung loggen
    if [ "$(free -m | awk '/^Mem:/ {print $4}')" -lt 100 ]; then
        log "WARN" "Wenig Speicher verfügbar, Operation könnte fehlschlagen"
    fi
    
    # Kritische Operation mit Fehlerprotokollierung
    if ! some_critical_command; then
        log "ERROR" "Kritische Operation fehlgeschlagen: $?"
        return 1
    fi
    
    log "INFO" "Kritische Operation erfolgreich abgeschlossen"
    return 0
}

12.7.14 Proaktive Überwachung und selbstheilende Skripte

Skripte können auch so konzipiert werden, dass sie sich selbst überwachen und bei Problemen korrigierend eingreifen:

#!/bin/bash
set -euo pipefail

# Funktion zur Selbstheileung
self_heal() {
    local problem=$1
    local fix_command=$2
    
    echo "Problem erkannt: $problem" >&2
    echo "Führe Selbstheilung durch: $fix_command" >&2
    
    if ! eval "$fix_command"; then
        echo "Selbstheilung fehlgeschlagen" >&2
        return 1
    fi
    
    echo "Selbstheilung erfolgreich" >&2
    return 0
}

# Funktion zum Prüfen und Beheben von Problemen
healthcheck() {
    # Prüfen auf fehlende Konfigurationsdatei
    if [ ! -f "/etc/myapp/config.json" ]; then
        self_heal "Konfigurationsdatei fehlt" "cp /etc/myapp/config.json.default /etc/myapp/config.json"
    fi
    
    # Prüfen auf fehlenden Dienst
    if ! systemctl is-active --quiet myapp; then
        self_heal "Dienst ist nicht aktiv" "systemctl start myapp"
    fi
    
    # Prüfen auf übermäßige CPU-Auslastung
    local cpu_usage
    cpu_usage=$(ps -p $(pgrep -f myapp) -o %cpu | tail -n 1)
    if [ "${cpu_usage/.*/}" -gt 90 ]; then
        self_heal "Hohe CPU-Auslastung" "systemctl restart myapp"
    fi
    
    # Prüfen auf Speicherlecks
    local memory_usage
    memory_usage=$(ps -p $(pgrep -f myapp) -o %mem | tail -n 1)
    if [ "${memory_usage/.*/}" -gt 80 ]; then
        self_heal "Hohe Speicherauslastung" "systemctl restart myapp"
    fi
    
    # Prüfen auf Festplattenplatz
    local disk_usage
    disk_usage=$(df -h / | awk 'NR==2 {print $5}' | tr -d '%')
    if [ "$disk_usage" -gt 90 ]; then
        self_heal "Festplatte fast voll" "find /var/log -name '*.gz' -mtime +30 -delete"
    fi
    
    return 0
}

# Regelmäßige Ausführung des Healthchecks
while true; do
    healthcheck || echo "Warnung: Healthcheck fehlgeschlagen" >&2
    sleep 300  # Alle 5 Minuten wiederholen
done