7 Kontrollstrukturen

Kontrollstrukturen sind das Fundament imperativer Programmierung. Ohne sie wäre jeder Code ein linearer Strom aus Anweisungen – starr, unbeweglich, reaktiv statt intelligent. Erst durch Verzweigungen, Schleifen und bedingte Ausführung bekommt ein Programm die Fähigkeit zur Entscheidung, zur Wiederholung, zur Dynamik.

Sie sind das, was den Unterschied macht zwischen einer bloßen Befehlsliste und einem echten, denkenden Ablauf. Wer kontrolliert, der steuert – wer steuert, der programmiert.

7.1 Bedingte Anweisungen: if, else, elif

In Shell-Skripten müssen wir häufig Entscheidungen treffen, die den weiteren Programmablauf bestimmen. Bedingte Anweisungen ermöglichen es, Code nur dann auszuführen, wenn bestimmte Bedingungen erfüllt sind. Die Bash bietet hierfür die if-Anweisung mit ihren Erweiterungen else und elif.

7.1.1 Grundstruktur der if-Anweisung

Die einfachste Form einer bedingten Anweisung ist die if-Struktur:

if BEDINGUNG
then
    BEFEHLE
fi

Der Befehlsblock zwischen then und fi wird nur dann ausgeführt, wenn die BEDINGUNG “wahr” (erfolgreich) ist. In der Shell bedeutet “wahr”, dass der Befehl einen Exit-Status von 0 zurückgibt.

Beispiel:

#!/bin/bash

# Prüfen, ob eine Datei existiert
if [ -f /etc/passwd ]
then
    echo "Die Datei /etc/passwd existiert."
fi

7.1.2 Erweiterung mit else

Mit else können wir einen alternativen Codeblock ausführen, wenn die Bedingung nicht erfüllt ist:

if BEDINGUNG
then
    BEFEHLE_WENN_WAHR
else
    BEFEHLE_WENN_FALSCH
fi

Beispiel:

#!/bin/bash

# Prüfen, ob der Benutzer root ist
if [ "$(id -u)" -eq 0 ]
then
    echo "Skript wird als Root ausgeführt."
else
    echo "Skript wird nicht als Root ausgeführt."
fi

7.1.3 Mehrfachverzweigungen mit elif

Für komplexere Entscheidungsstrukturen mit mehreren Bedingungen nutzen wir elif (else if):

if BEDINGUNG1
then
    BEFEHLE1
elif BEDINGUNG2
then
    BEFEHLE2
elif BEDINGUNG3
then
    BEFEHLE3
else
    STANDARD_BEFEHLE
fi

Beispiel:

#!/bin/bash

# Prüfung der Tageszeit und Ausgabe einer passenden Nachricht
STUNDE=$(date +%H)

if [ $STUNDE -lt 12 ]
then
    echo "Guten Morgen!"
elif [ $STUNDE -lt 18 ]
then
    echo "Guten Tag!"
else
    echo "Guten Abend!"
fi

7.1.4 Syntax-Varianten

Die Bash bietet verschiedene Möglichkeiten, if-Anweisungen zu schreiben:

  1. Die klassische mehrzeilige Form (wie oben gezeigt)
  2. Eine einzeilige Form mit Semikolons:
if BEDINGUNG; then BEFEHLE; fi
  1. Die then-Anweisung kann auch auf der gleichen Zeile wie if stehen:
if BEDINGUNG
then
    BEFEHLE
fi

7.1.5 Bedingte Ausführung mit && und ||

Für einfache bedingte Anweisungen bietet die Shell auch die Operatoren && (AND) und || (OR):

# Führe BEFEHL2 nur aus, wenn BEFEHL1 erfolgreich war (Exit-Status 0)
BEFEHL1 && BEFEHL2

# Führe BEFEHL2 nur aus, wenn BEFEHL1 nicht erfolgreich war (Exit-Status nicht 0)
BEFEHL1 || BEFEHL2

Beispiele:

# Verzeichnis erstellen und hineinwechseln (wenn Erstellung erfolgreich)
mkdir -p /tmp/test && cd /tmp/test

# Paket installieren, falls es nicht bereits installiert ist
dpkg -l | grep -q apache2 || apt-get install apache2

Diese Kurzschreibweisen sind nützlich für einfache bedingte Operationen, ersetzen aber nicht die vollständige if-Struktur für komplexere Logik.

7.1.6 Geschachtelte if-Anweisungen

Bedingte Anweisungen können auch geschachtelt werden:

if BEDINGUNG1
then
    if BEDINGUNG2
    then
        BEFEHLE
    fi
fi

Diese Schachtelung sollte jedoch sparsam eingesetzt werden, da sie die Lesbarkeit des Codes beeinträchtigen kann. Häufig lassen sich geschachtelte Bedingungen durch logische Operatoren (&&, ||) innerhalb einer einzelnen Bedingung ersetzen.

7.1.7 Praxistipp: Indentierung

Um die Lesbarkeit von Shell-Skripten mit bedingten Anweisungen zu verbessern, ist eine konsistente Einrückung (Indentierung) wichtig:

if [ "$1" = "start" ]
then
    echo "Starte Dienst..."
    # Eingerückter Code für bessere Lesbarkeit
    service_start
elif [ "$1" = "stop" ]
then
    echo "Stoppe Dienst..."
    service_stop
else
    echo "Unbekannter Parameter: $1"
    echo "Verwendung: $0 {start|stop}"
    exit 1
fi

Bedingte Anweisungen sind ein fundamentales Werkzeug für Shell-Skripte und ermöglichen die flexible Steuerung des Programmflusses basierend auf verschiedenen Bedingungen. In den folgenden Abschnitten werden wir detailliert betrachten, wie wir Bedingungen mit dem test-Befehl formulieren können.

7.2 Dateitests und Berechtigungsprüfungen

Im Herzen der Shell-Programmierung steht die Fähigkeit, mit dem Dateisystem zu interagieren und Dateien sowie deren Eigenschaften zu prüfen. Der test-Befehl, oder in seiner gebräuchlicheren Schreibweise die eckigen Klammern [ ], bietet ein leistungsstarkes Werkzeug, um eine Vielzahl von Dateieigenschaften zu prüfen. Dieses Feature ist einer der größten Vorteile der Bash gegenüber anderen Skriptsprachen bei systemnahen Operationen.

7.2.1 Der test-Befehl und seine Schreibweisen

Der test-Befehl existiert in verschiedenen Formen:

# Äquivalente Schreibweisen
test AUSDRUCK
[ AUSDRUCK ]
[[ AUSDRUCK ]]  # Erweiterte Bash-Syntax

Die Schreibweise mit eckigen Klammern [ ] ist am weitesten verbreitet und am besten lesbar. Die doppelten eckigen Klammern [[ ]] bieten erweiterte Funktionen, sind aber Bash-spezifisch und daher nicht in allen POSIX-konformen Shells verfügbar.

Wichtig: Bei der Verwendung von [ ] sind Leerzeichen nach der öffnenden und vor der schließenden Klammer obligatorisch! Dies ist ein häufiger Fehler bei Einsteigern.

7.2.2 Dateitests

Hier sind die wichtigsten Dateitestoperatoren, die in Kombination mit test oder [ ] verwendet werden:

7.2.2.1 Existenz und Typ

Operator Prüfung Beschreibung
-e DATEI Existenz Wahr, wenn DATEI existiert
-f DATEI Reguläre Datei Wahr, wenn DATEI existiert und eine reguläre Datei ist
-d DATEI Verzeichnis Wahr, wenn DATEI existiert und ein Verzeichnis ist
-L DATEI Symbolischer Link Wahr, wenn DATEI existiert und ein symbolischer Link ist
-p DATEI Named Pipe Wahr, wenn DATEI existiert und eine Named Pipe ist
-S DATEI Socket Wahr, wenn DATEI existiert und ein Socket ist
-b DATEI Block Device Wahr, wenn DATEI existiert und ein Block Device ist
-c DATEI Character Device Wahr, wenn DATEI existiert und ein Character Device ist

7.2.2.2 Berechtigungen

Operator Prüfung Beschreibung
-r DATEI Lesbar Wahr, wenn DATEI existiert und lesbar ist
-w DATEI Schreibbar Wahr, wenn DATEI existiert und schreibbar ist
-x DATEI Ausführbar Wahr, wenn DATEI existiert und ausführbar ist
-u DATEI SUID-Bit Wahr, wenn DATEI existiert und das Set-UID-Bit gesetzt ist
-g DATEI SGID-Bit Wahr, wenn DATEI existiert und das Set-GID-Bit gesetzt ist
-k DATEI Sticky Bit Wahr, wenn DATEI existiert und das Sticky-Bit gesetzt ist

7.2.2.3 Dateiattribute und Vergleiche

Operator Prüfung Beschreibung
-s DATEI Größe > 0 Wahr, wenn DATEI existiert und eine Größe größer als Null hat
-t FD Terminal Wahr, wenn der Dateideskriptor FD ein Terminal ist
-N DATEI Modifikation Wahr, wenn DATEI existiert und seit dem letzten Lesen modifiziert wurde
DATEI1 -nt DATEI2 Neuer als Wahr, wenn DATEI1 neuer ist als DATEI2 (Modifikationszeit)
DATEI1 -ot DATEI2 Älter als Wahr, wenn DATEI1 älter ist als DATEI2 (Modifikationszeit)
DATEI1 -ef DATEI2 Gleiche Datei Wahr, wenn DATEI1 und DATEI2 auf die gleiche Inode verweisen

7.2.3 Praktische Beispiele

7.2.3.1 Prüfen der Existenz einer Datei vor dem Zugriff

#!/bin/bash

CONFIG_FILE="/etc/myapp/config.conf"

if [ -f "$CONFIG_FILE" ]; then
    echo "Konfigurationsdatei gefunden, verarbeite..."
    # Verarbeitung der Datei
else
    echo "Fehler: Konfigurationsdatei nicht gefunden!"
    echo "Erstelle Standardkonfiguration..."
    mkdir -p "$(dirname "$CONFIG_FILE")"
    echo "# Standardkonfiguration" > "$CONFIG_FILE"
fi

7.2.3.2 Sicheres Ausführen eines Befehls mit Berechtigungsprüfung

#!/bin/bash

SCRIPT="/usr/local/bin/wichtiges_skript.sh"

if [ -x "$SCRIPT" ]; then
    echo "Führe Skript aus..."
    "$SCRIPT"
else
    echo "Fehler: $SCRIPT ist nicht ausführbar oder existiert nicht!"
    exit 1
fi

7.2.3.3 Backup-Skript mit Dateivergleichen

#!/bin/bash

SOURCE_DIR="/home/user/dokumente"
BACKUP_DIR="/mnt/backup/dokumente"
BACKUP_MARKER="$BACKUP_DIR/.last_backup"

# Prüfung, ob das Quellverzeichnis existiert
if [ ! -d "$SOURCE_DIR" ]; then
    echo "Fehler: Quellverzeichnis existiert nicht!"
    exit 1
fi

# Prüfung, ob das Zielverzeichnis existiert und beschreibbar ist
if [ ! -d "$BACKUP_DIR" ] || [ ! -w "$BACKUP_DIR" ]; then
    echo "Fehler: Backup-Verzeichnis existiert nicht oder ist nicht beschreibbar!"
    exit 1
fi

# Prüfung, ob ein Backup notwendig ist
if [ -f "$BACKUP_MARKER" ] && [ ! "$(find "$SOURCE_DIR" -type f -newer "$BACKUP_MARKER")" ]; then
    echo "Kein Backup notwendig, keine neuen oder geänderten Dateien."
    exit 0
fi

# Backup durchführen
echo "Starte Backup-Prozess..."
rsync -av "$SOURCE_DIR/" "$BACKUP_DIR/"
touch "$BACKUP_MARKER"
echo "Backup abgeschlossen."

7.2.4 Fortgeschrittene Techniken

7.2.4.1 Kombinieren von Dateitests mit logischen Operatoren

# Prüfen, ob eine Datei existiert UND schreibbar ist
if [ -f "$FILE" ] && [ -w "$FILE" ]; then
    echo "Datei existiert und ist schreibbar"
fi

# Prüfen, ob eine Datei ein Verzeichnis ODER ein symbolischer Link ist
if [ -d "$PATH" ] || [ -L "$PATH" ]; then
    echo "Pfad ist ein Verzeichnis oder ein symbolischer Link"
fi

7.2.4.2 Verwenden der erweiterten Bash-Syntax für komplexere Tests

Die doppelten eckigen Klammern [[ ]] bieten zusätzliche Funktionen:

# Pattern Matching mit Wildcards
if [[ "$FILENAME" == *.jpg ]]; then
    echo "Datei ist ein JPEG-Bild"
fi

# Reguläre Ausdrücke
if [[ "$EMAIL" =~ ^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$ ]]; then
    echo "Gültige E-Mail-Adresse"
fi

7.2.4.3 Direkte Integration in Schleifen

# Über alle ausführbaren Dateien in einem Verzeichnis iterieren
for file in /usr/local/bin/*; do
    if [ -x "$file" ] && [ ! -d "$file" ]; then
        echo "Ausführbare Datei gefunden: $file"
    fi
done

7.2.5 Best Practices für Dateitests

  1. Variablen immer in Anführungszeichen setzen, um Probleme mit Leerzeichen oder Sonderzeichen zu vermeiden:

    if [ -f "$FILENAME" ]; then  # Richtig
    if [ -f $FILENAME ]; then    # Falsch (wenn FILENAME Leerzeichen enthält)
  2. Pfade mit relativen und absoluten Pfaden testen:

    # Absoluter Pfad für systemweite Dateien
    if [ -f "/etc/hosts" ]; then
        # ...
    fi
    
    # Relativer Pfad für Dateien im aktuellen Kontext
    if [ -f "./config.ini" ]; then
        # ...
    fi
  3. Negation für Fehlerbehandlung verwenden:

    if [ ! -d "$DIR" ]; then
        echo "Fehler: Verzeichnis existiert nicht!"
        exit 1
    fi
  4. Vorsicht bei Symbolischen Links: -L prüft, ob etwas ein symbolischer Link ist, während andere Tests dem Link folgen:

    # Prüft, ob es sich um einen symbolischen Link handelt
    if [ -L "$PATH" ]; then
        echo "Ist ein symbolischer Link"
    fi
    
    # Prüft, ob das Ziel des Links ein Verzeichnis ist
    if [ -d "$PATH" ]; then
        echo "Ist ein Verzeichnis (oder Link auf ein Verzeichnis)"
    fi

7.2.6 Fallstricke und ihre Vermeidung

  1. Fehlende Anführungszeichen bei Variablen:

    FILE="Mein Dokument.txt"
    if [ -f $FILE ]; then  # Fehlerhaft! Bash interpretiert als: [ -f Mein Dokument.txt ]
        # ...
    fi
    
    # Korrekt:
    if [ -f "$FILE" ]; then
        # ...
    fi
  2. Fehlende Leerzeichen innerhalb der eckigen Klammern:

    if [-f "$FILE" ]; then  # Fehler: Bash interpretiert [-f als Befehl
        # ...
    fi
    
    # Korrekt:
    if [ -f "$FILE" ]; then
        # ...
    fi
  3. Vergessen der Existenzprüfung vor anderen Tests:

    # Gefährlich: Könnte zu irreführenden Fehlermeldungen führen, wenn die Datei nicht existiert
    if [ -w "$FILE" ]; then
        # ...
    fi
    
    # Sicherer:
    if [ -e "$FILE" ] && [ -w "$FILE" ]; then
        # ...
    fi
  4. Verwechslung von -e (existiert) und -f (ist reguläre Datei):

    # Prüft nur, ob etwas existiert (könnte auch ein Verzeichnis sein)
    if [ -e "$PATH" ]; then
        # ...
    fi
    
    # Prüft, ob es eine reguläre Datei ist
    if [ -f "$PATH" ]; then
        # ...
    fi

7.2.7 Praktische Anwendungsfälle für Systemadministratoren

7.2.7.1 Überwachung von Festplattenplatz

#!/bin/bash

DISK_USAGE=$(df -h / | awk 'NR==2 {print $5}' | sed 's/%//g')
THRESHOLD=90

if [ "$DISK_USAGE" -gt "$THRESHOLD" ]; then
    echo "WARNUNG: Festplattennutzung bei ${DISK_USAGE}% überschreitet den Schwellenwert von ${THRESHOLD}%!"
    
    # Prüfen, ob das Log-Verzeichnis existiert und beschreibbar ist
    LOG_DIR="/var/log/disk-alerts"
    if [ ! -d "$LOG_DIR" ]; then
        if mkdir -p "$LOG_DIR" 2>/dev/null; then
            echo "Log-Verzeichnis erstellt: $LOG_DIR"
        else
            echo "Fehler: Konnte Log-Verzeichnis nicht erstellen!" >&2
            LOG_DIR="/tmp"  # Fallback auf /tmp
        fi
    fi
    
    # Log-Datei schreiben
    LOG_FILE="$LOG_DIR/disk-alert-$(date +%Y%m%d).log"
    echo "$(date): Festplattennutzung bei ${DISK_USAGE}%" >> "$LOG_FILE"
    
    # Weitere Aktionen...
else
    echo "Festplattennutzung normal (${DISK_USAGE}%)"
fi

7.2.7.2 Automatische Backup-Validierung

#!/bin/bash

BACKUP_FILE="/mnt/backup/system-backup-$(date +%Y%m%d).tar.gz"
MIN_SIZE=$((1024 * 1024 * 100))  # 100 MB Mindestgröße

# Prüfen, ob die Backup-Datei existiert
if [ ! -f "$BACKUP_FILE" ]; then
    echo "FEHLER: Backup-Datei nicht gefunden: $BACKUP_FILE" >&2
    exit 1
fi

# Größenprüfung
FILE_SIZE=$(stat -c %s "$BACKUP_FILE")
if [ "$FILE_SIZE" -lt "$MIN_SIZE" ]; then
    echo "WARNUNG: Backup-Datei ist verdächtig klein: $(numfmt --to=iec-i --suffix=B $FILE_SIZE)" >&2
    exit 2
fi

# Integritätsprüfung
if command -v gzip -t "$BACKUP_FILE" >/dev/null 2>&1; then
    echo "Backup-Datei ist valide."
else
    echo "FEHLER: Backup-Datei ist beschädigt!" >&2
    exit 3
fi

# Berechtigungsprüfung
if [ ! -r "$BACKUP_FILE" ]; then
    echo "WARNUNG: Backup-Datei ist nicht lesbar!" >&2
    chmod +r "$BACKUP_FILE" || exit 4
fi

echo "Backup-Validierung erfolgreich abgeschlossen."

Die Beherrschung der Dateisystemtests und Berechtigungsprüfungen in Bash-Skripten ist einer der wichtigsten Aspekte erfolgreicher Systemadministration und Automatisierung unter Linux/Unix. Mit diesem Arsenal an Testoperatoren können Sie robuste, sichere und zuverlässige Skripte schreiben, die elegant mit dem Dateisystem interagieren.

7.3 Vergleichsoperatoren für Zahlen und Strings

Eine wesentliche Komponente bedingter Anweisungen in Shell-Skripten ist die Fähigkeit, Werte zu vergleichen. Die Bash bietet spezifische Operatoren für den Vergleich von Zahlen und Strings, die sich in ihrer Syntax und Anwendung unterscheiden. Das Verständnis dieser Operatoren ist entscheidend für die Erstellung zuverlässiger und präziser Bedingungen.

7.3.1 Numerische Vergleiche

Für den Vergleich von Zahlen verwendet die Bash spezielle Operatoren, die innerhalb der test-Umgebung (also zwischen [ ]) stehen:

Operator Bedeutung Beispiel
-eq Equal (gleich) [ "$a" -eq "$b" ]
-ne Not Equal (ungleich) [ "$a" -ne "$b" ]
-gt Greater Than (größer als) [ "$a" -gt "$b" ]
-ge Greater or Equal (größer oder gleich) [ "$a" -ge "$b" ]
-lt Less Than (kleiner als) [ "$a" -lt "$b" ]
-le Less or Equal (kleiner oder gleich) [ "$a" -le "$b" ]

7.3.1.1 Beispiel für numerische Vergleiche

#!/bin/bash

# Überprüfen des verfügbaren Festplattenspeichers
AVAILABLE_SPACE=$(df -k / | awk 'NR==2 {print $4}')
MIN_REQUIRED=1048576  # 1 GB in Kilobytes

if [ "$AVAILABLE_SPACE" -lt "$MIN_REQUIRED" ]; then
    echo "Warnung: Weniger als 1 GB Festplattenspeicher verfügbar!"
    echo "Verfügbar: $((AVAILABLE_SPACE / 1024)) MB"
else
    echo "Ausreichend Festplattenspeicher vorhanden."
    echo "Verfügbar: $((AVAILABLE_SPACE / 1024)) MB"
fi

7.3.2 String-Vergleiche

Für den Vergleich von Zeichenketten stehen folgende Operatoren zur Verfügung:

Operator Bedeutung Beispiel
= oder == Gleich (Inhaltsvergleich) [ "$a" = "$b" ]
!= Ungleich [ "$a" != "$b" ]
< Lexikografisch kleiner [ "$a" \< "$b" ]
> Lexikografisch größer [ "$a" \> "$b" ]
-z Leerer String (Zero length) [ -z "$a" ]
-n Nicht-leerer String (Non-zero length) [ -n "$a" ]

Wichtig: Bei den lexikografischen Vergleichen (< und >) müssen die Operatoren in [ ] mit einem Backslash maskiert werden, um eine Interpretation als I/O-Umleitungen zu verhindern.

7.3.2.1 Beispiel für String-Vergleiche

#!/bin/bash

USERNAME="admin"
PASSWORD="geheim123"

echo -n "Benutzername: "
read INPUT_USER

echo -n "Passwort: "
read -s INPUT_PASS
echo  # Zeilenumbruch nach Passwort-Eingabe

if [ -z "$INPUT_USER" ]; then
    echo "Fehler: Benutzername darf nicht leer sein!"
    exit 1
fi

if [ "$INPUT_USER" = "$USERNAME" ] && [ "$INPUT_PASS" = "$PASSWORD" ]; then
    echo "Login erfolgreich. Willkommen, $USERNAME!"
else
    echo "Fehler: Falsche Anmeldedaten!"
    exit 1
fi

7.3.3 Unterschiede zwischen [ ] und [[ ]]

Die Bash bietet mit [[ ]] eine erweiterte Syntax für Vergleiche, die einige Vorteile bietet:

  1. Keine Notwendigkeit für Maskierung:

    # Mit [ ] - Maskierung nötig
    [ "$a" \< "$b" ]
    
    # Mit [[ ]] - Keine Maskierung nötig
    [[ "$a" < "$b" ]]
  2. Unterstützung für Pattern Matching:

    # Prüfen, ob eine Zeichenkette einem Muster entspricht
    [[ "$filename" == *.txt ]]
  3. Unterstützung für reguläre Ausdrücke:

    # Prüfen, ob eine E-Mail-Adresse gültig ist
    [[ "$email" =~ ^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$ ]]
  4. Intelligenterer Umgang mit leeren Variablen:

    # Bei [ ] könnte dies zu einem Fehler führen, wenn $var leer ist
    [ $var = "test" ]
    
    # Bei [[ ]] ist dies sicher
    [[ $var = "test" ]]

Hinweis: Die [[ ]]-Syntax ist eine Bash-Erweiterung und nicht POSIX-konform. Wenn Sie Portabilität zu anderen Shells benötigen, verwenden Sie die traditionelle [ ]-Syntax.

7.3.4 Arithmetische Vergleiche mit (( ))

Für rein arithmetische Vergleiche bietet die Bash die (( ))-Syntax, die mathematische Notationen und C-ähnliche Operatoren unterstützt:

# Arithmetische Vergleiche
if (( a > b )); then
    echo "a ist größer als b"
fi

# Arithmetische Ausdrücke
if (( (a + b) * c > 100 )); then
    echo "Das Ergebnis ist größer als 100"
fi
Operator Bedeutung
== Gleich
!= Ungleich
> Größer als
>= Größer oder gleich
< Kleiner als
<= Kleiner oder gleich

7.3.5 Praktische Anwendungsfälle

7.3.5.1 Version-Checking

#!/bin/bash

# Mindestversion für eine Anwendung prüfen
REQUIRED_VERSION="2.5"
CURRENT_VERSION=$(myapp --version | awk '{print $2}')

# Versionsteile extrahieren
REQUIRED_MAJOR=$(echo "$REQUIRED_VERSION" | cut -d. -f1)
REQUIRED_MINOR=$(echo "$REQUIRED_VERSION" | cut -d. -f2)
CURRENT_MAJOR=$(echo "$CURRENT_VERSION" | cut -d. -f1)
CURRENT_MINOR=$(echo "$CURRENT_VERSION" | cut -d. -f2)

if [ "$CURRENT_MAJOR" -lt "$REQUIRED_MAJOR" ] || 
   ([ "$CURRENT_MAJOR" -eq "$REQUIRED_MAJOR" ] && [ "$CURRENT_MINOR" -lt "$REQUIRED_MINOR" ]); then
    echo "Fehler: myapp Version $REQUIRED_VERSION oder höher wird benötigt!"
    echo "Aktuelle Version: $CURRENT_VERSION"
    exit 1
fi

echo "Versionsanforderung erfüllt. Fahre fort..."

7.3.5.2 Benutzerinteraktion

#!/bin/bash

echo "Möchten Sie fortfahren? (j/n): "
read ANSWER

# String-Vergleich mit Berücksichtigung von Groß- und Kleinschreibung
if [[ "${ANSWER,,}" == "j" || "${ANSWER,,}" == "ja" ]]; then
    echo "Fortfahren..."
elif [[ "${ANSWER,,}" == "n" || "${ANSWER,,}" == "nein" ]]; then
    echo "Abbruch."
    exit 0
else
    echo "Ungültige Eingabe. Bitte 'j' oder 'n' eingeben."
    exit 1
fi

7.3.5.3 Erweiterte Textanalyse

#!/bin/bash

# Prüfen, ob ein Text einer bestimmten Struktur entspricht
LOG_LINE="2023-04-10 14:25:32 ERROR: Connection timeout"

# Mit regulären Ausdrücken prüfen
if [[ "$LOG_LINE" =~ ^[0-9]{4}-[0-9]{2}-[0-9]{2}\ [0-9]{2}:[0-9]{2}:[0-9]{2}\ (ERROR|WARNING|INFO): ]]; then
    echo "Gültiges Log-Format"
    
    # Schweregrad extrahieren
    if [[ "$LOG_LINE" =~ ERROR ]]; then
        echo "Schweregrad: ERROR - Kritischer Fehler!"
    elif [[ "$LOG_LINE" =~ WARNING ]]; then
        echo "Schweregrad: WARNING - Beachten Sie diese Meldung"
    else
        echo "Schweregrad: INFO - Nur zur Information"
    fi
else
    echo "Ungültiges Log-Format"
fi

7.3.6 Best Practices

  1. Variablen in Anführungszeichen setzen um Probleme mit Leerzeichen und Sonderzeichen zu vermeiden:

    # Gut
    if [ "$variable" = "wert" ]; then
        # ...
    fi
    
    # Problematisch, wenn $variable Leerzeichen enthält
    if [ $variable = "wert" ]; then
        # ...
    fi
  2. Für komplexe String-Manipulationen [[ ]] verwenden:

    # Prüfen, ob ein String mit einem bestimmten Präfix beginnt
    if [[ "$filename" == prefix_* ]]; then
        # ...
    fi
  3. Für Zahlenvergleiche die passenden Operatoren verwenden:

    # Falsch (String-Vergleich)
    if [ "$count" = "5" ]; then
        # ...
    fi
    
    # Richtig (numerischer Vergleich)
    if [ "$count" -eq 5 ]; then
        # ...
    fi
  4. Für komplexe arithmetische Ausdrücke (( )) verwenden:

    if (( count * factor > threshold )); then
        # ...
    fi
  5. Bei der Verwendung von test oder [ ] auch auf leere Variablen achten:

    # Sicherer Vergleich, auch wenn $var leer ist
    if [ -n "$var" ] && [ "$var" = "wert" ]; then
        # ...
    fi

Die richtige Verwendung von Vergleichsoperatoren ist ein wesentlicher Bestandteil jedes Shell-Skripts. Mit dem Verständnis der unterschiedlichen Operatoren für Zahlen- und String-Vergleiche sowie deren korrekte Anwendung können Sie robuste und zuverlässige Bedingungen formulieren, die die Grundlage für fortgeschrittene Shell-Skripte bilden.

7.4 Logische Operatoren: AND, OR, NOT

Logische Operatoren sind unverzichtbare Werkzeuge in der Shell-Programmierung, da sie es ermöglichen, mehrere Bedingungen zu kombinieren oder zu negieren. Mit ihrer Hilfe können komplexe Entscheidungsstrukturen aufgebaut werden, die auf verschiedenen Bedingungen basieren. Die Bash bietet verschiedene Möglichkeiten, diese Operationen durchzuführen, jeweils mit eigenen Syntax- und Anwendungsbereichen.

7.4.1 Grundlegende logische Operatoren

In der Bash gibt es drei grundlegende logische Operatoren:

Operator Bedeutung Verwendung in [ ] Verwendung in [[ ]] Verwendung als Befehlsverbinder
AND Beide Bedingungen müssen wahr sein -a && &&
OR Mindestens eine Bedingung muss wahr sein -o || ||
NOT Negiert eine Bedingung ! ! !

7.4.2 Logische Operatoren in Testbefehlen

7.4.2.1 Innerhalb eines einzelnen [ ]-Tests mit -a und -o

# AND-Verknüpfung mit -a
if [ "$age" -ge 18 -a "$has_id" = "true" ]; then
    echo "Zutritt gewährt."
fi

# OR-Verknüpfung mit -o
if [ "$is_admin" = "true" -o "$is_superuser" = "true" ]; then
    echo "Administrator-Rechte gewährt."
fi

# Negation mit !
if [ ! -f "$config_file" ]; then
    echo "Konfigurationsdatei existiert nicht!"
fi

Hinweis: Die Verwendung von -a und -o innerhalb eines einzelnen [ ]-Tests ist zwar möglich, wird aber wegen potenzieller Mehrdeutigkeiten bei komplexeren Ausdrücken nicht empfohlen. Besser ist die Verwendung separater Tests mit && und ||.

7.4.2.2 Verkettung mehrerer Tests mit && und ||

# AND-Verknüpfung mit &&
if [ "$age" -ge 18 ] && [ "$has_id" = "true" ]; then
    echo "Zutritt gewährt."
fi

# OR-Verknüpfung mit ||
if [ "$is_admin" = "true" ] || [ "$is_superuser" = "true" ]; then
    echo "Administrator-Rechte gewährt."
fi

# Kombination von AND und OR mit expliziter Gruppierung
if [ "$is_weekend" = "true" ] && ([ "$is_admin" = "true" ] || [ "$has_override" = "true" ]); then
    echo "Wochenend-Zugriff gewährt."
fi

7.4.2.3 Verwendung in der erweiterten [[ ]]-Syntax

Die [[ ]]-Syntax bietet eine natürlichere Verwendung logischer Operatoren:

# AND-Verknüpfung
if [[ "$age" -ge 18 && "$has_id" = "true" ]]; then
    echo "Zutritt gewährt."
fi

# OR-Verknüpfung
if [[ "$is_admin" = "true" || "$is_superuser" = "true" ]]; then
    echo "Administrator-Rechte gewährt."
fi

# Negation
if [[ ! -f "$config_file" ]]; then
    echo "Konfigurationsdatei existiert nicht!"
fi

# Komplexere Ausdrücke
if [[ ("$is_weekend" = "true" && "$is_holiday" = "false") || "$emergency" = "true" ]]; then
    echo "Spezialzugang gewährt."
fi

7.4.3 Befehlsverkettung mit logischen Operatoren

Logische Operatoren können auch für die Verkettung von Befehlen verwendet werden:

# Führe command2 nur aus, wenn command1 erfolgreich war (AND)
command1 && command2

# Führe command2 nur aus, wenn command1 nicht erfolgreich war (OR)
command1 || command2

# Führe command2 nur aus, wenn command1 nicht erfolgreich war, dann command3
command1 || command2 && command3

7.4.3.1 Praktisches Beispiel: Installation und Konfiguration

#!/bin/bash

# Prüfen, ob ein Paket installiert ist, andernfalls installieren
dpkg -l | grep -q "apache2" || {
    echo "Apache2 nicht gefunden, installiere..."
    apt-get update && apt-get install -y apache2 || {
        echo "Fehler bei der Installation von Apache2!" >&2
        exit 1
    }
}

# Konfiguration anpassen, wenn die Datei existiert
if [ -f "/etc/apache2/apache2.conf" ]; then
    echo "Konfiguriere Apache2..."
    cp /etc/apache2/apache2.conf /etc/apache2/apache2.conf.bak && \
    sed -i 's/Timeout 300/Timeout 600/' /etc/apache2/apache2.conf && \
    echo "Apache2 erfolgreich konfiguriert."
fi

# Service neustarten, wenn alles erfolgreich war
systemctl restart apache2 && echo "Apache2 neu gestartet." || echo "Fehler beim Neustart von Apache2!"

7.4.4 Kurzschlussauswertung (Short-Circuit Evaluation)

Ein wichtiges Konzept bei logischen Operatoren ist die Kurzschlussauswertung:

Dies kann gezielt zur Steuerung des Programmflusses genutzt werden:

# Verzeichnis erstellen und hineinwechseln (nur wenn Erstellung erfolgreich)
mkdir -p /tmp/workdir && cd /tmp/workdir

# Fallback bei Fehler
ping -c 1 server.example.com || echo "Server nicht erreichbar!"

# Kombination für Fehlerbehandlung
cp wichtige_datei.txt /backup/ || {
    echo "Backup fehlgeschlagen!" >&2
    send_alert_email
    exit 1
}

7.4.5 Fortgeschrittene Anwendungsfälle

7.4.5.1 Kombinierte Bedingungen in komplexen Skripten

#!/bin/bash

# Prüfungen vor einem Systemupdate
DISK_SPACE=$(df -k / | awk 'NR==2 {print $4}')
RAM_FREE=$(free -m | awk 'NR==2 {print $4}')
NETWORK=$(ping -c 1 updates.example.com >/dev/null && echo "online" || echo "offline")
BATTERY=$(acpi -b 2>/dev/null | grep -q "Discharging" && echo "discharging" || echo "charging")

# Komplexe Bedingungsprüfung
if [[ "$DISK_SPACE" -gt 1048576 && "$RAM_FREE" -gt 512 && "$NETWORK" = "online" ]] && \
   [[ "$BATTERY" = "charging" || $(id -u) -eq 0 ]]; then
    echo "Alle Bedingungen für ein Update sind erfüllt."
    apt-get update && apt-get upgrade -y
else
    echo "Update abgebrochen. Nicht alle Bedingungen erfüllt:"
    [ "$DISK_SPACE" -le 1048576 ] && echo "- Zu wenig Festplattenplatz"
    [ "$RAM_FREE" -le 512 ] && echo "- Zu wenig freier Arbeitsspeicher"
    [ "$NETWORK" != "online" ] && echo "- Keine Netzwerkverbindung"
    [ "$BATTERY" = "discharging" ] && [ "$(id -u)" -ne 0 ] && \
        echo "- Batterie wird entladen und keine Root-Rechte"
fi

7.4.5.2 Verwendung in Schleifen mit bedingtem Abbruch

#!/bin/bash

# Verarbeite Dateien, aber brich ab, wenn ein Fehler auftritt oder nach 10 Dateien
COUNT=0
MAX_FILES=10
ERROR=false

for file in *.txt; do
    # Abbruch, wenn MAX_FILES erreicht oder ERROR gesetzt
    [[ "$COUNT" -ge "$MAX_FILES" || "$ERROR" = "true" ]] && break
    
    echo "Verarbeite $file..."
    
    # Fehler-Flag setzen bei Problemen
    process_file "$file" || ERROR=true
    
    # Zähler erhöhen
    ((COUNT++))
done

# Ausgabe nach Schleifenende
if [ "$ERROR" = "true" ]; then
    echo "Verarbeitung wegen eines Fehlers abgebrochen."
elif [ "$COUNT" -ge "$MAX_FILES" ]; then
    echo "Maximale Anzahl an Dateien ($MAX_FILES) verarbeitet."
else
    echo "Alle Dateien erfolgreich verarbeitet."
fi

7.4.6 Best Practices für logische Operatoren

  1. Klare Strukturierung bei komplexen Bedingungen:

    # Besser lesbar mit Einrückung und Zeilenumbrüchen
    if [[ "$condition1" = true && 
          "$condition2" = true ]] || 
       [[ "$override" = true ]]; then
        # ...
    fi
  2. Verwendung von Klammern für explizite Gruppierung:

    # Ohne Klammern: möglicherweise unerwartete Auswertungsreihenfolge
    if [ "$a" = true ] && [ "$b" = true ] || [ "$c" = true ]; then
        # ...
    fi
    
    # Mit Klammern: klare Gruppierung
    if ([ "$a" = true ] && [ "$b" = true ]) || [ "$c" = true ]; then
        # ...
    fi
  3. Bevorzugung von [[ ]] für komplexe Bedingungen:

    # Besser: Verwenden von [[ ]] für komplexe Bedingungen
    if [[ "$string" == *pattern* && "$number" -gt 10 ]]; then
        # ...
    fi
  4. Vorsicht bei der Mischung verschiedener logischer Stile:

    # Vermeiden: Mischung von -a/-o und &&/||
    if [ "$a" = true -a "$b" = true ] && [ "$c" = true ]; then
        # ...
    fi
    
    # Besser: Konsistente Verwendung eines Stils
    if [[ "$a" = true && "$b" = true && "$c" = true ]]; then
        # ...
    fi
  5. Explizite Tests auf boolesche Werte:

    # Kürzer, aber weniger deutlich
    if [ "$success" ]; then
        # ...
    fi
    
    # Expliziter und klarer
    if [ "$success" = true ]; then
        # ...
    fi

7.4.7 Häufige Fallstricke

  1. Fehlerhafte Priorität bei logischen Operatoren:

    # Möglicherweise unerwartetes Verhalten
    if [ "$a" = true ] || [ "$b" = true ] && [ "$c" = true ]; then
        # Dies wird ausgeführt, wenn $a wahr ist, ODER wenn beide $b und $c wahr sind
    fi
  2. Syntaxfehler bei Verwendung von -a und -o in separaten Tests:

    # Fehler: -a nur innerhalb eines einzelnen [ ]-Tests gültig
    if [ "$a" = true ] -a [ "$b" = true ]; then
        # ...
    fi
    
    # Korrekt
    if [ "$a" = true -a "$b" = true ]; then
        # ...
    fi
    
    # Oder besser
    if [ "$a" = true ] && [ "$b" = true ]; then
        # ...
    fi
  3. Unbeabsichtigte Ausführung bei Befehlsverkettung:

    # Vorsicht: Bei Fehler in command1 wird command2 ausgeführt
    command1 || command2
    
    # Bei einer Gruppe von Befehlen als Fehlerbehandlung Klammern verwenden
    command1 || {
        echo "Fehler in command1"
        command2
        exit 1
    }

Die richtige Anwendung logischer Operatoren ist entscheidend für die Entwicklung zuverlässiger Shell-Skripte. Durch das Verständnis der verschiedenen Syntaxformen und deren gezielten Einsatz können selbst komplexe Entscheidungsstrukturen klar und wartbar umgesetzt werden.

7.5 Schleifen: for, while, until

Schleifen sind unerlässliche Bausteine in der Shell-Programmierung, die es ermöglichen, Befehle oder Codeblöcke wiederholt auszuführen. Die Bash bietet drei Haupttypen von Schleifen: for, while und until. Jede dieser Schleifenstrukturen hat eigene Charakteristika und eignet sich für unterschiedliche Anwendungsfälle.

7.5.1 Die for-Schleife

Die for-Schleife ist vermutlich die am häufigsten verwendete Schleifenstruktur in Shell-Skripten. Sie iteriert über eine Liste von Werten und führt für jeden Wert einen Codeblock aus.

7.5.1.1 Grundsyntax der for-Schleife

for VARIABLE in WERTE; do
    BEFEHLE
done

7.5.1.2 Beispiele für for-Schleifen

Iteration über eine explizite Liste:

#!/bin/bash

# Über eine explizite Liste von Werten iterieren
for name in Alice Bob Charlie Dave; do
    echo "Hallo, $name!"
done

Iteration über Dateien mit Globbing (Wildcards):

#!/bin/bash

# Über alle .txt-Dateien im aktuellen Verzeichnis iterieren
for file in *.txt; do
    # Prüfen, ob Dateien existieren (vermeidet Probleme, wenn keine .txt-Dateien vorhanden sind)
    if [ -f "$file" ]; then
        echo "Verarbeite Datei: $file"
        # Weitere Verarbeitung hier...
    fi
done

Iteration über Befehlsausgabe:

#!/bin/bash

# Über die Ausgabe eines Befehls iterieren (Liste aktiver Benutzer)
for user in $(who | cut -d' ' -f1 | sort -u); do
    echo "Benutzer $user ist angemeldet."
done

Traditionelle numerische Schleife:

#!/bin/bash

# Zählen von 1 bis 10
for i in {1..10}; do
    echo "Zahl: $i"
done

# Erweiterte Syntax mit Schrittweite (Bash 4.0+)
for i in {0..20..5}; do  # 0, 5, 10, 15, 20
    echo "Zahl mit Schritt: $i"
done

C-ähnliche for-Schleife:

#!/bin/bash

# C-ähnliche Syntax für mehr Flexibilität
for ((i=0; i<5; i++)); do
    echo "Index: $i"
done

7.5.2 Die while-Schleife

Die while-Schleife führt einen Codeblock aus, solange eine bestimmte Bedingung wahr (true) ist.

7.5.2.1 Grundsyntax der while-Schleife

while BEDINGUNG; do
    BEFEHLE
done

7.5.2.2 Beispiele für while-Schleifen

Einfache Zählschleife:

#!/bin/bash

# Zählen von 1 bis 5
count=1
while [ $count -le 5 ]; do
    echo "Zähler: $count"
    ((count++))
done

Dateieingabe zeilenweise lesen:

#!/bin/bash

# Eine Datei zeilenweise lesen
while IFS= read -r line; do
    echo "Gelesen: $line"
done < "input.txt"

Warten, bis eine Bedingung erfüllt ist:

#!/bin/bash

# Warten, bis ein Prozess beendet ist
while pgrep -x "apt-get" > /dev/null; do
    echo "Warte auf Abschluss von apt-get..."
    sleep 2
done
echo "Prozess beendet, fahre fort..."

Endlosschleife mit expliziter Abbruchbedingung:

#!/bin/bash

# Endlosschleife (mit break zum Abbruch)
while true; do
    echo "Geben Sie einen Befehl ein (oder 'exit' zum Beenden):"
    read cmd
    
    if [ "$cmd" = "exit" ]; then
        break
    fi
    
    # Ausführen des eingegebenen Befehls
    eval "$cmd"
done

7.5.3 Die until-Schleife

Die until-Schleife ist das logische Gegenstück zur while-Schleife. Sie führt einen Codeblock aus, bis eine bestimmte Bedingung wahr wird (also solange die Bedingung falsch ist).

7.5.3.1 Grundsyntax der until-Schleife

until BEDINGUNG; do
    BEFEHLE
done

7.5.3.2 Beispiele für until-Schleifen

Warten auf Verfügbarkeit eines Dienstes:

#!/bin/bash

# Warten, bis ein Webserver verfügbar ist
until curl -s --head http://localhost:8080 | grep "200 OK" > /dev/null; do
    echo "Warte auf Webserver..."
    sleep 5
done
echo "Webserver ist jetzt verfügbar!"

Countdown:

#!/bin/bash

# Countdown von 10 auf 0
counter=10
until [ $counter -lt 0 ]; do
    echo "Countdown: $counter"
    ((counter--))
    sleep 1
done
echo "Start!"

Wiederholungsversuche mit Begrenzung:

#!/bin/bash

# Versuchen, eine Verbindung herzustellen, mit begrenzter Anzahl an Versuchen
attempts=0
max_attempts=5

until ping -c 1 -W 1 example.com > /dev/null || [ $attempts -ge $max_attempts ]; do
    attempts=$((attempts + 1))
    echo "Verbindungsversuch $attempts von $max_attempts fehlgeschlagen, versuche erneut..."
    sleep 2
done

if [ $attempts -lt $max_attempts ]; then
    echo "Verbindung erfolgreich hergestellt."
else
    echo "Maximale Anzahl an Versuchen erreicht. Konnte keine Verbindung herstellen."
    exit 1
fi

7.5.4 Vergleich der Schleifentypen

Schleifentyp Verwendungszweck Besonderheiten
for Iteration über bekannte Listen von Elementen Sehr vielseitig, einfach zu verwenden für Listen, Bereiche, Dateien
while Ausführung, solange eine Bedingung wahr ist Gut für unbekannte Anzahl von Iterationen, Dateioperationen, interaktive Schleifen
until Ausführung, bis eine Bedingung wahr wird Praktisch für Warten auf Ereignisse, Timeouts, semantisch manchmal klarer als while mit negierter Bedingung

7.5.5 Fortgeschrittene Techniken

7.5.5.1 Verschachtelte Schleifen

Schleifen können auch ineinander verschachtelt werden, was für komplexere Algorithmen nützlich ist:

#!/bin/bash

# Generieren einer Multiplikationstabelle
for i in {1..5}; do
    for j in {1..5}; do
        product=$((i * j))
        printf "%3d " "$product"
    done
    echo # Zeilenumbruch nach jeder inneren Schleife
done

7.5.5.2 Dynamische Schleifenbereiche

#!/bin/bash

# Dynamische Bereiche basierend auf Variablen oder Befehlen
start=5
end=$(wc -l < input.txt)

for ((i=start; i<=end; i++)); do
    echo "Verarbeite Zeile $i"
    # Verarbeitung hier...
done

7.5.5.3 Parallele Bearbeitung mit Prozessbegrenzung

#!/bin/bash

# Parallele Ausführung von Aufgaben mit maximaler Anzahl gleichzeitiger Prozesse
MAX_PROCS=4
count=0

for job in job1 job2 job3 job4 job5 job6 job7 job8; do
    # Starte Hintergrundprozess
    (
        echo "Starte Job: $job"
        sleep $((RANDOM % 5 + 1))  # Simuliere Arbeit
        echo "Job $job abgeschlossen"
    ) &
    
    # Zähle laufende Prozesse
    ((count++))
    
    # Wenn maximale Anzahl erreicht ist, warte auf Beendigung eines Prozesses
    if [ $count -ge $MAX_PROCS ]; then
        wait -n  # Warte auf Beendigung eines Hintergrundprozesses
        ((count--))
    fi
done

# Warte auf alle verbleibenden Hintergrundprozesse
wait
echo "Alle Jobs abgeschlossen"

7.5.6 Best Practices für Schleifen

  1. Variablen in Anführungszeichen setzen, insbesondere bei Dateinamen:

    for file in *.txt; do
        echo "Verarbeite \"$file\""  # Anführungszeichen verhindern Probleme mit Leerzeichen
    done
  2. IFS (Internal Field Separator) für zeilenweise Verarbeitung anpassen:

    # Sichere Methode zum Lesen von Zeilen mit Leerzeichen
    while IFS= read -r line; do
        echo "Zeile: $line"
    done < input.txt
  3. Vermeiden von Subshells bei Dateieingabe, wenn möglich:

    # Besser: Direkte Eingabeumleitung
    while read -r line; do
        process_line "$line"
    done < input.txt
    
    # Vermeiden (erzeugt Subshell, Variablen gehen außerhalb verloren):
    cat input.txt | while read -r line; do
        process_line "$line"
    done
  4. Prüfen, ob Globbing-Muster Ergebnisse liefert:

    # Verhindert Fehler, wenn keine .log-Dateien existieren
    shopt -s nullglob  # Leere Liste statt des Musters, wenn keine Übereinstimmung
    for logfile in *.log; do
        if [ -f "$logfile" ]; then  # Zusätzliche Prüfung für Sicherheit
            process_log "$logfile"
        fi
    done
  5. Fortschrittsanzeige bei langen Schleifen:

    total=$(find . -type f | wc -l)
    current=0
    
    find . -type f | while read -r file; do
        ((current++))
        printf "\rVerarbeite Datei %d von %d (%d%%)" "$current" "$total" "$((current * 100 / total))"
        process_file "$file"
    done
    echo # Abschließender Zeilenumbruch

7.5.7 Fallstricke und deren Vermeidung

  1. Subshell-Problem bei Pipelines:

    # Problem: count bleibt 0, da die Schleife in einer Subshell läuft
    count=0
    cat data.txt | while read -r line; do
        ((count++))
    done
    echo "Zeilen: $count"  # Wird immer 0 ausgeben!
    
    # Lösung: Prozesssubstitution oder direkte Eingabeumleitung
    count=0
    while read -r line; do
        ((count++))
    done < data.txt
    echo "Zeilen: $count"  # Korrektes Ergebnis
  2. Unerwartetes Globbing-Verhalten:

    # Problem: Wenn keine .txt-Dateien existieren, wird "*.txt" wörtlich verwendet
    for file in *.txt; do
        echo "Datei: $file"  # Könnte "Datei: *.txt" ausgeben!
    done
    
    # Lösung:
    shopt -s nullglob  # Leere Liste bei keiner Übereinstimmung
    for file in *.txt; do
        echo "Datei: $file"
    done
  3. Endlosschleifen vermeiden:

    # Potenzielles Problem: Endlosschleife, wenn Bedingung nie falsch wird
    while [ true ]; do
        # Sicherstellen, dass mindestens eine Abbruchbedingung existiert
        if [ "$condition" = true ]; then
            break
        fi
    
        # Oder einen Timeout einbauen
        ((iterations++))
        if [ $iterations -gt $MAX_ITERATIONS ]; then
            echo "Maximale Anzahl an Iterationen erreicht, breche ab."
            break
        fi
    done
  4. Behandlung leerer Listen:

    # Problem: Schleife wird einmal ausgeführt, auch wenn die Liste leer ist
    result=$(some_command)  # Könnte leer sein
    for item in $result; do
        echo "Item: $item"  # Wird "Item: " ausgeben, wenn $result leer ist!
    done
    
    # Lösung:
    result=$(some_command)
    if [ -n "$result" ]; then
        for item in $result; do
            echo "Item: $item"
        done
    else
        echo "Keine Elemente zum Verarbeiten."
    fi

Schleifen sind ein mächtiges Werkzeug in der Shell-Programmierung und ermöglichen die effiziente Automatisierung repetitiver Aufgaben. Mit dem Verständnis der verschiedenen Schleifentypen und ihrer Anwendungsbereiche können Sie elegante und effiziente Skripte erstellen, die selbst komplexe Automatisierungsaufgaben bewältigen.

7.6 Schleifenkontrolle mit break und continue

Während Schleifen einen Codeblock wiederholen, ist es oft notwendig, die normale Schleifenausführung zu unterbrechen oder bestimmte Iterationen zu überspringen. Die Bash bietet hierfür zwei wichtige Befehle: break und continue. Diese Befehle ermöglichen eine präzise Kontrolle über den Programmfluss innerhalb von Schleifen und sind besonders bei komplexen Szenarien unverzichtbar.

7.6.1 Der break-Befehl

Der break-Befehl beendet die Ausführung einer Schleife sofort und setzt den Programmfluss nach der Schleife fort. Er ist besonders nützlich, wenn eine Bedingung erreicht wird, die das weitere Durchlaufen der Schleife überflüssig macht.

7.6.1.1 Grundlegende Verwendung von break

#!/bin/bash

# Suche nach einer bestimmten Datei und breche ab, sobald sie gefunden wird
for file in *; do
    if [ "$file" = "config.ini" ]; then
        echo "Datei gefunden: $file"
        break  # Schleife beenden, sobald die Datei gefunden wurde
    fi
done
echo "Suche beendet."

7.6.1.2 Verwendung mit while-Schleifen

#!/bin/bash

# Interaktive Eingabeschleife, die bei 'exit' beendet wird
while true; do
    echo -n "Geben Sie einen Befehl ein (oder 'exit' zum Beenden): "
    read command
    
    if [ "$command" = "exit" ]; then
        echo "Beende Programm..."
        break
    fi
    
    echo "Führe aus: $command"
    eval "$command"
done

7.6.1.3 Vorzeitiger Abbruch bei Fehlern

#!/bin/bash

# Verarbeite mehrere Dateien, breche bei Fehler ab
for file in *.csv; do
    echo "Verarbeite $file..."
    
    if ! grep -q "Header" "$file"; then
        echo "Fehler: Ungültiges Dateiformat in $file. Header nicht gefunden."
        echo "Abbruch der Verarbeitung."
        break
    fi
    
    # Verarbeitung fortsetzen...
    echo "$file erfolgreich verarbeitet."
done

7.6.2 Der continue-Befehl

Der continue-Befehl überspringt den Rest des aktuellen Schleifendurchlaufs und beginnt sofort mit der nächsten Iteration. Dies ist nützlich, wenn bestimmte Elemente übersprungen werden sollen, ohne die gesamte Schleife zu beenden.

7.6.2.1 Grundlegende Verwendung von continue

#!/bin/bash

# Verarbeite alle Textdateien, überspringe temporäre Dateien
for file in *.txt; do
    # Überspringe Dateien, die mit 'temp_' beginnen
    if [[ "$file" == temp_* ]]; then
        echo "Überspringe temporäre Datei: $file"
        continue
    fi
    
    echo "Verarbeite Datei: $file"
    # Weitere Verarbeitung hier...
done

7.6.2.2 Fehlerbehandlung mit continue

#!/bin/bash

# Verarbeite mehrere Log-Dateien, überspringe defekte
for logfile in /var/log/*.log; do
    # Überprüfe, ob die Datei lesbar ist
    if [ ! -r "$logfile" ]; then
        echo "Warnung: Keine Leseberechtigung für $logfile, überspringe..."
        continue
    fi
    
    # Überprüfe, ob die Datei leer ist
    if [ ! -s "$logfile" ]; then
        echo "Hinweis: $logfile ist leer, überspringe..."
        continue
    fi
    
    echo "Analysiere $logfile..."
    # Analyse durchführen...
done

7.6.2.3 Filtern von Datensätzen

#!/bin/bash

# Verarbeite Benutzerliste und filtere bestimmte Benutzer
while IFS=: read -r username password uid gid info home shell; do
    # Überspringe System-Benutzer (UID < 1000)
    if [ "$uid" -lt 1000 ]; then
        continue
    fi
    
    # Überspringe Benutzer ohne Login-Shell
    if [ "$shell" = "/usr/sbin/nologin" ] || [ "$shell" = "/bin/false" ]; then
        continue
    fi
    
    echo "Regulärer Benutzer: $username (UID: $uid, Home: $home)"
done < /etc/passwd

7.6.3 break und continue mit verschachtelten Schleifen

Bei verschachtelten Schleifen beenden break und continue standardmäßig nur die innerste Schleife. Mit einem numerischen Argument kann man jedoch steuern, wie viele Schleifenebenen betroffen sind.

7.6.3.1 break mit mehreren Schleifenebenen

#!/bin/bash

# Verschachtelte Schleifen mit mehrstufigem break
for directory in /home/*/; do
    echo "Durchsuche Verzeichnis: $directory"
    
    for file in "$directory"*.conf; do
        if [ -f "$file" ] && grep -q "CRITICAL_ERROR" "$file"; then
            echo "Kritischer Fehler in $file gefunden!"
            echo "Breche die gesamte Suche ab."
            break 2  # Beende sowohl die innere als auch die äußere Schleife
        fi
    done
    
    echo "Verzeichnis $directory durchsucht."
done

echo "Durchsuchung abgeschlossen."

7.6.3.2 continue mit mehreren Schleifenebenen

#!/bin/bash

# Verschachtelte Schleifen mit mehrstufigem continue
for year in {2020..2023}; do
    echo "Verarbeite Daten für Jahr $year"
    
    for month in {01..12}; do
        # Überspringe alle Monate des Jahres 2022 (springe zur nächsten Jahr-Iteration)
        if [ "$year" -eq 2022 ]; then
            echo "Jahr 2022 wird übersprungen."
            continue 2
        fi
        
        echo "  Verarbeite Monat $month/$year"
        # Verarbeitung hier...
    done
    
    echo "Jahr $year abgeschlossen."
done

7.6.4 Praktische Anwendungsfälle

7.6.4.1 Dateisuche mit Begrenzung

#!/bin/bash

# Suche nach Dateien mit bestimmtem Muster, begrenzt auf 5 Treffer
found=0
max_results=5

find /var/log -type f -name "*.log" | while read -r file; do
    if grep -q "ERROR" "$file"; then
        echo "Fehler gefunden in: $file"
        ((found++))
        
        if [ "$found" -ge "$max_results" ]; then
            echo "Maximale Anzahl an Ergebnissen erreicht."
            break
        fi
    fi
done

7.6.4.2 Batch-Verarbeitung mit Fehlertoleranz

#!/bin/bash

# Verarbeite mehrere Dateien, überspringe problematische, aber führe fort
success=0
failed=0

for file in data/*.csv; do
    echo "Verarbeite $file..."
    
    # Überprüfe Dateiformat
    if ! head -1 "$file" | grep -q "^ID,Name,Date,Value$"; then
        echo "Warnung: $file hat ein ungültiges Format, überspringe..."
        ((failed++))
        continue
    fi
    
    # Versuche Daten zu importieren
    if ! import_data "$file"; then
        echo "Fehler beim Import von $file, überspringe..."
        ((failed++))
        continue
    fi
    
    echo "$file erfolgreich verarbeitet."
    ((success++))
done

echo "Verarbeitung abgeschlossen: $success erfolgreich, $failed fehlgeschlagen."

7.6.4.3 Implementierung eines Timeouts

#!/bin/bash

# Warte auf einen Prozess mit Timeout
pid=$1
timeout=60
elapsed=0

while kill -0 "$pid" 2>/dev/null; do
    if [ "$elapsed" -ge "$timeout" ]; then
        echo "Timeout erreicht ($timeout Sekunden). Prozess läuft noch."
        echo "Breche Warteschleife ab."
        break
    fi
    
    echo "Warte auf Prozess $pid... ($elapsed/$timeout s)"
    sleep 5
    elapsed=$((elapsed + 5))
done

if [ "$elapsed" -lt "$timeout" ]; then
    echo "Prozess $pid wurde innerhalb des Timeouts beendet."
fi

7.6.5 Fortgeschrittene Techniken

7.6.5.1 Kombination mit Signalbehandlung

#!/bin/bash

# Kontrollierte Schleifenbeendigung durch Signale
cleanup() {
    echo "Signal empfangen, beende Schleife sauber..."
    break_loop=true
}

# Fange SIGINT (Ctrl+C) und SIGTERM
trap cleanup SIGINT SIGTERM

break_loop=false
while [ "$break_loop" = false ]; do
    echo "Verarbeite..."
    sleep 1
    
    # Hier könnte ein anderer Grund sein, die Schleife zu beenden
    if [ -f "stop_processing" ]; then
        echo "Stopp-Datei gefunden."
        break
    fi
done

echo "Schleife beendet, führe Aufräumarbeiten durch..."
# Aufräumarbeiten hier...

7.6.5.2 Dynamische Entscheidungen basierend auf Statistiken

#!/bin/bash

# Verarbeite Daten, passe Verhalten basierend auf Erfolgsrate an
total=0
success=0
error_threshold=0.3  # 30% Fehlerrate

for file in input/*.dat; do
    ((total++))
    
    if process_file "$file"; then
        ((success++))
    else
        # Berechne aktuelle Fehlerrate
        error_rate=$(echo "scale=2; ($total-$success)/$total" | bc)
        
        echo "Aktuelle Fehlerrate: $error_rate (Schwellwert: $error_threshold)"
        
        # Wenn Fehlerrate zu hoch, breche ab
        if (( $(echo "$error_rate > $error_threshold" | bc -l) )); then
            echo "Fehlerrate überschreitet Schwellwert. Abbruch der Verarbeitung."
            break
        fi
    fi
    
    # Nach jeweils 10 Dateien Status ausgeben
    if [ $((total % 10)) -eq 0 ]; then
        echo "Fortschritt: $total Dateien verarbeitet, Erfolgsrate: $(echo "scale=2; $success/$total" | bc)"
    fi
done

7.6.6 Best Practices für break und continue

  1. Klar dokumentieren, warum eine Schleife vorzeitig beendet wird:

    for file in "$directory"/*; do
        # Breche ab, wenn eine bestimmte Bedingung erfüllt ist
        if [ condition ]; then
            echo "Abbruch wegen: <spezifischer Grund>"
            break
        fi
    done
  2. Verwende continue für erwartete Ausnahmen, break für unerwartete Zustände:

    while read -r line; do
        # Überspringe Kommentarzeilen (erwartete Ausnahme)
        if [[ "$line" == \#* ]]; then
            continue
        fi
    
        # Breche bei schwerwiegendem Fehler ab (unerwarteter Zustand)
        if ! validate_line "$line"; then
            echo "Ungültiges Datenformat. Abbruch." >&2
            break
        fi
    done < "$input_file"
  3. Vermeide zu viele break/continue-Anweisungen in einer Schleife:

    # Besser: Logikstruktur verbessern statt viele continue-Anweisungen
    for item in "${items[@]}"; do
        # Kombinierte Bedingung statt mehrerer continue
        if [[ "$item" == valid_* && -f "$item" && ! -L "$item" ]]; then
            process_item "$item"
        fi
    done
  4. Vorsicht bei break/continue in Funktionen innerhalb von Schleifen:

    # Break in einer Funktion bricht nur die Schleife in der Funktion ab,
    # nicht die aufrufende Schleife
    process_item() {
        local item="$1"
        for part in $item; do
            # Dieses break beendet nur die innere Schleife
            if [ condition ]; then break; fi
        done
    }
    
    for item in "${items[@]}"; do
        process_item "$item"
        # Die äußere Schleife wird nicht durch break in process_item beendet
    done
  5. Klar definierte Exit-Strategien für komplexe Schleifen:

    # Klare Bedingungen für Schleifenabbruch definieren
    max_iterations=100
    iterations=0
    
    while true; do
        ((iterations++))
    
        # Verschiedene Exit-Bedingungen
        if [ "$iterations" -gt "$max_iterations" ]; then
            echo "Maximale Anzahl an Iterationen erreicht."
            break
        fi
    
        if [ -f "abort_flag" ]; then
            echo "Externe Abbruch-Flagge gefunden."
            break
        fi
    
        if ! process_step "$iterations"; then
            echo "Verarbeitungsfehler aufgetreten."
            break
        fi
    done

7.6.7 Häufige Fallstricke und deren Vermeidung

  1. Vergessen von break bei true-while-Schleifen:

    # Problem: Keine Abbruchbedingung
    while true; do
        # Wenn hier kein break ist, entsteht eine Endlosschleife
    done
    
    # Lösung: Immer eine Abbruchbedingung einbauen
    while true; do
        # Verarbeitung
        if [ condition ]; then
            break
        fi
    done
  2. Falsche Schleifenebene bei verschachtelten Schleifen:

    # Problem: break beendet nur die innere Schleife
    for i in {1..10}; do
        for j in {1..10}; do
            if [ condition ]; then
                break  # Beendet nur die innere j-Schleife
            fi
        done
        # Ausführung geht hier weiter, auch wenn condition in der inneren Schleife true war
    done
    
    # Lösung: Verwende break 2 oder eine Flag-Variable
    found=false
    for i in {1..10}; do
        for j in {1..10}; do
            if [ condition ]; then
                found=true
                break  # Beendet die innere Schleife
            fi
        done
        # Prüfe Flag-Variable für äußere Schleife
        if [ "$found" = true ]; then
            break
        fi
    done
  3. continue in der letzten Anweisung einer Schleife:

    # Überflüssiges continue am Ende einer Schleife
    for item in "${items[@]}"; do
        process_item "$item"
        continue  # Unnötig, da die Schleife ohnehin zur nächsten Iteration übergehen würde
    done
    
    # Besser: Einfach weglassen
    for item in "${items[@]}"; do
        process_item "$item"
    done
  4. Übersehen des Kontextwechsels bei Pipelines:

    # Problem: break in einer Pipeline wirkt nur auf die Subshell
    found=0
    cat file.txt | while read -r line; do
        ((found++))
        if [ "$found" -eq 10 ]; then
            break  # Beendet nur die Schleife in der Subshell
        fi
    done
    echo "Gefunden: $found"  # Wird immer 0 ausgeben!
    
    # Lösung: Direkte Eingabeumleitung verwenden
    found=0
    while read -r line; do
        ((found++))
        if [ "$found" -eq 10 ]; then
            break  # Beendet die Schleife in der aktuellen Shell
        fi
    done < file.txt
    echo "Gefunden: $found"  # Gibt korrekt die Anzahl aus

Die Befehle break und continue sind mächtige Werkzeuge zur Steuerung des Programmflusses in Schleifen. Mit ihnen können Sie Ihre Shell-Skripte effizienter gestalten und präzise auf verschiedene Bedingungen reagieren. Besonders in komplexen Skripten mit verschachtelten Schleifen oder umfangreicher Fehlerbehandlung sind diese Kontrollmechanismen unverzichtbar.

7.7 Der case-Befehl für Mehrfachverzweigungen

Während if-Anweisungen für einfache bedingte Ausführungen geeignet sind, werden sie bei mehreren Verzweigungen schnell unübersichtlich. Die Bash bietet mit dem case-Befehl eine elegante Alternative für Situationen, in denen ein Wert mit verschiedenen Mustern verglichen werden soll. Der case-Befehl ist besonders nützlich für Menüsysteme, die Verarbeitung von Befehlszeilenparametern und für Skripte, die unterschiedliche Aktionen basierend auf Benutzereingaben ausführen.

7.7.1 Grundsyntax des case-Befehls

case WERT in
    MUSTER1)
        BEFEHLE1
        ;;
    MUSTER2)
        BEFEHLE2
        ;;
    MUSTER3 | MUSTER4)  # Mehrere Muster mit | kombinieren
        BEFEHLE3
        ;;
    *)  # Standardfall (ähnlich wie else)
        STANDARD_BEFEHLE
        ;;
esac

Der case-Befehl vergleicht den angegebenen WERT nacheinander mit jedem MUSTER. Wenn ein Muster übereinstimmt, werden die zugehörigen Befehle ausgeführt, und die Ausführung wird nach dem entsprechenden ;; fortgesetzt. Das Schlüsselwort esac (umgekehrtes “case”) schließt den case-Block ab.

7.7.2 Einfaches Beispiel: Auswertung eines Kommandozeilenparameters

#!/bin/bash

# Auswertung des ersten Parameters
case "$1" in
    start)
        echo "Starte den Dienst..."
        service myservice start
        ;;
    stop)
        echo "Stoppe den Dienst..."
        service myservice stop
        ;;
    restart)
        echo "Starte den Dienst neu..."
        service myservice restart
        ;;
    status)
        echo "Status des Dienstes:"
        service myservice status
        ;;
    *)
        echo "Verwendung: $0 {start|stop|restart|status}"
        exit 1
        ;;
esac

7.7.3 Pattern Matching in case

Der case-Befehl unterstützt verschiedene Arten von Mustern:

  1. Einfache Zeichenketten:

    case "$input" in
        yes)
            # Wenn $input genau "yes" ist
            ;;
    esac
  2. Wildcards und Globbing:

    case "$filename" in
        *.jpg | *.jpeg)
            # Wenn $filename auf .jpg oder .jpeg endet
            echo "JPEG-Bild gefunden"
            ;;
        *.png)
            # Wenn $filename auf .png endet
            echo "PNG-Bild gefunden"
            ;;
        *.txt)
            # Wenn $filename auf .txt endet
            echo "Textdatei gefunden"
            ;;
    esac
  3. Zeichenklassen:

    case "$key" in
        [yY] | [yY][eE][sS])
            # Wenn $key "y", "Y", "yes", "YES", "Yes", etc. ist
            echo "Sie haben zugestimmt"
            ;;
        [nN] | [nN][oO])
            # Wenn $key "n", "N", "no", "NO", "No", etc. ist
            echo "Sie haben abgelehnt"
            ;;
    esac
  4. Bereichsmuster:

    case "$char" in
        [a-z])
            echo "Kleinbuchstabe"
            ;;
        [A-Z])
            echo "Großbuchstabe"
            ;;
        [0-9])
            echo "Ziffer"
            ;;
    esac

7.7.4 Fortgeschrittene Beispiele

7.7.4.1 Interaktives Menüsystem

#!/bin/bash

while true; do
    echo -e "\nSystem-Administration"
    echo "======================="
    echo "1. Systemstatus anzeigen"
    echo "2. Dienste verwalten"
    echo "3. Benutzer verwalten"
    echo "4. Backup erstellen"
    echo "0. Beenden"
    
    echo -n "Wählen Sie eine Option (0-4): "
    read choice
    
    case "$choice" in
        1)
            echo -e "\nSystemstatus:"
            echo "-------------"
            uptime
            df -h
            free -m
            ;;
        2)
            echo -e "\nDienste:"
            echo "--------"
            echo "a) Apache starten"
            echo "b) Apache stoppen"
            echo "c) MySQL starten"
            echo "d) MySQL stoppen"
            echo -n "Wählen Sie einen Dienst (a-d): "
            read service_choice
            
            case "$service_choice" in
                [aA])
                    echo "Starte Apache..."
                    systemctl start apache2
                    ;;
                [bB])
                    echo "Stoppe Apache..."
                    systemctl stop apache2
                    ;;
                [cC])
                    echo "Starte MySQL..."
                    systemctl start mysql
                    ;;
                [dD])
                    echo "Stoppe MySQL..."
                    systemctl stop mysql
                    ;;
                *)
                    echo "Ungültige Auswahl!"
                    ;;
            esac
            ;;
        3)
            echo -e "\nBenutzerverwaltung:"
            echo "------------------"
            # Benutzerverwaltung-Code hier...
            ;;
        4)
            echo -e "\nBackup erstellen:"
            echo "----------------"
            # Backup-Code hier...
            ;;
        0)
            echo "Programm wird beendet."
            exit 0
            ;;
        *)
            echo "Ungültige Auswahl! Bitte 0-4 eingeben."
            ;;
    esac
    
    echo -e "\nDrücken Sie Enter, um fortzufahren..."
    read
done

7.7.4.2 Verarbeitung von Dateitypen

#!/bin/bash

# Verarbeitet eine Liste von Dateien basierend auf ihren Erweiterungen
process_files() {
    for file in "$@"; do
        if [ ! -f "$file" ]; then
            echo "Warnung: $file ist keine Datei oder existiert nicht."
            continue
        fi
        
        # Erweiterung extrahieren
        ext="${file##*.}"
        
        case "${ext,,}" in  # ${ext,,} konvertiert zu Kleinbuchstaben
            jpg|jpeg|png|gif|svg)
                echo "Verarbeite Bild: $file"
                process_image "$file"
                ;;
            mp3|wav|flac|ogg)
                echo "Verarbeite Audio: $file"
                process_audio "$file"
                ;;
            mp4|avi|mkv|mov)
                echo "Verarbeite Video: $file"
                process_video "$file"
                ;;
            txt|md|csv)
                echo "Verarbeite Text: $file"
                process_text "$file"
                ;;
            pdf|doc|docx|odt)
                echo "Verarbeite Dokument: $file"
                process_document "$file"
                ;;
            *)
                echo "Unbekannter Dateityp für $file"
                ;;
        esac
    done
}

# Dummy-Funktionen für die Verarbeitung
process_image() { echo "  Bild $1 verarbeitet"; }
process_audio() { echo "  Audio $1 verarbeitet"; }
process_video() { echo "  Video $1 verarbeitet"; }
process_text() { echo "  Text $1 verarbeitet"; }
process_document() { echo "  Dokument $1 verarbeitet"; }

# Beispielaufruf
process_files sample.jpg document.pdf unknown.xyz music.mp3

7.7.4.3 Verarbeitung von Befehlszeilenoptionen (ähnlich wie getopt)

#!/bin/bash

# Standardwerte
verbose=false
output_file=""
count=1

# Verarbeitung der Optionen
while [ $# -gt 0 ]; do
    case "$1" in
        -v|--verbose)
            verbose=true
            shift
            ;;
        -o|--output)
            if [ -n "$2" ]; then
                output_file="$2"
                shift 2
            else
                echo "Fehler: Option --output benötigt ein Argument." >&2
                exit 1
            fi
            ;;
        -c|--count)
            if [ -n "$2" ] && [[ "$2" =~ ^[0-9]+$ ]]; then
                count="$2"
                shift 2
            else
                echo "Fehler: Option --count benötigt eine positive Ganzzahl." >&2
                exit 1
            fi
            ;;
        -h|--help)
            echo "Verwendung: $0 [OPTIONEN] DATEI"
            echo "Optionen:"
            echo "  -v, --verbose       Ausführliche Ausgabe"
            echo "  -o, --output DATEI  Ausgabe in DATEI schreiben"
            echo "  -c, --count ZAHL    Anzahl der Wiederholungen (Standard: 1)"
            echo "  -h, --help          Diese Hilfe anzeigen"
            exit 0
            ;;
        -*)
            echo "Fehler: Unbekannte Option: $1" >&2
            echo "Verwenden Sie --help für Hilfe." >&2
            exit 1
            ;;
        *)
            # Kein Options-Argument mehr, behandle als Dateiname
            input_file="$1"
            shift
            ;;
    esac
done

# Prüfe, ob eine Eingabedatei angegeben wurde
if [ -z "$input_file" ]; then
    echo "Fehler: Keine Eingabedatei angegeben." >&2
    echo "Verwenden Sie --help für Hilfe." >&2
    exit 1
fi

# Ausgabe der Einstellungen
$verbose && echo "Einstellungen:"
$verbose && echo "- Eingabedatei: $input_file"
$verbose && echo "- Ausgabedatei: ${output_file:-Standard-Ausgabe}"
$verbose && echo "- Anzahl: $count"

# Hier folgt die eigentliche Verarbeitung...

7.7.5 Fall-Through mit ;&

Ab Bash 4.0 unterstützt der case-Befehl mit ;& einen “Fall-Through”-Mechanismus, der die Ausführung mit dem nächsten Muster fortsetzt, ähnlich wie bei switch ohne break in anderen Sprachen:

#!/bin/bash

echo -n "Geben Sie eine Zahl ein (1-3): "
read num

case $num in
    1)
        echo "Eins"
        ;& # Fall-Through zum nächsten Muster
    2)
        echo "Kleiner oder gleich Zwei"
        ;& # Fall-Through zum nächsten Muster
    3)
        echo "Kleiner oder gleich Drei"
        ;;
    *)
        echo "Größer als Drei oder keine Zahl"
        ;;
esac

Bei Eingabe von 1 würden alle drei Meldungen ausgegeben.

7.7.6 Fall-Through nur bei Übereinstimmung mit ;;&

Ab Bash 4.0 gibt es auch ;;&, das die Prüfung mit dem nächsten Muster fortsetzt, statt direkt zum nächsten Muster zu springen:

#!/bin/bash

echo -n "Geben Sie ein Zeichen ein: "
read char

case $char in
    [0-9])
        echo "Das Zeichen ist eine Ziffer."
        ;;& # Prüfe nächstes Muster
    [0-9a-fA-F])
        echo "Das Zeichen ist eine hexadezimale Ziffer."
        ;;& # Prüfe nächstes Muster
    [a-zA-Z])
        echo "Das Zeichen ist ein Buchstabe."
        ;;
    *)
        echo "Das Zeichen ist weder eine Ziffer noch ein Buchstabe."
        ;;
esac

Hier werden nur passende Muster ausgeführt. Eine Eingabe von 7 würde “Das Zeichen ist eine Ziffer.” und “Das Zeichen ist eine hexadezimale Ziffer.” ausgeben, aber nicht “Das Zeichen ist ein Buchstabe.”

7.7.7 Best Practices für case

  1. Klare Strukturierung: Achten Sie auf eine konsistente Einrückung und Formatierung, um die Lesbarkeit zu verbessern.

    case "$input" in
        yes)
            # Eingerückte Befehle
            command1
            command2
            ;;
        no)
            # Eingerückte Befehle
            command3
            command4
            ;;
    esac
  2. Der Fallback-Fall: Implementieren Sie immer einen Fallback-Fall (*), um unerwartete Eingaben abzufangen.

    case "$choice" in
        1) echo "Option 1";;
        2) echo "Option 2";;
        *) echo "Ungültige Option. Bitte 1 oder 2 wählen.";;
    esac
  3. Variablen in Anführungszeichen setzen: Dies verhindert Probleme mit Leerzeichen und Sonderzeichen.

    case "$variable" in  # Gut
        # ...
    esac
    
    case $variable in   # Kann bei Leerzeichen Probleme verursachen
        # ...
    esac
  4. Mehrere Muster zusammenfassen:

    case "$option" in
        -h|--help|-\?)
            show_help
            ;;
        -v|--version)
            show_version
            ;;
    esac
  5. Vermeiden Sie übermäßig komplexe Muster: Teilen Sie komplexe Logik bei Bedarf in separate Funktionen oder verschachtelte case-Anweisungen auf.

7.7.8 Häufige Anwendungsfälle

7.7.8.1 Einfache Menüsysteme

#!/bin/bash

echo "Wählen Sie eine Option:"
echo "1) Dateien auflisten"
echo "2) Aktuelles Verzeichnis anzeigen"
echo "3) Datum und Uhrzeit anzeigen"
echo "q) Beenden"

read -p "Ihre Wahl: " option

case "$option" in
    1)
        ls -la
        ;;
    2)
        pwd
        ;;
    3)
        date
        ;;
    [qQ])
        echo "Programm wird beendet."
        exit 0
        ;;
    *)
        echo "Ungültige Option!"
        ;;
esac

7.7.8.2 Verarbeitung von Shell-Skript-Argumente

#!/bin/bash

# Hilfe-Funktion
show_help() {
    echo "Verwendung: $0 BEFEHL [ARGUMENTE]"
    echo
    echo "Befehle:"
    echo "  backup SOURCE DEST   Backup von SOURCE nach DEST erstellen"
    echo "  restore BACKUP DIR   Backup in Verzeichnis DIR wiederherstellen"
    echo "  list [PATTERN]       Verfügbare Backups auflisten, optional nach PATTERN filtern"
    echo "  help                 Diese Hilfe anzeigen"
}

# Hauptlogik
case "$1" in
    backup)
        if [ -z "$2" ] || [ -z "$3" ]; then
            echo "Fehler: backup benötigt SOURCE und DEST Parameter."
            show_help
            exit 1
        fi
        echo "Erstelle Backup von $2 nach $3..."
        # Backup-Code hier
        ;;
    restore)
        if [ -z "$2" ] || [ -z "$3" ]; then
            echo "Fehler: restore benötigt BACKUP und DIR Parameter."
            show_help
            exit 1
        fi
        echo "Stelle Backup $2 in Verzeichnis $3 wieder her..."
        # Restore-Code hier
        ;;
    list)
        echo "Verfügbare Backups:"
        # List-Code hier
        ;;
    help|--help|-h)
        show_help
        ;;
    "")
        echo "Fehler: Kein Befehl angegeben."
        show_help
        exit 1
        ;;
    *)
        echo "Fehler: Unbekannter Befehl: $1"
        show_help
        exit 1
        ;;
esac

7.7.8.3 Systeminitialisierungsskripte

#!/bin/bash

# Ein vereinfachtes Init-Skript
SERVICE_NAME="myservice"
DAEMON="/usr/bin/myservice"
PIDFILE="/var/run/myservice.pid"

case "$1" in
    start)
        echo "Starte $SERVICE_NAME..."
        if [ -f "$PIDFILE" ]; then
            echo "$SERVICE_NAME läuft bereits (PID: $(cat $PIDFILE))"
            exit 1
        fi
        $DAEMON &
        echo $! > "$PIDFILE"
        ;;
    stop)
        echo "Stoppe $SERVICE_NAME..."
        if [ ! -f "$PIDFILE" ]; then
            echo "$SERVICE_NAME läuft nicht."
            exit 1
        fi
        kill $(cat "$PIDFILE") && rm "$PIDFILE"
        ;;
    restart)
        $0 stop
        sleep 2
        $0 start
        ;;
    status)
        if [ -f "$PIDFILE" ] && kill -0 $(cat "$PIDFILE") 2>/dev/null; then
            echo "$SERVICE_NAME läuft (PID: $(cat $PIDFILE))"
        else
            echo "$SERVICE_NAME läuft nicht."
            [ -f "$PIDFILE" ] && rm "$PIDFILE"
        fi
        ;;
    *)
        echo "Verwendung: $0 {start|stop|restart|status}"
        exit 1
        ;;
esac

exit 0

Der case-Befehl ist ein mächtiges Werkzeug für Shell-Skripte, das Mehrfachverzweigungen elegant und lesbar implementiert. Im Vergleich zu verschachtelten if-Anweisungen bietet case eine klarere Struktur und bessere Lesbarkeit, insbesondere wenn viele verschiedene Bedingungen verglichen werden müssen. Die Möglichkeit, Muster mit Wildcards und regulären Ausdrücken zu verwenden, macht den case-Befehl besonders vielseitig und leistungsfähig für eine Vielzahl von Anwendungsfällen in der Shell-Programmierung.

7.8 Arbeiten mit der exit-Anweisung

Die exit-Anweisung ist ein fundamentaler Bestandteil der Shell-Programmierung, der es ermöglicht, ein Skript zu beenden und einen Statuscode an das aufrufende Programm zurückzugeben. Dieser Mechanismus ist entscheidend für die Kommunikation zwischen Skripten und anderen Programmen, für die Fehlerbehandlung und für die Implementierung robuster Programmflüsse.

7.8.1 Grundlagen der exit-Anweisung

Die Syntax der exit-Anweisung ist denkbar einfach:

exit [n]

wobei [n] ein optionaler Statuscode (eine Ganzzahl) ist. Wenn kein Statuscode angegeben wird, beendet die exit-Anweisung das Skript mit dem Statuscode der letzten ausgeführten Anweisung.

7.8.2 Exit-Statuscodes und ihre Bedeutung

In der Unix/Linux-Welt gilt folgende Konvention für Exit-Statuscodes:

Es gibt einige allgemein akzeptierte Konventionen für bestimmte Statuscodes:

Statuscode Typische Bedeutung
0 Erfolg
1 Allgemeiner Fehler
2 Falsche Verwendung des Shell-Builtin-Befehls
126 Befehl gefunden, aber nicht ausführbar
127 Befehl nicht gefunden
128 Ungültiges Argument für exit
128+n Vom Signal n (z.B. 130 = SIGINT) beendet
255* Exit-Status außerhalb des Bereichs

*Hinweis: Technisch gesehen werden Statuscodes > 255 auf den Rest modulo 256 reduziert, daher ist 255 der höchste effektive Statuscode.

7.8.3 Einfache Verwendung von exit

#!/bin/bash

# Prüfe, ob eine Datei existiert
if [ ! -f "/etc/hosts" ]; then
    echo "Fehler: Die Datei /etc/hosts existiert nicht!" >&2
    exit 1
fi

# Skript wird nur ausgeführt, wenn die Datei existiert
echo "Die Datei /etc/hosts existiert."
# Weitere Verarbeitung...

# Erfolgreicher Abschluss
exit 0

7.8.4 Abfragen des Exit-Status in der Shell

Der Exit-Status des zuletzt ausgeführten Befehls wird in der speziellen Variablen $? gespeichert und kann in Skripten oder auf der Kommandozeile verwendet werden:

grep "pattern" file.txt
echo "Exit-Status von grep: $?"

Wenn “pattern” in “file.txt” gefunden wird, gibt grep den Exit-Status 0 zurück; andernfalls gibt es einen Wert ungleich 0 zurück (normalerweise 1).

7.8.5 Prüfen des Exit-Status mit if

Die if-Anweisung prüft standardmäßig den Exit-Status eines Befehls:

if grep -q "wichtigerEintrag" /etc/config.conf; then
    echo "Eintrag gefunden, fahre fort..."
else
    echo "Eintrag nicht gefunden, beende..." >&2
    exit 1
fi

7.8.6 Exit-Anweisung in Funktionen

Wenn exit innerhalb einer Funktion aufgerufen wird, beendet es das gesamte Skript, nicht nur die Funktion:

#!/bin/bash

check_prerequisites() {
    if [ ! -f "required_file.txt" ]; then
        echo "Fehler: required_file.txt nicht gefunden!" >&2
        exit 1  # Beendet das gesamte Skript
    fi
    
    echo "Voraussetzungen erfüllt."
}

echo "Starte Skript..."
check_prerequisites
echo "Skript wird fortgesetzt."  # Dieser Code wird nicht ausgeführt, wenn required_file.txt nicht existiert

Um eine Funktion ohne Beendigung des gesamten Skripts zu verlassen, verwenden Sie return statt exit:

#!/bin/bash

check_prerequisites() {
    if [ ! -f "required_file.txt" ]; then
        echo "Fehler: required_file.txt nicht gefunden!" >&2
        return 1  # Verlässt nur die Funktion
    fi
    
    echo "Voraussetzungen erfüllt."
    return 0
}

echo "Starte Skript..."
if ! check_prerequisites; then
    echo "Voraussetzungen nicht erfüllt, beende Skript."
    exit 1
fi
echo "Skript wird fortgesetzt."

7.8.7 Komplexe Fehlerbehandlung mit exit

#!/bin/bash

# Definiere Fehlercodes
E_ARGS=65      # Falsche Anzahl an Argumenten
E_NOFILE=66    # Datei nicht gefunden
E_NOPERM=67    # Keine Berechtigung
E_FORMAT=68    # Falsches Format
E_NETWORK=69   # Netzwerkfehler

# Prüfe Befehlszeilenargumente
if [ $# -ne 2 ]; then
    echo "Fehler: Falsche Anzahl an Argumenten." >&2
    echo "Verwendung: $0 QUELLDATEI ZIELDATEI" >&2
    exit $E_ARGS
fi

source_file="$1"
target_file="$2"

# Prüfe, ob die Quelldatei existiert
if [ ! -f "$source_file" ]; then
    echo "Fehler: Quelldatei '$source_file' existiert nicht." >&2
    exit $E_NOFILE
fi

# Prüfe Leserechte für die Quelldatei
if [ ! -r "$source_file" ]; then
    echo "Fehler: Keine Leseberechtigung für '$source_file'." >&2
    exit $E_NOPERM
fi

# Prüfe, ob die Quelldatei ein gültiges Format hat
if ! validate_format "$source_file"; then
    echo "Fehler: '$source_file' hat ein ungültiges Format." >&2
    exit $E_FORMAT
fi

# Versuche, die Datei zu übertragen
if ! transfer_file "$source_file" "$target_file"; then
    echo "Fehler: Dateiübertragung fehlgeschlagen." >&2
    exit $E_NETWORK
fi

echo "Datei erfolgreich übertragen."
exit 0

7.8.8 Bedingte Ausführung mit Exit-Status

Die Operatoren && (AND) und || (OR) nutzen den Exit-Status zur bedingten Ausführung:

# Führe command2 nur aus, wenn command1 erfolgreich war (Exit-Status 0)
command1 && command2

# Führe command2 nur aus, wenn command1 nicht erfolgreich war (Exit-Status ungleich 0)
command1 || command2

Beispiel für die Verkettung von Befehlen mit bedingter Ausführung:

#!/bin/bash

# Erstelle Verzeichnis und wechsle hinein, beende bei Fehler
mkdir -p /tmp/workspace && cd /tmp/workspace || {
    echo "Fehler: Konnte Arbeitsverzeichnis nicht erstellen oder betreten." >&2
    exit 1
}

# Lade Konfiguration herunter, beende bei Fehler
wget -q https://example.com/config.ini || {
    echo "Fehler: Konnte Konfigurationsdatei nicht herunterladen." >&2
    exit 1
}

# Führe Installation durch
./install.sh

7.8.9 Temporäre Dateien und Aufräumen beim Beenden

Die exit-Anweisung kann in Kombination mit der trap-Anweisung verwendet werden, um beim Beenden des Skripts (unabhängig vom Grund) aufzuräumen:

#!/bin/bash

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

# Aufräumfunktion definieren
cleanup() {
    echo "Räume auf..."
    rm -f "$TEMP_FILE"
}

# trap registrieren, um die Aufräumfunktion beim Beenden auszuführen
trap cleanup EXIT

# Skript-Logik
echo "Arbeite mit temporärer Datei: $TEMP_FILE"
# ... weitere Verarbeitung ...

# Das Skript beendet sich hier normal
exit 0

# Die cleanup-Funktion wird automatisch aufgerufen, bevor das Skript beendet wird

7.8.10 Verwendung von Error Trapping

Mit trap können Sie auch spezifische Fehlerbehandlungen für verschiedene Signale definieren:

#!/bin/bash

# Fehlerbehandlungsfunktion
handle_error() {
    local exit_code=$?
    local line_number=$1
    echo "Fehler in Zeile $line_number: Befehl mit Exit-Code $exit_code fehlgeschlagen" >&2
    exit $exit_code
}

# Registriere trap für ERR-Signal mit Zeilennummer
trap 'handle_error $LINENO' ERR

# Skript-Logik
echo "Starte komplexe Operation..."

# Diese Anweisung wird fehlschlagen und die Fehlerbehandlung auslösen
non_existent_command  # Fehler: Befehl nicht gefunden

echo "Diese Zeile wird nie erreicht."

7.8.11 Graduelle Fehlerbehandlung mit EXIT_CODE-Variable

Ein eleganter Ansatz zur Fehlerbehandlung ist die Verwendung einer Statuscode-Variable, die den Exit-Status verfolgt:

#!/bin/bash

# Initialisiere Exit-Status
EXIT_CODE=0

# Führe verschiedene Aufgaben aus und aktualisiere EXIT_CODE bei Fehlern
echo "Führe Aufgabe 1 aus..."
task1
if [ $? -ne 0 ]; then
    echo "Warnung: Aufgabe 1 fehlgeschlagen." >&2
    EXIT_CODE=1
fi

echo "Führe Aufgabe 2 aus..."
task2
if [ $? -ne 0 ]; then
    echo "Warnung: Aufgabe 2 fehlgeschlagen." >&2
    EXIT_CODE=1
fi

echo "Führe kritische Aufgabe 3 aus..."
critical_task3
if [ $? -ne 0 ]; then
    echo "Fehler: Kritische Aufgabe 3 fehlgeschlagen!" >&2
    exit 2  # Sofortiger Abbruch bei kritischen Fehlern
fi

# Beende mit gesammeltem Exit-Status
echo "Alle Aufgaben abgeschlossen."
exit $EXIT_CODE

7.8.12 Verwendung von set -e für automatische Fehlerbehandlung

Mit set -e wird das Skript automatisch beendet, wenn ein Befehl fehlschlägt (Exit-Status ungleich 0):

#!/bin/bash
set -e  # Aktiviere automatisches Beenden bei Fehler

# Diese Befehle werden nacheinander ausgeführt, aber das Skript stoppt beim ersten Fehler
mkdir /tmp/testdir
cd /tmp/testdir
touch testfile
non_existent_command  # Hier stoppt das Skript automatisch
echo "Diese Zeile wird nie erreicht."

Um bestimmte Befehle von dieser Regel auszunehmen, können Sie || true anhängen:

#!/bin/bash
set -e

# Dieser Befehl darf fehlschlagen, ohne das Skript zu beenden
grep "pattern" file.txt || true

# Skript wird fortgesetzt, auch wenn grep fehlschlägt
echo "Skript läuft weiter..."

7.8.13 Kontrollierte Beendigung bei Signalen

Sie können trap verwenden, um auf Signale wie SIGINT (Ctrl+C) oder SIGTERM zu reagieren und das Skript kontrolliert zu beenden:

#!/bin/bash

# Signalbehandlungsfunktion
handle_signal() {
    echo -e "\nSignal empfangen. Führe sauberes Beenden durch..."
    # Aufräumarbeiten hier
    exit 130  # 128 + 2 (SIGINT)
}

# Fange SIGINT (Ctrl+C) und SIGTERM
trap handle_signal SIGINT SIGTERM

echo "Langläufiger Prozess gestartet. Drücken Sie Ctrl+C zum Abbrechen."
while true; do
    echo -n "."
    sleep 1
done

7.8.14 Best Practices für die Verwendung von exit

  1. Konsistente Exit-Codes verwenden: Definieren Sie am Anfang des Skripts bedeutungsvolle Exit-Codes und verwenden Sie diese durchgängig.

    #!/bin/bash
    
    # Definiere Exit-Codes
    SUCCESS=0
    ERR_USAGE=1
    ERR_NO_INPUT=2
    ERR_INVALID_INPUT=3
    ERR_PROCESSING=4
    
    # Verwendung im Skript
    if [ $# -eq 0 ]; then
        echo "Fehler: Keine Eingabedatei angegeben." >&2
        exit $ERR_NO_INPUT
    fi
  2. Fehler auf STDERR ausgeben: Verwenden Sie die Umleitung auf den Fehlerkanal (>&2) für Fehlermeldungen.

    if [ ! -f "$file" ]; then
        echo "Fehler: Datei nicht gefunden: $file" >&2
        exit 1
    fi
  3. Hilfreiche Fehlermeldungen: Geben Sie aussagekräftige Fehlermeldungen aus, die dem Benutzer helfen, das Problem zu verstehen und zu beheben.

    if [ ! -x "$command" ]; then
        echo "Fehler: '$command' ist nicht ausführbar. Bitte Berechtigungen prüfen (chmod +x $command)." >&2
        exit 1
    fi
  4. Immer explizit am Ende beenden: Beenden Sie das Skript am Ende explizit mit exit 0, um einen erfolgreichen Abschluss zu signalisieren.

    # Verarbeitung...
    echo "Alle Aufgaben erfolgreich abgeschlossen."
    exit 0  # Explizites Ende mit Erfolg
  5. Aufräumen vor dem Beenden: Stellen Sie sicher, dass temporäre Dateien und Ressourcen vor dem Beenden freigegeben werden.

    cleanup_and_exit() {
        rm -f "$TEMP_FILE"
        exit "$1"
    }
    
    # Verwendung
    if [ error_condition ]; then
        cleanup_and_exit 1
    fi
    
    # Erfolgreicher Abschluss
    cleanup_and_exit 0

7.8.15 Häufige Fehler bei der Verwendung von exit

  1. Vergessen, den Exit-Status zu prüfen:

    # Problematisch: Der Exit-Status wird nicht geprüft
    rsync -av source/ destination/
    
    # Besser: Überprüfe den Exit-Status
    if ! rsync -av source/ destination/; then
        echo "Fehler: Synchronisation fehlgeschlagen!" >&2
        exit 1
    fi
  2. Überschreiben des Exit-Status durch andere Befehle:

    # Problematisch: Der Exit-Status von command wird überschrieben
    command
    echo "Command ausgeführt."  # Überschreibt $?
    exit $?  # Gibt immer den Exit-Status von 'echo' zurück (fast immer 0)
    
    # Besser: Exit-Status speichern
    command
    status=$?  # Speichere Exit-Status sofort
    echo "Command ausgeführt."
    exit $status  # Verwende den gespeicherten Status
  3. Zu frühes Beenden bei nicht-kritischen Fehlern:

    # Problematisch: Skript endet bei jedem Fehler
    command1 || exit 1
    command2 || exit 1
    command3 || exit 1
    
    # Besser: Sammle Fehler, beende nur wenn nötig
    errors=0
    
    command1 || ((errors++))
    command2 || ((errors++))
    command3 || ((errors++))
    
    if [ $errors -gt 0 ]; then
        echo "Es sind $errors Fehler aufgetreten." >&2
        exit 1
    fi
  4. Ignorieren von Exit-Status in Pipelines:

    # Problematisch: Nur der Exit-Status des letzten Befehls in der Pipeline wird geprüft
    grep "pattern" file.txt | sort | uniq > results.txt
    
    # Besser mit Bash 4.1+: PIPEFAIL aktivieren
    set -o pipefail
    if ! grep "pattern" file.txt | sort | uniq > results.txt; then
        echo "Fehler in der Pipeline!" >&2
        exit 1
    fi

Die exit-Anweisung ist ein mächtiges Werkzeug in der Shell-Programmierung, das für die Kontrolle des Programmflusses und die Kommunikation mit dem aufrufenden Programm unerlässlich ist. Durch die konsequente und durchdachte Verwendung von Exit-Statuscodes können Sie robuste und zuverlässige Skripte erstellen, die sowohl menschlichen Benutzern als auch anderen Programmen klare Rückmeldungen über den Erfolg oder Misserfolg ihrer Ausführung geben.