11 Fortgeschrittene Ein- und Ausgabe

11.1 Umleitungen und Pipes in komplexen Szenarien

Während wir in früheren Abschnitten die grundlegenden Ein- und Ausgabeumleitungen kennengelernt haben, ist es für fortgeschrittene Shell-Skripte unerlässlich, die volle Bandbreite an Umlenkungs- und Pipe-Mechanismen zu beherrschen. In diesem Abschnitt werden wir komplexere Anwendungsszenarien und fortgeschrittene Techniken untersuchen.

11.1.1 File Descriptors verstehen und nutzen

In Unix/Linux-Systemen wird jeder geöffnete File (Datei, Socket, Pipe) durch einen File Descriptor (FD) repräsentiert. Standardmäßig sind drei File Descriptors beim Start jedes Prozesses verfügbar:

Die folgenden Beispiele zeigen, wie man mit diesen und zusätzlichen File Descriptors arbeiten kann:

# Umleitung von stderr zu einer Datei
command 2> error.log

# Umleitung von stdout und stderr in separate Dateien
command 1> output.log 2> error.log

# Umleitung von stderr zur gleichen Stelle wie stdout
command > output.log 2>&1

# Die moderne Schreibweise für das obige Beispiel
command &> output.log

11.1.2 Eigene File Descriptors öffnen und verwenden

In komplexen Skripten kann es nützlich sein, zusätzliche File Descriptors zu eröffnen:

# Öffnet FD 3 zum Schreiben in eine Datei
exec 3> logfile.txt

# Schreiben über FD 3
echo "Debugging-Information" >&3

# Schließen des FD 3
exec 3>&-

Dies ist besonders praktisch für Logging in komplexen Skripten, da Sie verschiedene Ausgabeströme separat verwalten können.

11.1.3 Gleichzeitige Umleitung zu Datei und Konsole

Oft möchte man eine Ausgabe sowohl in eine Datei schreiben als auch auf dem Bildschirm anzeigen:

# Mittels tee
command | tee output.log

# Mit tee und Anhängen an eine Datei
command | tee -a output.log

# Auch stderr durch tee leiten
command 2>&1 | tee complete_log.txt

11.1.4 Prozess-Substitution (Process Substitution)

Die Prozess-Substitution ist eine besonders mächtige Technik, die es ermöglicht, die Ausgabe eines Befehls dort zu verwenden, wo normalerweise ein Dateiname erwartet wird:

# Vergleich der Ausgabe zweier Befehle
diff <(ls -l dir1) <(ls -l dir2)

# Mehrere Quellen für einen Befehl kombinieren
grep "pattern" <(cat file1) <(echo "additional line") <(ssh remote_host cat file2)

11.1.5 Heredocs und Herestrings für komplexe Eingaben

Für komplexere Eingaben bieten sich Heredocs an, die Mehrzeilentexte direkt im Skript definieren:

# Einfaches Heredoc
cat << EOF
Erste Zeile
Zweite Zeile
  Eingerückte Zeile
EOF

# Heredoc mit Variablensubstitution unterdrücken
cat << 'EOT'
$HOME wird nicht ersetzt
$(hostname) wird ebenfalls nicht ausgeführt
EOT

# Heredoc mit Einrückungen für bessere Lesbarkeit
cat << 'EOF' | grep "wichtig"
    Dies ist ein Text mit
    mehreren Zeilen, wo wir nach
    wichtigen Informationen suchen.
EOF

Herestrings sind eine kompaktere Alternative für kurze Inhalte:

# Herestring
grep "muster" <<< "Dies ist ein Text mit einem Muster darin"

# Ersatz für echo | command
tr '[a-z]' '[A-Z]' <<< "in großbuchstaben umwandeln"

11.1.6 Named Pipes (FIFOs) für Interprozesskommunikation

Named Pipes (auch als FIFOs bekannt) erlauben die Kommunikation zwischen nicht verwandten Prozessen:

# Erstellen einer Named Pipe
mkfifo mypipe

# In einem Terminal: Daten in die Pipe schreiben
echo "Daten für den anderen Prozess" > mypipe

# In einem anderen Terminal: Aus der Pipe lesen
cat < mypipe

# Aufräumen
rm mypipe

Im Skript könnte das so aussehen:

#!/bin/bash

# Temporäre Named Pipe erstellen
PIPE=$(mktemp -u)
mkfifo "$PIPE"

# Aufräumfunktion definieren
cleanup() {
    rm -f "$PIPE"
}
trap cleanup EXIT

# Hintergrundprozess, der Daten in die Pipe schreibt
{
    for i in {1..5}; do
        echo "Nachricht $i"
        sleep 1
    done
} > "$PIPE" &

# Hauptprozess, der aus der Pipe liest und die Daten verarbeitet
while read line; do
    echo "Verarbeite: $line"
    # Weitere Verarbeitung hier...
done < "$PIPE"

11.1.7 Pipe-Versagen behandeln mit pipefail

Standardmäßig gibt eine Pipe den Exit-Status des letzten Befehls zurück, was problematisch sein kann, wenn frühere Befehle in der Pipe fehlschlagen:

# Aktivieren von pipefail, um den ersten Fehler in einer Pipe zu erfassen
set -o pipefail

# Jetzt schlägt die gesamte Pipeline fehl, wenn grep fehlschlägt,
# auch wenn sort erfolgreich ist
grep "pattern" nicht_existierende_datei | sort

11.1.8 Mehrstufige Pipelines mit Zwischenspeicherung

Bei komplexen Datenverarbeitungsketten kann es nützlich sein, Zwischenergebnisse zu speichern:

# Komplexe Verarbeitungspipeline mit Zwischenspeicherung
cat großedatei.log | 
    grep "ERROR" | 
    tee fehler_gesamt.log | 
    awk '{print $4, $5}' | 
    sort | 
    uniq -c | 
    tee fehler_zusammenfassung.log |
    sort -nr | 
    head -10 > top_fehler.log

11.1.9 Praktisches Beispiel: Log-Analyse-Skript

Das folgende Beispiel demonstriert die Kombination mehrerer fortgeschrittener Techniken:

#!/bin/bash
set -o pipefail

# Konfiguration
LOG_FILE="/var/log/apache2/access.log"
REPORT_DIR="/var/reports/$(date +%Y-%m-%d)"
mkdir -p "$REPORT_DIR"

# File Descriptors für verschiedene Logs einrichten
exec 3> "$REPORT_DIR/errors.log"
exec 4> "$REPORT_DIR/warnings.log"
exec 5> "$REPORT_DIR/info.log"

# Logging-Funktionen
log_error() { echo "[ERROR] $(date +%H:%M:%S) - $1" >&3; }
log_warn() { echo "[WARN]  $(date +%H:%M:%S) - $1" >&4; }
log_info() { echo "[INFO]  $(date +%H:%M:%S) - $1" >&5; }

# Hauptverarbeitung mit Prozess-Substitution und Pipelines
log_info "Starte Log-Analyse"

# 404-Fehler extrahieren und nach Häufigkeit sortieren
grep "HTTP/1.1\" 404" "$LOG_FILE" |
    awk '{print $7}' |
    sort |
    uniq -c |
    sort -nr > "$REPORT_DIR/404_urls.txt"

# IP-Adressen mit 403-Fehlern finden
grep "HTTP/1.1\" 403" "$LOG_FILE" |
    awk '{print $1}' |
    sort |
    uniq -c |
    sort -nr > "$REPORT_DIR/forbidden_ips.txt"

# Vergleich der heutigen 404s mit gestern
YESTERDAY=$(date -d "yesterday" +%Y-%m-%d)
if [ -f "/var/reports/$YESTERDAY/404_urls.txt" ]; then
    log_info "Vergleiche 404-Fehler mit gestern"
    diff <(cut -d' ' -f2 "$REPORT_DIR/404_urls.txt") \
         <(cut -d' ' -f2 "/var/reports/$YESTERDAY/404_urls.txt") \
         > "$REPORT_DIR/new_404s.txt" || true
else
    log_warn "Keine gestrigen Daten zum Vergleichen verfügbar"
fi

# Zusammenfassung mit Heredoc
cat << EOF > "$REPORT_DIR/summary.txt"
Log-Analyse für $(date +%Y-%m-%d)
================================

Top 5 404-Fehler:
$(head -5 "$REPORT_DIR/404_urls.txt")

Top 5 blockierte IPs (403):
$(head -5 "$REPORT_DIR/forbidden_ips.txt")

Gesamtanzahl der Anfragen: $(wc -l < "$LOG_FILE")
EOF

log_info "Analyse abgeschlossen, Berichte in $REPORT_DIR gespeichert"

# File Descriptors schließen
exec 3>&-
exec 4>&-
exec 5>&-

11.1.10 Fortgeschrittene Fehlerbehandlung mit Umleitungen

Bei einigen komplexen Szenarien möchten Sie möglicherweise nur bestimmte Fehler abfangen oder umleiten:

# Nur bestimmte Fehlermeldungen in eine Datei umleiten
command 2> >(grep "wichtiger Fehler" > gefilterte_fehler.log)

# Fehler in eine Datei schreiben und gleichzeitig auf stderr ausgeben
command 2> >(tee -a error.log >&2)

11.2 Here-Dokumente und Here-Strings

Here-Dokumente (Heredocs) und Here-Strings gehören zu den mächtigsten Werkzeugen für die Eingabesteuerung in Shell-Skripten. Sie ermöglichen die Integration von Mehrzeilentexten direkt in Ihre Skripte und bieten flexible Optionen zur Verarbeitung dieser Texte. In diesem Abschnitt werden wir diese Konzepte im Detail untersuchen und ihre praktischen Anwendungsfälle demonstrieren.

11.2.1 Here-Dokumente (Heredocs)

Ein Here-Dokument erlaubt es, einen Textblock direkt im Skript zu definieren und als Standardeingabe an einen Befehl zu übergeben. Die grundlegende Syntax ist:

command << DELIMITER
text content
more text content
DELIMITER

11.2.1.1 Grundlegende Syntax und Verwendung

Here-Dokumente werden mit dem Umleitungsoperator << eingeleitet, gefolgt von einem selbst gewählten Begrenzungszeichen (Delimiter):

cat << EOF
Dies ist ein mehrzeiliger Text.
Er wird als Eingabe für den cat-Befehl verwendet.
EOF

Der Textblock beginnt in der nächsten Zeile nach dem Delimiter und endet, wenn der Delimiter erneut auf einer eigenen Zeile erscheint.

11.2.1.2 Variablensubstitution in Heredocs

Standardmäßig findet in Heredocs eine Variablensubstitution statt:

name="Alice"
cat << EOF
Hallo, $name!
Heute ist $(date +%d.%m.%Y).
EOF

Ausgabe:

Hallo, Alice!
Heute ist 10.04.2025.

11.2.1.3 Variablensubstitution verhindern

Wenn Sie die Variablensubstitution unterdrücken möchten, setzen Sie den Delimiter in einfache Anführungszeichen:

cat << 'EOF'
In diesem Text werden $variablen und $(befehle)
nicht interpretiert oder ersetzt.
EOF

11.2.1.4 Einrücken von Here-Dokumenten für bessere Lesbarkeit

Bei komplexeren Skripten ist es oft wünschenswert, den Heredoc-Inhalt einzurücken, um die Lesbarkeit zu verbessern. Mit dem <<- Operator können Sie Tabulatoren (nicht Leerzeichen!) am Anfang jeder Zeile im Heredoc ignorieren:

if true; then
    cat <<- EOF
        Dieser Text ist im Skript eingerückt,
        aber die Ausgabe enthält diese Einrückungen nicht.
        Der Abschluss-Delimiter kann ebenfalls eingerückt sein.
    EOF
fi

11.2.1.5 Here-Dokumente mit Pipes und Umleitungen

Here-Dokumente lassen sich nahtlos mit anderen Umleitungen und Pipes kombinieren:

# Heredoc mit Ausgabeumleitung
cat << EOF > konfiguration.txt
user=admin
password=sicher123
host=localhost
EOF

# Heredoc mit Pipe
cat << EOF | grep "wichtig"
Dies ist eine Zeile.
Diese Zeile ist wichtig.
Dies ist noch eine Zeile.
EOF

11.2.1.6 Praktische Anwendungsfälle für Heredocs

  1. Generieren von Konfigurationsdateien:
cat << EOF > /etc/nginx/sites-available/mysite.conf
server {
    listen 80;
    server_name ${DOMAIN};
    
    location / {
        proxy_pass http://localhost:3000;
        proxy_set_header Host \$host;
        proxy_set_header X-Real-IP \$remote_addr;
    }
}
EOF
  1. Mehrzeilige SQL-Abfragen:
mysql -u user -p database << SQL
CREATE TABLE users (
    id INT AUTO_INCREMENT PRIMARY KEY,
    username VARCHAR(255) NOT NULL,
    email VARCHAR(255) NOT NULL,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
INSERT INTO users (username, email) VALUES ('admin', 'admin@example.com');
SQL
  1. Skripte auf Remote-Systemen ausführen:
ssh user@remote_server bash << 'ENDSSH'
cd /var/log
find . -name "*.log" -type f -mtime +30 | xargs rm -f
echo "Alte Logs wurden bereinigt"
ENDSSH
  1. Mehrzeilige Kommentare:
: << 'KOMMENTAR'
Dies ist ein mehrzeiliger Kommentar.
Der Befehl ':' tut nichts, und die Ausgabe
des Heredocs wird an ihn übergeben und ignoriert.
KOMMENTAR

11.2.2 Here-Strings

Here-Strings sind eine kompaktere Alternative zu Heredocs, wenn Sie nur einen einzelnen String als Eingabe übergeben möchten. Die Syntax verwendet den Operator <<<:

command <<< "string"

11.2.2.1 Grundlegende Syntax und Verwendung

# Zählt die Wörter im übergebenen String
wc -w <<< "Dies sind fünf Wörter"

Ausgabe:

5

11.2.2.2 Variablensubstitution in Here-Strings

Wie bei Heredocs findet in Here-Strings Variablensubstitution statt:

name="Bob"
grep "Bob" <<< "Hallo, mein Name ist $name"

11.2.2.3 Praktische Anwendungsfälle für Here-Strings

  1. Einfache Texttransformation:
# Umwandlung in Großbuchstaben
tr 'a-z' 'A-Z' <<< "konvertiere diesen text in großbuchstaben"
  1. Schnelles Filtern:
grep "^ERROR" <<< "INFO: Alles normal\nERROR: Etwas ist schiefgelaufen"
  1. Kompakte Alternativen für echo-Pipe-Kombinationen:
# Anstatt:
echo "user:password" | cut -d':' -f1

# Kann man schreiben:
cut -d':' -f1 <<< "user:password"
  1. Mehrzeilige Here-Strings:
# Auch mehrzeilige Strings funktionieren
grep "Zeile 2" <<< "Zeile 1
Zeile 2
Zeile 3"

11.2.3 Fortgeschrittene Techniken und Tricks

11.2.3.1 Kombinieren von Here-Dokumenten und Funktionen

Here-Dokumente können innerhalb von Funktionen verwendet werden, um wiederkehrende Textvorlagen zu generieren:

generate_html() {
    local title="$1"
    local content="$2"
    
    cat << EOF
<!DOCTYPE html>
<html>
<head>
    <title>$title</title>
</head>
<body>
    <h1>$title</h1>
    <div class="content">
        $content
    </div>
</body>
</html>
EOF
}

# Verwendung
generate_html "Meine Seite" "Willkommen auf meiner Webseite!" > index.html

11.2.3.2 Dynamische Delimiter

Bei spezialisierten Anwendungen kann es nützlich sein, dynamische Delimiter zu verwenden, um Konflikte zu vermeiden:

DELIMITER="ENDE_$(date +%s)"

cat << $DELIMITER
Dieser Text könnte problematische Inhalte wie EOF enthalten,
aber da wir einen zufälligen Delimiter verwenden, gibt es keine Konflikte.
$DELIMITER

11.2.3.3 Here-Dokumente mit temporärer Variablensubstitution

Manchmal möchten Sie nur bestimmte Variablen im Heredoc ersetzen, aber andere unverändert lassen:

username="admin"

# $username wird ersetzt, $HOME bleibt unverändert
cat << EOF | sed "s/\$HOME/\/custom\/path/"
Benutzer: $username
Pfad: $HOME
EOF

11.2.3.4 Here-Dokumente für die Erzeugung ausführbarer Skripte

Here-Dokumente eignen sich hervorragend, um dynamisch neue Skripte zu erzeugen:

#!/bin/bash

# Parameter für das neue Skript
backup_dir="/var/backups"
log_file="/var/log/backup.log"

# Erzeugung eines Backup-Skripts
cat << 'EOF' > /usr/local/bin/daily-backup.sh
#!/bin/bash
# Automatisch generiertes Backup-Skript
EOF

# Jetzt fügen wir dynamische Inhalte hinzu
cat << EOF >> /usr/local/bin/daily-backup.sh
BACKUP_DIR="$backup_dir"
LOG_FILE="$log_file"

echo "Backup gestartet am \$(date)" >> \$LOG_FILE
rsync -av /home/ \$BACKUP_DIR/\$(date +%Y%m%d)/ >> \$LOG_FILE 2>&1
echo "Backup abgeschlossen am \$(date)" >> \$LOG_FILE
EOF

# Ausführbar machen
chmod +x /usr/local/bin/daily-backup.sh

11.2.4 Vergleich und Anwendungsrichtlinien

Wann sollten Sie Heredocs oder Here-Strings verwenden? Hier einige Richtlinien:

Szenario Empfohlene Technik
Mehrzeiliger Text Here-Dokument
Einzeiliger Text Here-String
Komplexe Textformatierung Here-Dokument
Dynamische Skripterzeugung Here-Dokument
Ersetzen von echo "text" \| command Here-String
Große Konfigurationsdateien Here-Dokument mit Ausgabeumleitung

11.3 Prozesssubstitution

Prozesssubstitution ist eine fortgeschrittene Technik in der Shell-Programmierung, die es ermöglicht, die Ausgabe von Befehlen direkt als Datei-ähnliche Objekte zu behandeln. Diese leistungsstarke Funktion wird häufig übersehen, kann aber die Lesbarkeit und Effizienz von Shell-Skripten erheblich verbessern. In diesem Abschnitt werden wir die Prozesssubstitution detailliert untersuchen und ihre praktischen Anwendungsfälle demonstrieren.

11.3.1 Grundkonzept der Prozesssubstitution

Prozesssubstitution stellt die Ausgabe eines Befehls oder einer Pipeline als temporäre Datei dar, auf die andere Befehle zugreifen können. Statt komplexe temporäre Dateien zu erstellen, können Sie diesen Mechanismus nutzen, um Daten direkt zwischen Prozessen zu übertragen.

Die Syntax für Prozesssubstitution verwendet die Operatoren <(command) für Lesezugriff und >(command) für Schreibzugriff:

# Lesezugriff: <(command)
command1 <(command2)

# Schreibzugriff: >(command)
command1 >(command2)

Wenn die Shell diese Konstrukte sieht, führt sie den Befehl in den Klammern aus und ersetzt den Ausdruck durch einen temporären Dateipfad (meistens in Form von /dev/fd/N), der auf eine Pipe zum Prozess verweist.

11.3.2 Prozesssubstitution beim Lesen (<(command))

Die häufigere Form der Prozesssubstitution ist die lesende Form <(command), bei der die Ausgabe eines Befehls als Datei für einen anderen Befehl verfügbar gemacht wird.

11.3.2.1 Vergleich von Befehlsausgaben

Ein klassisches Beispiel ist der Vergleich der Ausgaben zweier Befehle:

# Vergleich der Verzeichnisinhalte von zwei Ordnern
diff <(ls -l /etc/apache2) <(ls -l /etc/apache2.old)

# Vergleich der aktiven Prozesse zu zwei verschiedenen Zeitpunkten
diff <(ps aux) <(sleep 5; ps aux)

Ohne Prozesssubstitution müssten temporäre Dateien erstellt werden:

# Ältere Methode mit temporären Dateien
ls -l /etc/apache2 > /tmp/dir1
ls -l /etc/apache2.old > /tmp/dir2
diff /tmp/dir1 /tmp/dir2
rm /tmp/dir1 /tmp/dir2

11.3.2.2 Verwendung mehrerer Eingabequellen

Befehle, die nur eine einzelne Eingabedatei unterstützen, können mit Prozesssubstitution trotzdem mit mehreren Quellen arbeiten:

# Suche nach Mustern in der Ausgabe mehrerer Befehle
grep "error" <(cat /var/log/syslog) <(journalctl -xe) <(cat /var/log/apache2/error.log)

# Verarbeite die kombinierte Ausgabe mehrerer Befehle
sort -u <(cat file1.txt) <(cat file2.txt) <(echo "Zusätzliche Zeile")

11.3.2.3 Verarbeitung von Remote-Daten

Prozesssubstitution eignet sich hervorragend, um mit Remote-Daten zu arbeiten, ohne temporäre Dateien zu erstellen:

# Vergleich einer lokalen mit einer Remote-Datei
diff <(cat local_file.txt) <(ssh remote_server cat remote_file.txt)

# Suche in Remote-Logs
grep "kritischer Fehler" <(ssh webserver cat /var/log/nginx/error.log)

11.3.3 Prozesssubstitution beim Schreiben (>(command))

Die schreibende Form der Prozesssubstitution >(command) ermöglicht es, Daten in einen verarbeitenden Befehl zu leiten, während der Hauptbefehl weiterläuft.

11.3.3.1 Parallele Verarbeitung und Protokollierung

Besonders nützlich ist dies für parallele Verarbeitung und Protokollierung:

# Ausgabe sowohl an die Konsole als auch in eine Datei schreiben
echo "Wichtige Nachricht" | tee >(logger)

# Daten erfassen und gleichzeitig verschiedene Analysen durchführen
cat huge_log.txt > >(grep "ERROR" > errors.log) > >(grep "WARNING" > warnings.log)

11.3.3.2 Mehrfache Datenströme erzeugen

Mit der schreibenden Prozesssubstitution können Sie einen Datenstrom auf mehrere Verarbeitungspfade aufteilen:

# Verteile die Ausgabe auf mehrere Verarbeitungspipelines
cat access.log | tee >(awk '/404/ {print $1}' | sort -u > not_found_ips.txt) \
                      >(awk '/503/ {print $1}' | sort -u > service_unavailable_ips.txt) \
                      >(grep -i "bot" > bot_requests.log) \
                      > /dev/null

11.3.4 Kombination beider Formen

In fortgeschrittenen Szenarien können beide Formen der Prozesssubstitution kombiniert werden:

# Komplexes Beispiel: Daten aus einer Quelle lesen, aufteilen und verarbeiten
cat <(zcat log_archive.gz) | 
    tee >(grep "ERROR" > errors.log) |
    awk '{count[$1]++} END {for (ip in count) print ip, count[ip]}' |
    sort -k2 -nr > ip_frequency.txt

11.3.5 Praktische Anwendungsbeispiele

11.3.5.1 Beispiel 1: CSV-Dateien zusammenführen und transformieren

#!/bin/bash

# Zwei CSV-Dateien mit gemeinsamer Schlüsselspalte zusammenführen 
# und gleichzeitig transformieren

join -t, -1 1 -2 1 \
    <(sort -t, -k1 users.csv) \
    <(sort -t, -k1 orders.csv) |
    awk -F, '{print $1","$2","$4","$5}' > combined_report.csv

11.3.5.2 Beispiel 2: Systemüberwachungsskript

#!/bin/bash

# Systemstatus erfassen und in verschiedene Berichte aufteilen

current_date=$(date +"%Y-%m-%d_%H-%M-%S")

# Systemdaten sammeln und in verschiedene Berichte aufteilen
{
    echo "=== Systemstatus vom $current_date ==="
    echo
    echo "=== CPU-Auslastung ==="
    top -bn1 | head -20
    echo
    echo "=== Speichernutzung ==="
    free -h
    echo
    echo "=== Festplattennutzung ==="
    df -h
    echo
    echo "=== Netzwerkverbindungen ==="
    netstat -tuln
} | tee >(grep -A10 "CPU" > $current_date-cpu.log) \
          >(grep -A8 "Speicher" > $current_date-memory.log) \
          >(grep -A8 "Festplatten" > $current_date-disk.log) \
          >(grep -A15 "Netzwerk" > $current_date-network.log) \
          > $current_date-full.log

11.3.5.3 Beispiel 3: Datenmigration mit Transformation

#!/bin/bash

# Daten aus einer alten Datenbank in eine neue migrieren,
# mit Backup und Transformation

mysqldump -u olduser -poldpass olddb |
    tee >(gzip > olddb_backup.sql.gz) |
    sed 's/`old_prefix_/`new_prefix_/g' |
    mysql -u newuser -pnewpass newdb

11.3.5.4 Beispiel 4: Kontinuierliches Log-Monitoring

#!/bin/bash

# Kontinuierliche Überwachung eines Logs mit verschiedenen Filtern

tail -f /var/log/application.log |
    tee >(grep "ERROR" | ts "%Y-%m-%d %H:%M:%S" >> errors.log) \
        >(grep "WARN" | ts "%Y-%m-%d %H:%M:%S" >> warnings.log) \
        >(grep -i "security" | ts "%Y-%m-%d %H:%M:%S" >> security.log) \
        | grep --color=always -E "ERROR|WARN|$"

11.3.6 Technische Details und Einschränkungen

11.3.6.1 Wie funktioniert Prozesssubstitution intern?

Unter der Haube verwendet die Shell für Prozesssubstitution benannte Pipes oder spezielle /dev/fd/ Dateien:

  1. Die Shell erstellt eine Pipeline.
  2. Sie startet den Befehl in den Klammern und verbindet seine Standard-Ein- oder Ausgabe mit der Pipeline.
  3. Sie ersetzt den Prozesssubstitutionsausdruck durch einen Pfad wie /dev/fd/63, der auf die Pipeline verweist.
  4. Der Hauptbefehl wird mit diesem Pfad als Argument ausgeführt.

Sie können dies selbst beobachten:

echo <(echo "test")  # Zeigt etwas wie /dev/fd/63 an

11.3.6.2 Kompatibilität und Einschränkungen

Beachten Sie die folgenden Einschränkungen:

  1. Shell-Unterstützung: Prozesssubstitution wird von Bash, Zsh und Ksh unterstützt, aber nicht von POSIX-konformen Shells wie Dash.

  2. Rückgabewerte: Es kann schwierig sein, die Rückgabewerte von Befehlen innerhalb einer Prozesssubstitution zu erfassen.

  3. Parallele Ausführung: Befehle in einer Prozesssubstitution laufen parallel zum Hauptbefehl, was zu unerwarteten Timing-Problemen führen kann.

Für bessere POSIX-Kompatibilität können Sie explizit Bash verwenden:

#!/usr/bin/env bash

11.3.6.3 Performance-Betrachtungen

Prozesssubstitution ist im Allgemeinen effizienter als temporäre Dateien zu verwenden, da:

Bei sehr großen Datenmengen sollten Sie jedoch die Auswirkungen auf den Speicher im Auge behalten.

11.3.7 Praktische Richtlinien

11.3.7.1 Wann sollte man Prozesssubstitution verwenden?

Prozesssubstitution ist besonders nützlich, wenn:

  1. Sie die Ausgabe mehrerer Befehle an einen Befehl übergeben möchten, der mehrere Dateien als Eingabe akzeptiert.
  2. Sie eine Datenstromverzweigung benötigen, ohne temporäre Dateien zu erstellen.
  3. Sie mit Remote-Daten arbeiten, ohne sie lokal speichern zu müssen.
  4. Sie parallele Verarbeitungspipelines implementieren möchten.

11.3.7.2 Wann sollte man andere Techniken bevorzugen?

In folgenden Situationen sind möglicherweise andere Techniken besser geeignet:

  1. Bei Skripten, die in POSIX-konformen Shells laufen müssen.
  2. Wenn Sie die Fehlerrückgabestatus der Subprozesse zuverlässig erfassen müssen.
  3. Bei sehr einfachen Pipelines, wo normale Pipes (|) ausreichen.

11.4 Arbeiten mit mehreren Ein- und Ausgabeströmen

In komplexen Shell-Skripten reicht die grundlegende Umleitung von Standard-Ein- und Ausgabeströmen oft nicht aus. Für anspruchsvolle Automatisierungsaufgaben und Datenverarbeitung ist es essentiell, mehrere Ein- und Ausgabeströme gleichzeitig zu kontrollieren und zu koordinieren. Dieser Abschnitt behandelt fortgeschrittene Techniken zum Arbeiten mit mehreren Datenströmen.

11.4.1 Grundlagen der File Descriptors

In Unix/Linux-Systemen wird jede geöffnete Datei, jeder Socket und jede Pipe durch einen numerischen File Descriptor (FD) repräsentiert. Standardmäßig sind diese drei File Descriptors für jeden Prozess definiert:

Aber das System unterstützt weit mehr als nur diese drei. In den meisten Shell-Implementierungen können Sie mit bis zu 9 FDs arbeiten, in fortgeschrittenen Szenarien manchmal sogar mehr.

11.4.2 Eigene File Descriptors öffnen und verwenden

Mit dem exec-Befehl können Sie zusätzliche File Descriptors öffnen und mit ihnen arbeiten:

# FD 3 zum Schreiben in eine Datei öffnen
exec 3> output.log

# Etwas über FD 3 schreiben
echo "Diese Nachricht geht in die Log-Datei" >&3

# FD 3 schließen
exec 3>&-

Dieser Ansatz ist besonders nützlich bei komplexen Skripten, die verschiedene Arten von Ausgaben an unterschiedliche Ziele senden müssen.

11.4.3 Multiple Log-Kanäle implementieren

Ein häufiger Anwendungsfall für mehrere Ausgabeströme ist die Implementierung von Log-Kanälen mit verschiedenen Detailstufen:

#!/bin/bash

# Log-Datei-Handles öffnen
exec 3> /var/log/script_error.log   # Nur Fehler
exec 4> /var/log/script_warn.log    # Warnungen und Fehler
exec 5> /var/log/script_info.log    # Info, Warnungen und Fehler
exec 6> /var/log/script_debug.log   # Alle Meldungen inkl. Debug

# Log-Funktionen
log_error() {
    local timestamp=$(date +"%Y-%m-%d %H:%M:%S")
    echo "[$timestamp] ERROR: $1" >&3
    echo "[$timestamp] ERROR: $1" >&4
    echo "[$timestamp] ERROR: $1" >&5
    echo "[$timestamp] ERROR: $1" >&6
    echo "ERROR: $1" >&2  # Auch auf stderr ausgeben
}

log_warn() {
    local timestamp=$(date +"%Y-%m-%d %H:%M:%S")
    echo "[$timestamp] WARNING: $1" >&4
    echo "[$timestamp] WARNING: $1" >&5
    echo "[$timestamp] WARNING: $1" >&6
}

log_info() {
    local timestamp=$(date +"%Y-%m-%d %H:%M:%S")
    echo "[$timestamp] INFO: $1" >&5
    echo "[$timestamp] INFO: $1" >&6
}

log_debug() {
    local timestamp=$(date +"%Y-%m-%d %H:%M:%S")
    echo "[$timestamp] DEBUG: $1" >&6
}

# Beispiel-Verwendung
log_debug "Initialisiere Skript"
log_info "Skript gestartet"
log_warn "Konfigurationsdatei nicht gefunden, verwende Standardwerte"
log_error "Kritischer Fehler: Datenbankverbindung fehlgeschlagen"

# Aufräumen am Ende
exec 3>&-
exec 4>&-
exec 5>&-
exec 6>&-

11.4.4 Duplikation von File Descriptors

Sie können einen File Descriptor duplizieren, um seine Ausgabe zu kopieren oder zu sichern:

# Sichere stdout in FD 3
exec 3>&1

# Umleitung von stdout in eine Datei
exec 1> output.log

# Führe Befehle aus, deren Ausgabe in die Datei geht
echo "Diese Zeile geht in die Datei"

# Stelle stdout von FD 3 wieder her
exec 1>&3

# Schließe den temporären FD
exec 3>&-

echo "Diese Zeile erscheint wieder auf dem Terminal"

Diese Technik ist besonders nützlich, wenn Sie temporär die Ausgabe umleiten und später wieder zum ursprünglichen Zustand zurückkehren möchten.

11.4.5 Erfassen der Ausgabe eines Blocks von Befehlen

Sie können einen Block von Befehlen in geschweifte Klammern einschließen und ihre gesamte Ausgabe auf einmal umleiten:

# Ausgabe eines Befehls-Blocks in eine Datei umleiten
{
    echo "Systemdiagnose"
    echo "==============="
    date
    uname -a
    df -h
    free -m
    ps aux | grep httpd
} > system_report.txt

# Ausgabe- und Fehlerströme separat erfassen
{
    echo "Start der Datenbanksicherung"
    mysqldump --all-databases
    echo "Sicherung abgeschlossen"
} > backup.sql 2> backup_errors.log

Dies ist ein sauberer Weg, um zusammenhängende Befehlsgruppen zu verarbeiten.

11.4.6 Temporäres Umschalten der Ausgabeströme

Ein fortgeschrittenes Muster ist das vorübergehende Umschalten zwischen verschiedenen Ausgabezielen:

#!/bin/bash

# Standard-Ausgabeströme sichern
exec 3>&1  # Sichere stdout
exec 4>&2  # Sichere stderr

# Standardmäßig ausführliche Meldungen ausgeben
VERBOSE=true

# Funktion zum Ausführen von Befehlen mit kontrollierter Ausgabe
run_command() {
    local cmd="$1"
    local desc="$2"
    
    echo "Führe aus: $desc"
    
    if [ "$VERBOSE" = true ]; then
        # Verbose Modus: Alle Ausgaben anzeigen
        eval "$cmd"
    else
        # Stiller Modus: Ausgaben unterdrücken, nur Fehler anzeigen
        exec 1>/dev/null
        eval "$cmd"
        exec 1>&3  # stdout wiederherstellen
    fi
}

# Beispiel-Verwendung
run_command "ls -la /etc" "Auflisten von Dateien in /etc"

# Stiller Modus
VERBOSE=false
run_command "find /var -type f -name '*.log'" "Suche nach Log-Dateien"

# Ausgabeströme wiederherstellen
exec 1>&3
exec 2>&4
exec 3>&-
exec 4>&-

11.4.7 Mehrere Eingabequellen kombinieren

So wie wir mehrere Ausgabeströme handhaben können, können wir auch mit mehreren Eingabequellen arbeiten:

#!/bin/bash

# Öffne verschiedene Dateien als Eingabeströme
exec 3< datei1.txt
exec 4< datei2.txt

# Lese abwechselnd aus beiden Dateien
while IFS= read -r line1 <&3 && IFS= read -r line2 <&4; do
    echo "Datei 1: $line1"
    echo "Datei 2: $line2"
done

# Schließe die File Descriptors
exec 3<&-
exec 4<&-

Diese Technik ist nützlich, wenn Sie mehrere Datenströme parallel verarbeiten müssen.

11.4.8 Bidirektionale Kommunikation mit Prozessen

In fortgeschrittenen Szenarien können Sie sowohl Eingabe- als auch Ausgabeströme für die bidirektionale Kommunikation mit einem Unterprozess verwenden:

#!/bin/bash

# Öffne eine bidirektionale Pipe zu einem Programm
coproc bc  # Starte bc als Koprozess

# Sende Berechnungen an bc und lese Ergebnisse
echo "2 + 2" >&"${COPROC[1]}"
read -r result <&"${COPROC[0]}"
echo "Ergebnis: $result"

echo "sqrt(16)" >&"${COPROC[1]}"
read -r result <&"${COPROC[0]}"
echo "Ergebnis: $result"

# Beende den Koprozess
echo "quit" >&"${COPROC[1]}"

Diese Technik ist besonders für die Interaktion mit interaktiven Programmen wie Interpretern oder Datenbanken nützlich.

11.4.9 Named Pipes (FIFOs) für komplexe Kommunikationsszenarien

Named Pipes (auch als FIFOs bekannt) bieten eine Möglichkeit, mehrere Prozesse in komplexen Datenflussszenarien zu verbinden:

#!/bin/bash

# Erstelle Named Pipes
PIPE1=$(mktemp -u)
PIPE2=$(mktemp -u)
mkfifo "$PIPE1"
mkfifo "$PIPE2"

# Aufräumen bei Beendigung
trap 'rm -f "$PIPE1" "$PIPE2"' EXIT

# Prozess 1: Generiere Daten und schreibe in PIPE1
{
    for i in {1..10}; do
        echo "Datensatz $i"
        sleep 0.5
    done
} > "$PIPE1" &

# Prozess 2: Lese aus PIPE1, verarbeite und schreibe in PIPE2
{
    while IFS= read -r line; do
        echo "Verarbeitet: ${line^^}"  # Umwandlung in Großbuchstaben
    done < "$PIPE1"
} > "$PIPE2" &

# Prozess 3: Lese das Endergebnis aus PIPE2
while IFS= read -r line; do
    echo "Endgültiges Ergebnis: $line"
done < "$PIPE2"

Named Pipes ermöglichen komplexe Datenflussarchitekturen und asynchrone Kommunikation zwischen Prozessen.

11.4.10 Praktisches Beispiel: Multi-Stream-Datenverarbeitung

Das folgende Beispiel demonstriert, wie man mehrere Ein- und Ausgabeströme in einem realen Szenario verwenden kann:

#!/bin/bash

# Dieses Skript verarbeitet eine Logdatei, extrahiert verschiedene Arten von Events
# und speichert sie in separaten Dateien, während es Echtzeit-Statistiken anzeigt.

# Definiere Log-Dateien
ERROR_LOG="errors.log"
WARN_LOG="warnings.log"
INFO_LOG="info.log"
ACCESS_LOG="access.log"
STATS_FILE="statistics.txt"

# Lösche bestehende Log-Dateien
> "$ERROR_LOG"
> "$WARN_LOG"
> "$INFO_LOG"
> "$ACCESS_LOG"
> "$STATS_FILE"

# Öffne File Descriptors für die Log-Dateien
exec 3> "$ERROR_LOG"
exec 4> "$WARN_LOG"
exec 5> "$INFO_LOG"
exec 6> "$ACCESS_LOG"
exec 7> "$STATS_FILE"

# Zähler initialisieren
errors=0
warnings=0
infos=0
accesses=0

# Sichere stdout für spätere Wiederherstellung
exec 8>&1

# Funktion zum Aktualisieren der Statistiken
update_stats() {
    # Ausgabe auf Standard-stdout umleiten
    exec 1>&8
    
    # Aktuelle Statistiken anzeigen
    echo -ne "\rVerarbeitet: Fehler: $errors | Warnungen: $warnings | Infos: $infos | Zugriffe: $accesses"
    
    # Auch in Statistik-Datei schreiben
    echo "$(date +%H:%M:%S) - Fehler: $errors | Warnungen: $warnings | Infos: $infos | Zugriffe: $accesses" >&7
}

# Hauptverarbeitungsschleife
while IFS= read -r line; do
    # Kategorisiere jede Zeile basierend auf ihrem Inhalt
    if [[ "$line" == *"ERROR"* ]]; then
        echo "$line" >&3
        ((errors++))
    elif [[ "$line" == *"WARN"* ]]; then
        echo "$line" >&4
        ((warnings++))
    elif [[ "$line" == *"INFO"* ]]; then
        echo "$line" >&5
        ((infos++))
    elif [[ "$line" == *"GET"* || "$line" == *"POST"* ]]; then
        echo "$line" >&6
        ((accesses++))
    fi
    
    # Statistiken alle 10 Einträge aktualisieren
    if (( (errors + warnings + infos + accesses) % 10 == 0 )); then
        update_stats
    fi
done < "combined.log"  # Die zu verarbeitende Log-Datei

# Abschließende Statistiken
update_stats
echo # Neue Zeile nach dem Update

# Schließe alle File Descriptors
exec 3>&-
exec 4>&-
exec 5>&-
exec 6>&-
exec 7>&-
exec 8>&-

echo "Verarbeitung abgeschlossen. Log-Dateien wurden erstellt."
echo "Zusammenfassung:"
echo "  - Fehler: $errors (gespeichert in $ERROR_LOG)"
echo "  - Warnungen: $warnings (gespeichert in $WARN_LOG)"
echo "  - Infos: $infos (gespeichert in $INFO_LOG)"
echo "  - Zugriffe: $accesses (gespeichert in $ACCESS_LOG)"
echo "  - Fortlaufende Statistiken: $STATS_FILE"

11.4.11 Best Practices und Hinweise

Beim Arbeiten mit mehreren Ein- und Ausgabeströmen sollten Sie folgende Best Practices beachten:

  1. Immer aufräumen: Schließen Sie alle geöffneten File Descriptors vor Beendigung des Skripts, um Ressourcenlecks zu vermeiden.

  2. Nutzung von trap: Verwenden Sie trap-Befehle, um sicherzustellen, dass Aufräumarbeiten auch bei unerwartetem Skriptabbruch durchgeführt werden.

  3. Kommentieren Sie Ihre FDs: Bei komplexen Skripten mit vielen FDs sollten Sie deren Verwendungszweck klar kommentieren.

  4. Verwenden Sie sprechende Variablen: Anstatt direkt mit Zahlen zu arbeiten, können Sie Variablen für bessere Lesbarkeit verwenden:

    ERROR_OUT=3
    WARN_OUT=4
    
    exec $ERROR_OUT> error.log
    echo "Fehlermeldung" >&$ERROR_OUT
  5. Fehlerbehandlung: Überprüfen Sie, ob das Öffnen der File Descriptors erfolgreich war, besonders bei kritischen Operationen.

  6. Vermeiden Sie zu komplizierte Konstrukte: Wenn ein Design zu komplex wird, erwägen Sie, es in separate Skripte aufzuteilen oder eine stärkere Programmiersprache zu verwenden.

11.4.12 Begrenzungen und Hinweise

11.5 Interaktive Skripte und Benutzereingaben

Interaktive Shell-Skripte, die mit Benutzereingaben arbeiten, bieten eine leistungsstarke Möglichkeit, anpassbare und benutzerfreundliche Automatisierungslösungen zu erstellen. In diesem Abschnitt werden wir verschiedene Techniken zur Erfassung, Validierung und Verarbeitung von Benutzereingaben untersuchen, sowie Methoden zur Erstellung ansprechender interaktiver Benutzeroberflächen in der Shell.

11.5.1 Grundlegende Eingabeerfassung mit read

Der read-Befehl ist das Hauptwerkzeug zur Erfassung von Benutzereingaben in Shell-Skripten:

#!/bin/bash

# Einfache Eingabeaufforderung
echo "Wie lautet dein Name?"
read name
echo "Hallo, $name!"

# Mehrere Variablen in einer Zeile
echo "Gib deinen Vor- und Nachnamen ein:"
read vorname nachname
echo "Hallo, $vorname $nachname!"

11.5.1.1 Nützliche Optionen des read-Befehls

Der read-Befehl verfügt über zahlreiche Optionen, die seine Funktionalität erheblich erweitern:

# Timeout für Eingabe (5 Sekunden)
read -t 5 -p "Schnell entscheiden (5 Sek.): " antwort
echo "Deine Antwort: ${antwort:-Keine Eingabe}"

# Eingabeaufforderung im selben Befehl
read -p "Serveradresse: " server

# Passwort ohne Anzeige eingeben
read -s -p "Passwort: " passwort
echo # Neue Zeile nach verdeckter Eingabe

# Begrenzte Anzahl von Zeichen einlesen
read -n 1 -p "Drücke eine Taste, um fortzufahren... " taste

# Mit Vorgabewert
read -p "Sprache [de]: " sprache
sprache=${sprache:-de}

11.5.2 Eingabevalidierung und Fehlerbehandlung

Bei interaktiven Skripten ist es wichtig, Benutzereingaben zu validieren und angemessen auf ungültige Eingaben zu reagieren:

#!/bin/bash

# Funktion zur Validierung numerischer Eingaben
validate_number() {
    local input=$1
    local min=$2
    local max=$3
    
    # Prüfe, ob die Eingabe eine gültige Zahl ist
    if ! [[ "$input" =~ ^[0-9]+$ ]]; then
        echo "Fehler: Bitte gib eine gültige Zahl ein."
        return 1
    fi
    
    # Prüfe Bereich, falls angegeben
    if [ -n "$min" ] && [ "$input" -lt "$min" ]; then
        echo "Fehler: Die Zahl muss mindestens $min sein."
        return 1
    fi
    
    if [ -n "$max" ] && [ "$input" -gt "$max" ]; then
        echo "Fehler: Die Zahl darf höchstens $max sein."
        return 1
    fi
    
    return 0
}

# Wiederholte Eingabeaufforderung bis zur gültigen Eingabe
get_valid_number() {
    local prompt=$1
    local min=$2
    local max=$3
    local value
    
    while true; do
        read -p "$prompt: " value
        
        if validate_number "$value" "$min" "$max"; then
            echo "$value"
            return 0
        fi
    done
}

# Beispielverwendung
echo "Server-Konfiguration"
port=$(get_valid_number "Gib den Port ein (1024-65535)" 1024 65535)
echo "Server wird auf Port $port konfiguriert."

11.5.3 Erweiterte Eingabevalidierung mit Regulären Ausdrücken

Für komplexere Validierungen sind reguläre Ausdrücke (Regex) unverzichtbar:

#!/bin/bash

# Funktion zur Validierung einer E-Mail-Adresse
validate_email() {
    local email=$1
    local email_regex="^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$"
    
    if [[ "$email" =~ $email_regex ]]; then
        return 0
    else
        echo "Fehler: '$email' ist keine gültige E-Mail-Adresse."
        return 1
    fi
}

# Funktion zur Validierung einer IP-Adresse
validate_ip() {
    local ip=$1
    local ip_regex="^([0-9]{1,3}\.){3}[0-9]{1,3}$"
    
    if ! [[ "$ip" =~ $ip_regex ]]; then
        echo "Fehler: Falsches IP-Format."
        return 1
    fi
    
    # Überprüfe, ob jede Oktett zwischen 0 und 255 liegt
    IFS='.' read -r -a octets <<< "$ip"
    for octet in "${octets[@]}"; do
        if [[ "$octet" -gt 255 ]]; then
            echo "Fehler: IP-Oktett > 255."
            return 1
        fi
    done
    
    return 0
}

# Eingabe mit Validierung
while true; do
    read -p "E-Mail-Adresse: " email
    if validate_email "$email"; then
        break
    fi
done

echo "Gültige E-Mail: $email"

11.5.4 Interaktive Menüs erstellen

Menüs bieten eine benutzerfreundliche Möglichkeit, in Shell-Skripten Optionen auszuwählen:

#!/bin/bash

# Funktion zur Anzeige eines einfachen Menüs
show_menu() {
    clear
    echo "==== System Administration Menü ===="
    echo "1) Systemstatus anzeigen"
    echo "2) Festplattenbelegung prüfen"
    echo "3) Benutzer verwalten"
    echo "4) Dienste verwalten"
    echo "0) Beenden"
    echo "================================="
}

# Hauptmenüschleife
while true; do
    show_menu
    read -p "Wähle eine Option: " option
    
    case "$option" in
        1)
            echo "Systemstatus:"
            uptime
            free -h
            read -n 1 -p "Drücke eine Taste, um fortzufahren..."
            ;;
        2)
            echo "Festplattenbelegung:"
            df -h
            read -n 1 -p "Drücke eine Taste, um fortzufahren..."
            ;;
        3)
            echo "Benutzerverwaltung noch nicht implementiert."
            read -n 1 -p "Drücke eine Taste, um fortzufahren..."
            ;;
        4)
            echo "Dienstverwaltung noch nicht implementiert."
            read -n 1 -p "Drücke eine Taste, um fortzufahren..."
            ;;
        0)
            echo "Programm wird beendet."
            exit 0
            ;;
        *)
            echo "Ungültige Option!"
            read -n 1 -p "Drücke eine Taste, um fortzufahren..."
            ;;
    esac
done

11.5.4.1 Hierarchische Menüs

Für komplexere Anwendungen können Sie verschachtelte oder hierarchische Menüs implementieren:

#!/bin/bash

# Hauptmenü-Funktion
main_menu() {
    clear
    echo "===== Hauptmenü ====="
    echo "1) Systemverwaltung"
    echo "2) Netzwerkverwaltung"
    echo "3) Benutzerverwaltung"
    echo "0) Beenden"
    echo "===================="
    
    read -p "Wähle eine Option: " option
    
    case "$option" in
        1) system_menu ;;
        2) network_menu ;;
        3) user_menu ;;
        0) exit 0 ;;
        *) 
            echo "Ungültige Option!"
            read -n 1 -p "Drücke eine Taste, um fortzufahren..."
            main_menu
            ;;
    esac
}

# Untermenü für Systemverwaltung
system_menu() {
    clear
    echo "===== Systemverwaltung ====="
    echo "1) Systemstatus anzeigen"
    echo "2) Festplattenbelegung prüfen"
    echo "3) Prozesse anzeigen"
    echo "9) Zurück zum Hauptmenü"
    echo "0) Beenden"
    echo "==========================="
    
    read -p "Wähle eine Option: " option
    
    case "$option" in
        1)
            uptime
            free -h
            read -n 1 -p "Drücke eine Taste, um fortzufahren..."
            system_menu
            ;;
        2)
            df -h
            read -n 1 -p "Drücke eine Taste, um fortzufahren..."
            system_menu
            ;;
        3)
            ps aux | head -20
            read -n 1 -p "Drücke eine Taste, um fortzufahren..."
            system_menu
            ;;
        9) main_menu ;;
        0) exit 0 ;;
        *)
            echo "Ungültige Option!"
            read -n 1 -p "Drücke eine Taste, um fortzufahren..."
            system_menu
            ;;
    esac
}

# Weitere Untermenüs können hier definiert werden
network_menu() {
    # Ähnlich wie system_menu
    echo "Netzwerkverwaltung noch nicht implementiert."
    read -n 1 -p "Drücke eine Taste, um fortzufahren..."
    main_menu
}

user_menu() {
    # Ähnlich wie system_menu
    echo "Benutzerverwaltung noch nicht implementiert."
    read -n 1 -p "Drücke eine Taste, um fortzufahren..."
    main_menu
}

# Start des Programms
main_menu

11.5.5 Auswahlmenüs mit select

Bash bietet den select-Befehl, der automatisch nummerierte Menüs generiert:

#!/bin/bash

echo "Wähle ein Betriebssystem:"
select os in "Linux" "Windows" "macOS" "FreeBSD" "Andere"; do
    case $os in
        "Linux")
            echo "Du hast Linux gewählt."
            break
            ;;
        "Windows")
            echo "Du hast Windows gewählt."
            break
            ;;
        "macOS")
            echo "Du hast macOS gewählt."
            break
            ;;
        "FreeBSD")
            echo "Du hast FreeBSD gewählt."
            break
            ;;
        "Andere")
            echo "Du hast eine andere Option gewählt."
            break
            ;;
        *)
            echo "Ungültige Auswahl. Bitte wähle 1-5."
            ;;
    esac
done

# select mit dynamischen Optionen aus einem Array
distros=("Ubuntu" "Fedora" "Debian" "Arch" "CentOS")
echo "Wähle eine Linux-Distribution:"
select distro in "${distros[@]}" "Zurück"; do
    if [[ "$distro" == "Zurück" ]]; then
        break
    elif [[ -n "$distro" ]]; then
        echo "Du hast $distro gewählt."
        break
    else
        echo "Ungültige Auswahl. Bitte versuche es erneut."
    fi
done

11.5.6 Fortgeschrittene Terminal-Interaktion

Für anspruchsvollere interaktive Skripte können Sie die ANSI-Escape-Sequenzen nutzen, um die Terminalanzeige zu steuern:

#!/bin/bash

# Terminalfarben und -formatierung
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m' # No Color (Zurücksetzen)

# Terminalsteuerung
clear_screen() { echo -e "\033[2J\033[H"; }
move_cursor() { echo -e "\033[$1;${2}H"; }
hide_cursor() { echo -e "\033[?25l"; }
show_cursor() { echo -e "\033[?25h"; }

# Beispiel für ein fortgeschrittenes Menü mit Farbakzenten
advanced_menu() {
    clear_screen
    echo -e "${BLUE}==============================${NC}"
    echo -e "${YELLOW}      SYSTEMVERWALTUNG      ${NC}"
    echo -e "${BLUE}==============================${NC}"
    echo
    echo -e "  ${GREEN}1)${NC} Systemstatus anzeigen"
    echo -e "  ${GREEN}2)${NC} Festplattenbelegung prüfen"
    echo -e "  ${GREEN}3)${NC} Benutzer verwalten"
    echo -e "  ${GREEN}4)${NC} Dienste verwalten"
    echo -e "  ${RED}0)${NC} Beenden"
    echo
    echo -e "${BLUE}------------------------------${NC}"
    
    read -p "Wähle eine Option: " option
    
    # Verarbeite die Option wie im vorherigen Beispiel
}

# Fortschrittsanzeige
progress_bar() {
    local duration=$1
    local steps=20
    local delay=$(echo "scale=3; $duration / $steps" | bc)
    
    echo -ne "${YELLOW}Verarbeitung: ${NC}"
    hide_cursor
    
    for ((i=0; i<=$steps; i++)); do
        local percentage=$((i * 100 / steps))
        local completed=$((i * steps / steps))
        local remaining=$((steps - completed))
        
        echo -ne "\r${YELLOW}Verarbeitung: ${NC}["
        echo -ne "${GREEN}"
        for ((j=0; j<completed; j++)); do echo -ne "#"; done
        echo -ne "${NC}"
        for ((j=0; j<remaining; j++)); do echo -ne "."; done
        echo -ne "] ${BLUE}${percentage}%${NC}"
        
        sleep $delay
    done
    
    echo -e "\r${YELLOW}Verarbeitung: ${GREEN}[####################] ${BLUE}100%${NC}"
    show_cursor
}

# Benutzung der Fortschrittsanzeige
echo "Starte Verarbeitung..."
progress_bar 5  # 5 Sekunden Dauer

echo -e "\n${GREEN}Verarbeitung abgeschlossen!${NC}"

11.5.7 Tastaturnavigation und Tastenevents

Sie können auch einzelne Tastenanschläge ohne Enter abfangen:

#!/bin/bash

# Funktion zum Erfassen eines einzelnen Zeichens ohne Enter
get_char() {
    IFS= read -r -s -n 1 char
    echo "$char"
}

# Pfeilnavigation
echo "Verwende die Pfeiltasten zum Navigieren (q zum Beenden):"
selected=1
max_options=4

while true; do
    # Menü anzeigen
    clear
    echo "Menüoptionen:"
    for i in $(seq 1 $max_options); do
        if [ $i -eq $selected ]; then
            echo -e " > Option $i"
        else
            echo -e "   Option $i"
        fi
    done
    
    # Taste abfangen
    char=$(get_char)
    
    case "$char" in
        A) # Pfeil nach oben
            ((selected--))
            if [ $selected -lt 1 ]; then
                selected=$max_options
            fi
            ;;
        B) # Pfeil nach unten
            ((selected++))
            if [ $selected -gt $max_options ]; then
                selected=1
            fi
            ;;
        q|Q)
            echo "Beenden..."
            exit 0
            ;;
        "")
            echo "Option $selected ausgewählt!"
            read -n 1 -p "Drücke eine Taste, um fortzufahren..."
            ;;
    esac
done

11.5.8 Interaktive Wizards für mehrstufige Konfigurationen

Für komplexe Konfigurationen können Sie einen mehrstufigen Wizard implementieren:

#!/bin/bash

# Arrays für die Konfigurationsdaten
declare -A config

# Farben
GREEN='\033[0;32m'
BLUE='\033[0;34m'
NC='\033[0m'

# Wizard-Header
show_header() {
    clear
    echo -e "${BLUE}===================================${NC}"
    echo -e "${BLUE}   Webserver-Konfigurationswizard  ${NC}"
    echo -e "${BLUE}===================================${NC}"
    echo
    echo -e "Schritt $1 von 4: $2"
    echo
}

# Schritt 1: Grundlegende Einstellungen
step_basic_settings() {
    show_header "1" "Grundlegende Einstellungen"
    
    read -p "Servername [localhost]: " config[server_name]
    config[server_name]=${config[server_name]:-localhost}
    
    read -p "Port [80]: " config[port]
    config[port]=${config[port]:-80}
    
    read -p "Dokumentenverzeichnis [/var/www/html]: " config[doc_root]
    config[doc_root]=${config[doc_root]:-/var/www/html}
    
    read -p "Weiter zum nächsten Schritt? (j/n) [j]: " next
    next=${next:-j}
    
    if [[ "$next" =~ ^[jJ]$ ]]; then
        step_ssl_settings
    else
        step_basic_settings
    fi
}

# Schritt 2: SSL-Einstellungen
step_ssl_settings() {
    show_header "2" "SSL-Einstellungen"
    
    read -p "SSL aktivieren? (j/n) [n]: " ssl_enabled
    ssl_enabled=${ssl_enabled:-n}
    
    if [[ "$ssl_enabled" =~ ^[jJ]$ ]]; then
        config[ssl_enabled]=true
        read -p "SSL-Zertifikatspfad: " config[ssl_cert]
        read -p "SSL-Schlüsselpfad: " config[ssl_key]
    else
        config[ssl_enabled]=false
    fi
    
    read -p "Weiter zum nächsten Schritt? (j/n) [j]: " next
    next=${next:-j}
    
    if [[ "$next" =~ ^[jJ]$ ]]; then
        step_advanced_settings
    else
        step_ssl_settings
    fi
}

# Schritt 3: Erweiterte Einstellungen
step_advanced_settings() {
    show_header "3" "Erweiterte Einstellungen"
    
    read -p "Maximale Verbindungen [150]: " config[max_connections]
    config[max_connections]=${config[max_connections]:-150}
    
    read -p "Timeout in Sekunden [60]: " config[timeout]
    config[timeout]=${config[timeout]:-60}
    
    read -p "Weiter zum nächsten Schritt? (j/n) [j]: " next
    next=${next:-j}
    
    if [[ "$next" =~ ^[jJ]$ ]]; then
        step_summary
    else
        step_advanced_settings
    fi
}

# Schritt 4: Zusammenfassung und Bestätigung
step_summary() {
    show_header "4" "Zusammenfassung und Bestätigung"
    
    echo "Konfigurationsübersicht:"
    echo "-----------------------"
    echo "Servername: ${config[server_name]}"
    echo "Port: ${config[port]}"
    echo "Dokumentenverzeichnis: ${config[doc_root]}"
    echo "SSL aktiviert: ${config[ssl_enabled]}"
    
    if [[ "${config[ssl_enabled]}" == "true" ]]; then
        echo "SSL-Zertifikat: ${config[ssl_cert]}"
        echo "SSL-Schlüssel: ${config[ssl_key]}"
    fi
    
    echo "Maximale Verbindungen: ${config[max_connections]}"
    echo "Timeout: ${config[timeout]} Sekunden"
    echo "-----------------------"
    
    read -p "Konfiguration anwenden? (j/n) [j]: " apply
    apply=${apply:-j}
    
    if [[ "$apply" =~ ^[jJ]$ ]]; then
        apply_configuration
    else
        echo "Konfiguration abgebrochen."
        exit 0
    fi
}

# Konfiguration anwenden
apply_configuration() {
    echo
    echo -e "${GREEN}Konfiguration wird angewendet...${NC}"
    
    # Hier würde die tatsächliche Konfigurationserstellung stattfinden
    # Zum Beispiel:
    #
    # cat > webserver.conf << EOF
    # server {
    #     listen ${config[port]};
    #     server_name ${config[server_name]};
    #     root ${config[doc_root]};
    #     ...
    # }
    # EOF
    
    # Hier nur eine Simulation:
    echo "Erstelle Konfigurationsdatei..."
    sleep 1
    echo "Überprüfe Konfigurationsdatei..."
    sleep 1
    echo "Starte Webserver neu..."
    sleep 2
    
    echo -e "${GREEN}Konfiguration erfolgreich angewendet!${NC}"
}

# Wizard starten
step_basic_settings

11.5.9 Tastatureingaben abfangen und verarbeiten

Bei fortgeschrittenen interaktiven Skripten können Sie auch direkt mit Tastatursignalen arbeiten:

#!/bin/bash

# Terminal in den "raw" Modus versetzen
old_stty_settings=$(stty -g)
stty raw -echo

# Bei Beendigung des Skripts Terminal zurücksetzen
trap 'stty "$old_stty_settings"; echo; exit 0' EXIT INT TERM

echo "Verwende die WASD-Tasten zum Bewegen (q zum Beenden):"

# Spielerposition
x=10
y=5

# Bildschirm aktualisieren
update_screen() {
    clear
    # Spielfeld zeichnen
    for ((i=1; i<=20; i++)); do
        for ((j=1; j<=40; j++)); do
            if [ "$i" -eq "$y" ] && [ "$j" -eq "$x" ]; then
                echo -n "X"
            else
                echo -n "."
            fi
        done
        echo
    done
    echo "Position: $x,$y"
}

# Hauptschleife
while true; do
    update_screen
    
    # Taste lesen
    read -r -n 1 key
    
    # Taste verarbeiten
    case "$key" in
        w|W) ((y--)) ;;
        a|A) ((x--)) ;;
        s|S) ((y++)) ;;
        d|D) ((x++)) ;;
        q|Q) break ;;
    esac
    
    # Grenzen einhalten
    if [ "$x" -lt 1 ]; then x=1; fi
    if [ "$x" -gt 40 ]; then x=40; fi
    if [ "$y" -lt 1 ]; then y=1; fi
    if [ "$y" -gt 20 ]; then y=20; fi
done

11.5.10 Graphische Elemente in Text-UI

Selbst in textbasierten Terminals können Sie bestimmte grafische Elemente simulieren:

#!/bin/bash

# Funktion zum Zeichnen eines Rahmens
draw_box() {
    local width=$1
    local height=$2
    local title=$3
    
    # Obere Kante
    echo -n "┌"
    printf "─%.0s" $(seq 1 $((width-2)))
    echo "┐"
    
    # Titelzeile, wenn vorhanden
    if [ -n "$title" ]; then
        local padding=$(( (width - ${#title} - 2) / 2 ))
        echo -n "│"
        printf " %.0s" $(seq 1 $padding)
        echo -n "$title"
        printf " %.0s" $(seq 1 $((width - ${#title} - 2 - padding)))
        echo "│"
        
        # Trennlinie unter dem Titel
        echo -n "├"
        printf "─%.0s" $(seq 1 $((width-2)))
        echo "┤"
    fi
    
    # Inhalt (leer)
    for ((i=0; i<height-2-(title?1:0); i++)); do
        echo -n "│"
        printf " %.0s" $(seq 1 $((width-2)))
        echo "│"
    done
    
    # Untere Kante
    echo -n "└"
    printf "─%.0s" $(seq 1 $((width-2)))
    echo "┘"
}

# Funktion für Radiobuttons
radio_buttons() {
    local options=("$@")
    local selected=0
    
    while true; do
        clear
        echo "Wähle eine Option (↑/↓ zum Navigieren, Enter zum Auswählen, q zum Beenden):"
        echo
        
        for i in "${!options[@]}"; do
            if [ "$i" -eq "$selected" ]; then
                echo -e "  (●) ${options[$i]}"
            else
                echo -e "  (○) ${options[$i]}"
            fi
        done
        
        IFS= read -r -s -n 1 key
        
        case "$key" in
            A) # Pfeil nach oben
                ((selected--))
                if [ "$selected" -lt 0 ]; then
                    selected=$((${#options[@]}-1))
                fi
                ;;
            B) # Pfeil nach unten
                ((selected++))
                if [ "$selected" -ge "${#options[@]}" ]; then
                    selected=0
                fi
                ;;
            q|Q)
                return 255
                ;;
            "")
                return "$selected"
                ;;
        esac
    done
}

# Funktion für Checkboxen
checkboxes() {
    local options=("$@")
    local selected=0
    local -a checked
    
    # Alle Checkboxen initial deaktivieren
    for i in "${!options[@]}"; do
        checked[$i]=0
    done
    
    while true; do
        clear
        echo "Wähle Optionen (↑/↓ zum Navigieren, Leertaste zum Umschalten, Enter zum Bestätigen):"
        echo
        
        for i in "${!options[@]}"; do
            if [ "$i" -eq "$selected" ]; then
                if [ "${checked[$i]}" -eq 1 ]; then
                    echo -e " ▶[✓] ${options[$i]}"
                else
                    echo -e " ▶[ ] ${options[$i]}"
                fi
            else
                if [ "${checked[$i]}" -eq 1 ]; then
                    echo -e "  [✓] ${options[$i]}"
                else
                    echo -e "  [ ] ${options[$i]}"
                fi
            fi
        done
        
        IFS= read -r -s -n 1 key
        
        case "$key" in
            A) # Pfeil nach oben
                ((selected--))
                if [ "$selected" -lt 0 ]; then
                    selected=$((${#options[@]}-1))
                fi
                ;;
            B) # Pfeil nach unten
                ((selected++))
                if [ "$selected" -ge "${#options[@]}" ]; then
                    selected=0
                fi
                ;;
            " ") # Leertaste zum Umschalten
                if [ "${checked[$selected]}" -eq 0 ]; then
                    checked[$selected]=1
                else
                    checked[$selected]=0
                fi
                ;;
            q|Q)
                return 255
                ;;
            "")
                # Gib ausgewählte Optionen zurück
                local result=""
                for i in "${!options[@]}"; do
                    if [ "${checked[$i]}" -eq 1 ]; then
                        result+="${options[$i]},"
                    fi
                done
                echo "${result%,}" # Entferne das letzte Komma
                return 0
                ;;
        esac
    done
}

# Beispielverwendung
draw_box 50 10 "Konfigurationsassistent"

echo "Wähle dein Betriebssystem:"
radio_buttons "Linux" "Windows" "macOS" "FreeBSD" "Anderes"
os_choice=$?
os_name="${@:1:$((os_choice+1)):1}"

echo "Du hast $os_name gewählt."

echo -e "\nWähle die zu installierenden Komponenten:"
components=$(checkboxes "Webserver" "Datenbank" "PHP" "Mail" "Proxy")

echo "Du hast folgende Komponenten gewählt: $components"

11.6 Terminalsteuerung und Farbausgabe

Die Fähigkeit, das Terminal zu steuern und farbige Ausgaben zu erzeugen, kann Shell-Skripte erheblich aufwerten. Mit diesen Techniken können Sie die Benutzerfreundlichkeit verbessern, wichtige Informationen hervorheben und komplexere textbasierte Benutzeroberflächen erstellen. In diesem Abschnitt werden wir verschiedene Methoden untersuchen, um das Terminal zu steuern und farbige, formatierte Ausgaben zu erzeugen.

11.6.1 ANSI-Escape-Sequenzen: Die Grundlage

ANSI-Escape-Sequenzen sind die Basis für Terminalsteuerung und Farbausgabe. Diese speziellen Zeichenfolgen beginnen mit dem Escape-Zeichen (\033 oder \e) und werden vom Terminal interpretiert, um verschiedene Formatierungen zu erzeugen oder Steuerungsbefehle auszuführen.

Die grundlegende Syntax einer ANSI-Escape-Sequenz ist:

\033[Parameter;Parameter;...m

11.6.1.1 Grundlegende Textformatierung

Hier sind die häufigsten Formatierungsparameter:

#!/bin/bash

# Textformatierung
echo -e "\033[1mFettgedruckter Text\033[0m"
echo -e "\033[3mKursiver Text\033[0m"  # Nicht von allen Terminals unterstützt
echo -e "\033[4mUnterstrichener Text\033[0m"
echo -e "\033[9mDurchgestrichener Text\033[0m"  # Nicht von allen Terminals unterstützt

# Zurücksetzen aller Formatierungen
echo -e "Normaler Text \033[1mFettgedruckt\033[0m Wieder normal"

11.6.1.2 Vordergrund- und Hintergrundfarben

Hier sind die gängigsten Farbcodes:

#!/bin/bash

# Vordergrundfarben (Text)
echo -e "\033[30mSchwarz\033[0m"
echo -e "\033[31mRot\033[0m"
echo -e "\033[32mGrün\033[0m"
echo -e "\033[33mGelb\033[0m"
echo -e "\033[34mBlau\033[0m"
echo -e "\033[35mMagenta\033[0m"
echo -e "\033[36mCyan\033[0m"
echo -e "\033[37mWeiß\033[0m"

# Helle Vordergrundfarben
echo -e "\033[90mHelles Schwarz (Grau)\033[0m"
echo -e "\033[91mHelles Rot\033[0m"
echo -e "\033[92mHelles Grün\033[0m"
echo -e "\033[93mHelles Gelb\033[0m"
echo -e "\033[94mHelles Blau\033[0m"
echo -e "\033[95mHelles Magenta\033[0m"
echo -e "\033[96mHelles Cyan\033[0m"
echo -e "\033[97mHelles Weiß\033[0m"

# Hintergrundfarben
echo -e "\033[40mSchwarz\033[0m"
echo -e "\033[41mRot\033[0m"
echo -e "\033[42mGrün\033[0m"
echo -e "\033[43mGelb\033[0m"
echo -e "\033[44mBlau\033[0m"
echo -e "\033[45mMagenta\033[0m"
echo -e "\033[46mCyan\033[0m"
echo -e "\033[47mWeiß\033[0m"

# Helle Hintergrundfarben
echo -e "\033[100mHelles Schwarz (Grau)\033[0m"
echo -e "\033[101mHelles Rot\033[0m"
echo -e "\033[102mHelles Grün\033[0m"
echo -e "\033[103mHelles Gelb\033[0m"
echo -e "\033[104mHelles Blau\033[0m"
echo -e "\033[105mHelles Magenta\033[0m"
echo -e "\033[106mHelles Cyan\033[0m"
echo -e "\033[107mHelles Weiß\033[0m"

11.6.1.3 Kombination von Formatierungen

Sie können mehrere Formatierungen in einer Sequenz kombinieren:

# Kombinierte Formatierungen
echo -e "\033[1;31mFettgedruckter roter Text\033[0m"
echo -e "\033[4;34;43mUnterstrichener blauer Text auf gelbem Hintergrund\033[0m"

# Komplexes Beispiel
echo -e "\033[1;37;44mWeiß, fett auf blauem Hintergrund\033[0m \033[4;31mRot unterstrichen\033[0m"

11.6.2 Farbcodes in Variablen für bessere Lesbarkeit

Die Verwendung von Variablen für Farbcodes verbessert die Lesbarkeit Ihrer Skripte erheblich:

#!/bin/bash

# Farbdefinitionen
RESET="\033[0m"
BOLD="\033[1m"
UNDERLINE="\033[4m"
BLACK="\033[30m"
RED="\033[31m"
GREEN="\033[32m"
YELLOW="\033[33m"
BLUE="\033[34m"
MAGENTA="\033[35m"
CYAN="\033[36m"
WHITE="\033[37m"
BG_BLACK="\033[40m"
BG_RED="\033[41m"
BG_GREEN="\033[42m"
BG_YELLOW="\033[43m"
BG_BLUE="\033[44m"
BG_MAGENTA="\033[45m"
BG_CYAN="\033[46m"
BG_WHITE="\033[47m"

# Verwendung
echo -e "${RED}Fehler:${RESET} Datei nicht gefunden."
echo -e "${GREEN}Erfolg:${RESET} Operation abgeschlossen."
echo -e "${YELLOW}Warnung:${RESET} Festplatte fast voll."
echo -e "${BOLD}${BLUE}Information:${RESET} System bereit."

11.6.3 Tput: Ein Portabler Ansatz

Eine alternative und portablere Methode zur Terminalsteuerung ist die Verwendung des tput-Befehls, der die Termcap- oder Terminfo-Datenbank des Systems nutzt:

#!/bin/bash

# Farbdefinitionen mit tput
RESET=$(tput sgr0)
BOLD=$(tput bold)
UNDERLINE=$(tput smul)
REVERSE=$(tput rev)
BLACK=$(tput setaf 0)
RED=$(tput setaf 1)
GREEN=$(tput setaf 2)
YELLOW=$(tput setaf 3)
BLUE=$(tput setaf 4)
MAGENTA=$(tput setaf 5)
CYAN=$(tput setaf 6)
WHITE=$(tput setaf 7)
BG_BLACK=$(tput setab 0)
BG_RED=$(tput setab 1)
BG_GREEN=$(tput setab 2)
BG_YELLOW=$(tput setab 3)
BG_BLUE=$(tput setab 4)
BG_MAGENTA=$(tput setab 5)
BG_CYAN=$(tput setab 6)
BG_WHITE=$(tput setab 7)

# Verwendung
echo "${RED}Fehler:${RESET} Datei nicht gefunden."
echo "${GREEN}Erfolg:${RESET} Operation abgeschlossen."
echo "${YELLOW}Warnung:${RESET} Festplatte fast voll."
echo "${BOLD}${BLUE}Information:${RESET} System bereit."

# Terminalbreite und -höhe ermitteln
COLUMNS=$(tput cols)
LINES=$(tput lines)
echo "Terminal-Größe: ${COLUMNS}x${LINES} Zeichen"

Der Vorteil von tput ist, dass es die Fähigkeiten des aktuellen Terminals erkennt und entsprechend anpasst, was die Portabilität Ihrer Skripte verbessert.

11.6.4 Terminalsteuerungsbefehle

Neben Farbformatierungen können Sie das Terminal auch steuern, um den Cursor zu bewegen, den Bildschirm zu löschen und mehr:

#!/bin/bash

# Bildschirm löschen (Alternative zu 'clear')
clear_screen() {
    echo -e "\033[2J\033[H"
    # oder mit tput:
    # tput clear
}

# Cursor an Position x,y bewegen
cursor_position() {
    echo -e "\033[${2};${1}H"
    # oder mit tput:
    # tput cup $2 $1
}

# Aktuelle Zeile löschen
clear_line() {
    echo -e "\033[2K"
    # oder mit tput:
    # tput el
}

# Cursor unsichtbar machen
hide_cursor() {
    echo -e "\033[?25l"
    # oder mit tput:
    # tput civis
}

# Cursor sichtbar machen
show_cursor() {
    echo -e "\033[?25h"
    # oder mit tput:
    # tput cnorm
}

# Text blinken lassen (wird nicht von allen Terminals unterstützt)
blink_text() {
    echo -e "\033[5m$1\033[0m"
}

# Den gesamten Bildschirm speichern und wiederherstellen
save_screen() {
    echo -e "\033[?47h"
}

restore_screen() {
    echo -e "\033[?47l"
}

# Beispielverwendung
clear_screen
cursor_position 10 5
echo "Text an Position 10,5"
sleep 2

hide_cursor
for i in {1..10}; do
    cursor_position 1 10
    clear_line
    echo -n "Zähler: $i"
    sleep 0.5
done
show_cursor

cursor_position 1 12
echo "Fertig!"

11.6.5 Praktische Anwendungsbeispiele

11.6.5.1 Statusmeldungen mit Farbkodierung

#!/bin/bash

# Farbdefinitionen
RESET="\033[0m"
RED="\033[31m"
GREEN="\033[32m"
YELLOW="\033[33m"
BLUE="\033[34m"

# Funktionen für Statusmeldungen
log_error() {
    echo -e "${RED}[FEHLER]${RESET} $1"
}

log_success() {
    echo -e "${GREEN}[ERFOLG]${RESET} $1"
}

log_warning() {
    echo -e "${YELLOW}[WARNUNG]${RESET} $1"
}

log_info() {
    echo -e "${BLUE}[INFO]${RESET} $1"
}

# Beispielverwendung
log_info "Starte Skript..."
log_warning "Festplattenplatz niedrig"
log_error "Datei konnte nicht geöffnet werden"
log_success "Datenbank erfolgreich gesichert"

11.6.5.2 Hervorhebung von Suchergebnissen

#!/bin/bash

# Funktion zum Hervorheben von Suchbegriffen
highlight_matches() {
    local search_term="$1"
    local file="$2"
    local YELLOW="\033[1;33m"
    local RESET="\033[0m"
    
    if [ -f "$file" ]; then
        cat "$file" | sed "s/$search_term/${YELLOW}$search_term${RESET}/gi"
    else
        echo "Datei nicht gefunden: $file"
    fi
}

# Beispielverwendung
echo "Suche nach 'error' in der Logdatei:"
highlight_matches "error" "/var/log/syslog"

11.6.5.3 Fortschrittsanzeige mit Farben

#!/bin/bash

# Farbdefinitionen
RESET="\033[0m"
GREEN="\033[32m"
YELLOW="\033[33m"
BLUE="\033[34m"

# Fortschrittsbalken anzeigen
show_progress() {
    local total=$1
    local current=$2
    local bar_length=50
    local filled_length=$((current * bar_length / total))
    
    # Prozentberechnung
    local percent=$((current * 100 / total))
    
    # Fortschrittsbalken erstellen
    local bar=""
    for ((i=0; i<filled_length; i++)); do
        bar="${bar}█"
    done
    for ((i=filled_length; i<bar_length; i++)); do
        bar="${bar}░"
    done
    
    # Cursor an den Anfang der Zeile bewegen und Zeile löschen
    echo -ne "\r\033[K"
    
    # Fortschrittsbalken mit Farben ausgeben
    echo -ne "${YELLOW}[${GREEN}${bar}${YELLOW}] ${BLUE}${percent}%${RESET}"
    
    # Wenn fertig, neue Zeile hinzufügen
    if [ "$current" -eq "$total" ]; then
        echo
    fi
}

# Beispielverwendung - Simulation eines längeren Prozesses
total_steps=100

for ((i=0; i<=total_steps; i++)); do
    show_progress $total_steps $i
    sleep 0.05
done

echo -e "${GREEN}Prozess abgeschlossen!${RESET}"

11.6.5.4 Interaktives Menü mit farbiger Hervorhebung

#!/bin/bash

# Farbdefinitionen
RESET="\033[0m"
BOLD="\033[1m"
BLACK="\033[30m"
RED="\033[31m"
GREEN="\033[32m"
YELLOW="\033[33m"
BLUE="\033[34m"
MAGENTA="\033[35m"
CYAN="\033[36m"
WHITE="\033[37m"
BG_BLACK="\033[40m"
BG_RED="\033[41m"
BG_GREEN="\033[42m"
BG_YELLOW="\033[43m"
BG_BLUE="\033[44m"
BG_MAGENTA="\033[45m"
BG_CYAN="\033[46m"
BG_WHITE="\033[47m"

# Cursor unsichtbar machen
echo -e "\033[?25l"

# Bei Beendigung des Skripts Cursor wieder sichtbar machen
trap 'echo -e "\033[?25h"; exit 0' SIGINT SIGTERM EXIT

# Menü anzeigen
selected=0
options=("System-Info anzeigen" "Festplattennutzung prüfen" "Prozesse auflisten" "Beenden")

draw_menu() {
    clear
    echo -e "${BOLD}${BG_BLUE}${WHITE}               Hauptmenü               ${RESET}"
    echo
    
    for i in "${!options[@]}"; do
        if [ "$i" -eq "$selected" ]; then
            echo -e "  ${BG_CYAN}${BLACK} $(($i+1)) ${RESET} ${BOLD}${YELLOW}${options[$i]}${RESET}"
        else
            echo -e "  ${BG_BLACK}${WHITE} $(($i+1)) ${RESET} ${options[$i]}"
        fi
    done
    
    echo
    echo -e "${BLUE}Verwende die Pfeiltasten zum Navigieren und Enter zum Auswählen${RESET}"
}

# Tastatureingabe verarbeiten
get_key() {
    IFS= read -r -s -n1 key
    case $key in
        'A') echo "UP";;    # Pfeil nach oben
        'B') echo "DOWN";;  # Pfeil nach unten
        '') echo "ENTER";;
        'q'|'Q') echo "QUIT";;
        *) ;;
    esac
}

# Hauptschleife
while true; do
    draw_menu
    
    # Auf Tastendruck warten
    read -s -n 1 key
    
    if [[ $key == $'\e' ]]; then
        read -s -n 2 key
        case $key in
            '[A') # Pfeil nach oben
                ((selected--))
                if [ "$selected" -lt 0 ]; then
                    selected=$((${#options[@]}-1))
                fi
                ;;
            '[B') # Pfeil nach unten
                ((selected++))
                if [ "$selected" -ge "${#options[@]}" ]; then
                    selected=0
                fi
                ;;
        esac
    elif [[ $key == "" ]]; then # Enter-Taste
        case $selected in
            0) 
                clear
                echo -e "${BOLD}${GREEN}=== Systeminformationen ===${RESET}\n"
                echo -e "${CYAN}Hostname:${RESET} $(hostname)"
                echo -e "${CYAN}Kernel:${RESET} $(uname -r)"
                echo -e "${CYAN}Uptime:${RESET} $(uptime -p)"
                echo -e "${CYAN}CPU:${RESET} $(grep 'model name' /proc/cpuinfo | head -1 | cut -d':' -f2 | sed 's/^[ \t]*//')"
                echo -e "${CYAN}Arbeitsspeicher:${RESET}"
                free -h
                echo
                read -n 1 -s -r -p "Drücke eine Taste, um fortzufahren..."
                ;;
            1)
                clear
                echo -e "${BOLD}${GREEN}=== Festplattennutzung ===${RESET}\n"
                df -h | awk '{print $1, $2, $3, $4, $5, $6}' | grep -v "tmpfs" | 
                while read fs size used avail use mount; do
                    if [ "$fs" = "Filesystem" ]; then
                        echo -e "${BOLD}${fs} ${size} ${used} ${avail} ${use} ${mount}${RESET}"
                    else
                        use_num=$(echo $use | sed 's/%//')
                        if [ "$use_num" -ge 90 ]; then
                            color=$RED
                        elif [ "$use_num" -ge 70 ]; then
                            color=$YELLOW
                        else
                            color=$GREEN
                        fi
                        echo -e "${fs} ${size} ${used} ${avail} ${color}${use}${RESET} ${mount}"
                    fi
                done
                echo
                read -n 1 -s -r -p "Drücke eine Taste, um fortzufahren..."
                ;;
            2)
                clear
                echo -e "${BOLD}${GREEN}=== Prozessliste ===${RESET}\n"
                ps aux | head -1 | awk '{print $1, $2, $3, $4, $11}' | 
                while read user pid cpu mem cmd; do
                    echo -e "${BOLD}${user} ${pid} ${cpu} ${mem} ${cmd}${RESET}"
                done
                
                ps aux | grep -v "USER" | sort -rn -k 3 | head -10 | awk '{print $1, $2, $3, $4, $11}' | 
                while read user pid cpu mem cmd; do
                    cpu_num=$(echo $cpu | sed 's/,/./') # Für Lokalisierungen, die Komma verwenden
                    if (( $(echo "$cpu_num > 10.0" | bc -l) )); then
                        color=$RED
                    elif (( $(echo "$cpu_num > 5.0" | bc -l) )); then
                        color=$YELLOW
                    else
                        color=$GREEN
                    fi
                    echo -e "${user} ${pid} ${color}${cpu}${RESET} ${mem} ${cmd}"
                done
                echo
                read -n 1 -s -r -p "Drücke eine Taste, um fortzufahren..."
                ;;
            3)
                echo -e "\n${BOLD}${GREEN}Programm wird beendet...${RESET}"
                sleep 1
                exit 0
                ;;
        esac
    elif [[ $key == "q" || $key == "Q" ]]; then
        echo -e "\n${BOLD}${GREEN}Programm wird beendet...${RESET}"
        sleep 1
        exit 0
    fi
done

11.6.6 256-Farben und True-Color-Unterstützung

Moderne Terminals unterstützen über die grundlegenden 16 Farben hinaus auch 256 Farben oder sogar True Color (16 Millionen Farben):

#!/bin/bash

# 256-Farben-Palette anzeigen
show_256_colors() {
    echo "256-Farben-Palette:"
    for i in {0..255}; do
        printf "\033[38;5;${i}m%3d\033[0m " $i
        if (( (i+1) % 16 == 0 )); then
            echo
        fi
    done
    echo
}

# Eine einzelne 256-Farbe verwenden
use_256_color() {
    local color_code=$1
    local text=$2
    echo -e "\033[38;5;${color_code}m${text}\033[0m"
}

# True Color (24-bit) verwenden
use_true_color() {
    local r=$1
    local g=$2
    local b=$3
    local text=$4
    echo -e "\033[38;2;${r};${g};${b}m${text}\033[0m"
}

# Beispiele
show_256_colors

echo
echo "Beispiele für 256-Farben:"
use_256_color 196 "Helles Rot (196)"
use_256_color 46 "Helles Grün (46)"
use_256_color 27 "Blau (27)"
use_256_color 208 "Orange (208)"
use_256_color 165 "Magenta (165)"

echo
echo "Beispiele für True Color:"
use_true_color 255 100 100 "Pastellrot (255,100,100)"
use_true_color 100 255 100 "Pastellgrün (100,255,100)"
use_true_color 100 100 255 "Pastellblau (100,100,255)"
use_true_color 255 165 0 "Orange (255,165,0)"
use_true_color 128 0 128 "Lila (128,0,128)"

# Farbverlauf mit True Color
echo
echo "Farbverlauf mit True Color:"
for i in {0..50}; do
    r=$((255 - i * 5))
    g=$((i * 5))
    b=100
    use_true_color $r $g $b "#"
done
echo

11.6.7 Farbige Rahmen und Box-Zeichnung

Mit Unicode-Box-Zeichenzeichen können Sie ansprechende Rahmen und Kästen erstellen:

#!/bin/bash

# Farbdefinitionen
RESET="\033[0m"
RED="\033[31m"
GREEN="\033[32m"
YELLOW="\033[33m"
BLUE="\033[34m"
MAGENTA="\033[35m"
CYAN="\033[36m"

# Funktion zum Zeichnen einer Box
draw_box() {
    local width=$1
    local title=$2
    local color=$3
    local content=$4
    
    # Obere Kante
    echo -e "${color}$( printf '─%.0s' $(seq 1 $((width-2))) )${RESET}"
    
    # Titel, falls vorhanden
    if [ -n "$title" ]; then
        local padding=$(( (width - ${#title} - 2) / 2 ))
        local extra_padding=$(( (width - ${#title} - 2) % 2 ))
        
        echo -en "${color}${RESET}"
        printf " %.0s" $(seq 1 $padding)
        echo -en "${YELLOW}${title}${RESET}"
        printf " %.0s" $(seq 1 $((padding + extra_padding)))
        echo -e "${color}${RESET}"
        
        # Trennlinie unter dem Titel
        echo -e "${color}$( printf '─%.0s' $(seq 1 $((width-2))) )${RESET}"
    fi
    
    # Inhalt, Zeile für Zeile
    IFS=$'\n'
    for line in $content; do
        local line_padding=$(( width - ${#line} - 2 ))
        
        echo -en "${color}${RESET} "
        echo -n "$line"
        printf " %.0s" $(seq 1 $line_padding)
        echo -e "${color}${RESET}"
    done
    
    # Untere Kante
    echo -e "${color}$( printf '─%.0s' $(seq 1 $((width-2))) )${RESET}"
}

# Beispielverwendung
system_info=$(cat << EOF
Hostname: $(hostname)
Kernel: $(uname -r)
Uptime: $(uptime -p)
CPU: $(grep -m1 'model name' /proc/cpuinfo | cut -d':' -f2 | sed 's/^[ \t]*//')
EOF
)

disk_info=$(df -h / /home | grep -v "Filesystem")

draw_box 60 "Systeminformationen" $BLUE "$system_info"
echo
draw_box 60 "Festplattennutzung" $GREEN "$disk_info"
echo

# Mehrere Boxen nebeneinander
cpu_info="CPU: $(grep -m1 'model name' /proc/cpuinfo | cut -d':' -f2 | sed 's/^[ \t]*//')"
mem_info="Speicher: $(free -h | grep Mem | awk '{print $3 " / " $2 " verwendet"}')"

echo -en "${RED}┌───────────────────────────┐${RESET}  "
echo -e "${CYAN}┌───────────────────────────┐${RESET}"

echo -en "${RED}${RESET} ${YELLOW}CPU-Information${RESET}         ${RED}${RESET}  "
echo -e "${CYAN}${RESET} ${YELLOW}Speicher-Information${RESET}    ${CYAN}${RESET}"

echo -en "${RED}├───────────────────────────┤${RESET}  "
echo -e "${CYAN}├───────────────────────────┤${RESET}"

echo -en "${RED}${RESET} $cpu_info ${RED}${RESET}  "
echo -e "${CYAN}${RESET} $mem_info ${CYAN}${RESET}"

echo -en "${RED}└───────────────────────────┘${RESET}  "
echo -e "${CYAN}└───────────────────────────┘${RESET}"

11.6.8 Statusanzeigen und Spinner

Animierte Statusanzeigen können die Benutzerfreundlichkeit bei länger laufenden Operationen verbessern:

#!/bin/bash

# Farbdefinitionen
RESET="\033[0m"
GREEN="\033[32m"
YELLOW="\033[33m"
BLUE="\033[34m"
MAGENTA="\033[35m"
CYAN="\033[36m"

# Cursor unsichtbar machen
hide_cursor() {
    echo -ne "\033[?25l"
}

# Cursor sichtbar machen
show_cursor() {
    echo -ne "\033[?25h"
}

# Bei Beendigung des Skripts Cursor wieder sichtbar machen
trap show_cursor EXIT INT TERM

# Einfacher animierter Spinner
spinner() {
    local pid=$1
    local delay=0.1
    local spinstr='|/-\'
    
    hide_cursor
    
    while ps -p $pid > /dev/null; do
        for i in $(seq 0 3); do
            echo -ne "\r[${spinstr:$i:1}] Läuft..."
            sleep $delay
        done
    done
    
    echo -e "\r[${GREEN}${RESET}] Abgeschlossen!     "
    show_cursor
}

# Spinner mit Farbrotation
fancy_spinner() {
    local pid=$1
    local msg=$2
    local delay=0.1
    local spinstr='⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏'
    local colors=("\033[31m" "\033[33m" "\033[32m" "\033[36m" "\033[34m" "\033[35m")
    
    hide_cursor
    
    while ps -p $pid > /dev/null; do
        for i in $(seq 0 9); do
            color_index=$((i % 6))
            echo -ne "\r${colors[$color_index]}${spinstr:$i:1}${RESET} $msg"
            sleep $delay
        done
    done
    
    echo -e "\r${GREEN}${RESET} $msg - Abgeschlossen!"
    show_cursor
}

# Fortschrittsbalken mit Prozentanzeige
progress_bar() {
    local pid=$1
    local msg=$2
    local total_time=$3  # Geschätzte Gesamtzeit in Sekunden
    local start_time=$(date +%s)
    local bar_length=30
    
    hide_cursor
    
    while ps -p $pid > /dev/null; do
        local current_time=$(date +%s)
        local elapsed=$((current_time - start_time))
        
        if [ $elapsed -gt $total_time ]; then
            local percent=99
        else
            local percent=$((elapsed * 100 / total_time))
        fi
        
        local filled_length=$((percent * bar_length / 100))
        local empty_length=$((bar_length - filled_length))
        
        local bar=""
        for ((i=0; i<filled_length; i++)); do
            bar="${bar}█"
        done
        for ((i=0; i<empty_length; i++)); do
            bar="${bar}░"
        done
        
        echo -ne "\r${BLUE}[$YELLOW$bar${BLUE}] ${GREEN}${percent}%${RESET} $msg"
        sleep 0.2
    done
    
    echo -ne "\r${BLUE}[${YELLOW}"
    for ((i=0; i<bar_length; i++)); do
        echo -n "█"
    done
    echo -e "${BLUE}] ${GREEN}100%${RESET} $msg - Abgeschlossen!"
    show_cursor
}

# Beispielverwendung
echo "Starte langwierigen Prozess..."

# Langwieriger Prozess im Hintergrund starten
{
    sleep 5
} &
# Prozess-ID speichern
bg_pid=$!

# Spinner anzeigen, während der Prozess läuft
spinner $bg_pid

echo "Starte Dateianalyse..."
{
    sleep 8
} &
bg_pid=$!

# Fancy Spinner anzeigen
fancy_spinner $bg_pid "Analysiere Dateien"

echo "Starte Backup..."
{
    sleep 10
} &
bg_pid=$!

# Fortschrittsbalken anzeigen
progress_bar $bg_pid "Sichere Datenbank" 10

echo -e "${GREEN}Alle Aufgaben abgeschlossen!${RESET}"

11.6.9 Countdown-Timer

Ein Countdown-Timer kann nützlich sein, um Benutzer über die verbleibende Zeit zu informieren:

#!/bin/bash

# Farbdefinitionen
RESET="\033[0m"
RED="\033[31m"
YELLOW="\033[33m"
GREEN="\033[32m"

# Countdown-Timer mit Farbwechsel
countdown_timer() {
    local seconds=$1
    local message=$2
    
    for (( i=seconds; i>=0; i-- )); do
        # Farbwechsel je nach verbleibender Zeit
        if [ $i -gt 10 ]; then
            color=$GREEN
        elif [ $i -gt 5 ]; then
            color=$YELLOW
        else
            color=$RED
        fi
        
        # Sekunden in Minuten:Sekunden umwandeln
        local min=$((i / 60))
        local sec=$((i % 60))
        
        # Ausgabe mit führenden Nullen für Sekunden unter 10
        if [ $sec -lt 10 ]; then
            echo -ne "\r${message}: ${color}${min}:0${sec}${RESET} verbleibend"
        else
            echo -ne "\r${message}: ${color}${min}:${sec}${RESET} verbleibend"
        fi
        
        sleep 1
    done
    
    echo -e "\r${message}: ${GREEN}Abgeschlossen!${RESET}      "
}

# Beispielverwendung
echo "System wird in 30 Sekunden neu gestartet."
countdown_timer 30 "Neustart"
echo "System würde jetzt neu gestartet werden."

11.6.10 Anzeigetabellen und formatierte Daten

Für die Darstellung strukturierter Daten können Sie Tabellen mit Rahmen erstellen:

#!/bin/bash

# Farbdefinitionen
RESET="\033[0m"
BOLD="\033[1m"
GREEN="\033[32m"
BLUE="\033[34m"
YELLOW="\033[33m"

# Funktion zur Erstellung einer formatierten Tabelle
print_table() {
    local -a header=("${!1}")
    local -a data=("${!2}")
    local columns=${#header[@]}
    local -a column_widths
    
    # Berechnung der Spaltenbreiten
    for ((i=0; i<columns; i++)); do
        column_widths[$i]=${#header[$i]}
    done
    
    # Überprüfe Daten für maximale Spaltenbreite
    for ((i=0; i<${#data[@]}; i+=columns)); do
        for ((j=0; j<columns; j++)); do
            if [ ${#data[$i+$j]} -gt ${column_widths[$j]} ]; then
                column_widths[$j]=${#data[$i+$j]}
            fi
        done
    done
    
    # Tabellenkopf zeichnen
    echo -en "${BLUE}┌"
    for ((i=0; i<columns; i++)); do
        printf '─%.0s' $(seq 1 $((column_widths[$i] + 2)))
        if [ $i -lt $((columns-1)) ]; then
            echo -en "┬"
        fi
    done
    echo -e "┐${RESET}"
    
    # Header ausgeben
    echo -en "${BLUE}${RESET}"
    for ((i=0; i<columns; i++)); do
        printf " ${BOLD}%-${column_widths[$i]}s${RESET} " "${header[$i]}"
        echo -en "${BLUE}${RESET}"
    done
    echo
    
    # Trennlinie nach Header
    echo -en "${BLUE}├"
    for ((i=0; i<columns; i++)); do
        printf '─%.0s' $(seq 1 $((column_widths[$i] + 2)))
        if [ $i -lt $((columns-1)) ]; then
            echo -en "┼"
        fi
    done
    echo -e "┤${RESET}"
    
    # Datenzeilen ausgeben
    local rows=$((${#data[@]} / columns))
    for ((row=0; row<rows; row++)); do
        echo -en "${BLUE}${RESET}"
        for ((col=0; col<columns; col++)); do
            printf " %-${column_widths[$col]}s " "${data[$row*$columns+$col]}"
            echo -en "${BLUE}${RESET}"
        done
        echo
        
        # Trennlinie zwischen Datenzeilen, aber nicht nach der letzten Zeile
        if [ $row -lt $((rows-1)) ]; then
            echo -en "${BLUE}├"
            for ((i=0; i<columns; i++)); do
                printf '─%.0s' $(seq 1 $((column_widths[$i] + 2)))
                if [ $i -lt $((columns-1)) ]; then
                    echo -en "┼"
                fi
            done
            echo -e "┤${RESET}"
        fi
    done
    
    # Untere Kante
    echo -en "${BLUE}└"
    for ((i=0; i<columns; i++)); do
        printf '─%.0s' $(seq 1 $((column_widths[$i] + 2)))
        if [ $i -lt $((columns-1)) ]; then
            echo -en "┴"
        fi
    done
    echo -e "┘${RESET}"
}

# Beispielverwendung
header=("Hostname" "IP-Adresse" "Status" "Uptime")
data=(
    "server1" "192.168.1.101" "${GREEN}Online${RESET}" "45 Tage"
    "server2" "192.168.1.102" "${RED}Offline${RESET}" "0 Tage"
    "server3" "192.168.1.103" "${GREEN}Online${RESET}" "12 Tage"
    "server4" "192.168.1.104" "${YELLOW}Wartung${RESET}" "5 Tage"
)

echo -e "\n${BOLD}Serverstatus-Übersicht:${RESET}\n"
print_table header[@] data[@]

11.6.11 Hervorhebung von Textdifferenzen

Die Hervorhebung von Unterschieden zwischen Texten kann bei der Analyse von Konfigurationsdateien oder Log-Dateien hilfreich sein:

#!/bin/bash

# Farbdefinitionen
RESET="\033[0m"
RED="\033[31m"
GREEN="\033[32m"
BLUE="\033[34m"

# Funktion zum Vergleich und zur Hervorhebung von Unterschieden
highlight_diff() {
    local file1="$1"
    local file2="$2"
    
    if [ ! -f "$file1" ] || [ ! -f "$file2" ]; then
        echo "Eine oder beide Dateien existieren nicht."
        return 1
    fi
    
    echo -e "${BLUE}Vergleiche Dateien:${RESET}"
    echo -e "  ${BLUE}< ${file1}${RESET}"
    echo -e "  ${BLUE}> ${file2}${RESET}"
    echo
    
    # Zeilen vergleichen und Unterschiede hervorheben
    diff -u "$file1" "$file2" | sed -n '1,2d; s/^-/'"${RED}-/; s/^+/""${GREEN}+/; s/$/""${RESET}/; p"
    
    echo
    echo -e "${BLUE}Legende:${RESET}"
    echo -e "${RED}- Zeile in ${file1} entfernt${RESET}"
    echo -e "${GREEN}+ Zeile in ${file2} hinzugefügt${RESET}"
}

# Beispielverwendung
highlight_diff "config.orig" "config.new"

11.6.12 Terminalkompatibilität und Fallback-Mechanismen

Da nicht alle Terminals Farben oder erweiterte Steuerungen unterstützen, ist es wichtig, Fallback-Mechanismen zu implementieren:

#!/bin/bash

# Farbunterstützung prüfen und konfigurieren
setup_colors() {
    # Prüfe, ob das Terminal Farben unterstützt
    if [ -t 1 ] && [ -n "$TERM" ] && [ "$TERM" != "dumb" ]; then
        # Prüfe die Anzahl der unterstützten Farben
        ncolors=$(tput colors 2>/dev/null || echo 0)
        if [ -n "$ncolors" ] && [ $ncolors -ge 8 ]; then
            # Mindestens 8 Farben verfügbar
            RESET=$(tput sgr0)
            BOLD=$(tput bold)
            BLACK=$(tput setaf 0)
            RED=$(tput setaf 1)
            GREEN=$(tput setaf 2)
            YELLOW=$(tput setaf 3)
            BLUE=$(tput setaf 4)
            MAGENTA=$(tput setaf 5)
            CYAN=$(tput setaf 6)
            WHITE=$(tput setaf 7)
            HAS_COLORS=1
        else
            # Keine oder zu wenige Farben, deaktiviere Farben
            RESET=""
            BOLD=""
            BLACK=""
            RED=""
            GREEN=""
            YELLOW=""
            BLUE=""
            MAGENTA=""
            CYAN=""
            WHITE=""
            HAS_COLORS=0
        fi
    else
        # Nicht-interaktives Terminal oder "dumb"-Terminal, deaktiviere Farben
        RESET=""
        BOLD=""
        BLACK=""
        RED=""
        GREEN=""
        YELLOW=""
        BLUE=""
        MAGENTA=""
        CYAN=""
        WHITE=""
        HAS_COLORS=0
    fi
    
    # Exportiere die Farbvariablen für untergeordnete Skripte
    export RESET BOLD BLACK RED GREEN YELLOW BLUE MAGENTA CYAN WHITE HAS_COLORS
}

# Terminalbreite ermitteln oder Standardwert verwenden
get_terminal_width() {
    if [ -t 1 ] && command -v tput >/dev/null 2>&1; then
        tput cols 2>/dev/null || echo 80
    else
        echo 80
    fi
}

# Generische Funktion für Statusnachrichten, die mit oder ohne Farben funktioniert
print_status() {
    local type=$1
    local message=$2
    local width=$(get_terminal_width)
    local max_message_length=$((width - 10))
    
    # Kürze zu lange Nachrichten
    if [ ${#message} -gt $max_message_length ]; then
        message="${message:0:$((max_message_length-3))}..."
    fi
    
    if [ "$HAS_COLORS" -eq 1 ]; then
        case "$type" in
            info)    echo -e "${BLUE}[INFO]${RESET} $message" ;;
            success) echo -e "${GREEN}[ OK ]${RESET} $message" ;;
            warning) echo -e "${YELLOW}[WARN]${RESET} $message" ;;
            error)   echo -e "${RED}[FAIL]${RESET} $message" ;;
            *)       echo "       $message" ;;
        esac
    else
        case "$type" in
            info)    echo "[INFO] $message" ;;
            success) echo "[ OK ] $message" ;;
            warning) echo "[WARN] $message" ;;
            error)   echo "[FAIL] $message" ;;
            *)       echo "       $message" ;;
        esac
    fi
}

# Initialisiere Farben
setup_colors

# Beispielverwendung
print_status info "Skript gestartet"
print_status warning "Festplattenplatz wird knapp"
print_status error "Datei nicht gefunden"
print_status success "Backup erfolgreich abgeschlossen"

# Demo mit Balken-Anzeige
echo
echo "Beispiel für eine Fortschrittsanzeige mit Fallback:"
width=$(get_terminal_width)
bar_width=$((width - 10))

for i in $(seq 1 $bar_width); do
    percent=$((i * 100 / bar_width))
    
    # Berechne die Anzahl der gefüllten und leeren Zeichen
    filled=$((i * bar_width / bar_width))
    empty=$((bar_width - filled))
    
    # Erstelle den Fortschrittsbalken
    if [ "$HAS_COLORS" -eq 1 ]; then
        bar=""
        for ((j=0; j<filled; j++)); do
            bar="${bar}█"
        done
        for ((j=0; j<empty; j++)); do
            bar="${bar}░"
        done
        printf "\r${YELLOW}[${GREEN}${bar}${YELLOW}] ${BLUE}%3d%%${RESET}" $percent
    else
        bar=""
        for ((j=0; j<filled; j++)); do
            bar="${bar}#"
        done
        for ((j=0; j<empty; j++)); do
            bar="${bar}."
        done
        printf "\r[%s] %3d%%" "$bar" $percent
    fi
    
    if [ $i -lt $bar_width ]; then
        sleep 0.05
    fi
done
echo

11.7 Dialog und andere TUI-Werkzeuge

Während die bisher behandelten Techniken zur Terminal-Steuerung und Farbausgabe direkt in Shell-Skripten implementiert werden können, bieten spezialisierte TUI-Werkzeuge (Text User Interface) vorgefertigte Lösungen für komplexere Benutzeroberflächen. Diese Tools ermöglichen die Erstellung anspruchsvoller interaktiver Anwendungen, ohne dass Sie alle Einzelheiten der Terminalsteuerung selbst implementieren müssen. In diesem Abschnitt werden wir die wichtigsten TUI-Werkzeuge untersuchen, mit einem besonderen Fokus auf dialog, eines der am weitesten verbreiteten Tools dieser Art.

11.7.1 Dialog: Grundlagen und Installation

dialog ist ein Kommandozeilenprogramm, das vorgefertigte Dialog-Boxen für Shell-Skripte bereitstellt. Es ist in den meisten Linux-Distributionen verfügbar und kann leicht installiert werden:

# Debian/Ubuntu
sudo apt-get install dialog

# Red Hat/CentOS/Fedora
sudo dnf install dialog

# Arch Linux
sudo pacman -S dialog

# macOS mit Homebrew
brew install dialog

Die Grundsyntax von dialog ist:

dialog --typebox [Optionen] <Höhe> <Breite> [<Optionale Parameter>]

11.7.2 Einfache Dialog-Fenster

11.7.2.1 Nachrichtenfenster (Message Box)

#!/bin/bash

# Einfaches Nachrichtenfenster
dialog --title "Information" --msgbox "Die Operation wurde erfolgreich abgeschlossen." 8 50

# Nachrichtenfenster mit Timeout (10 Sekunden)
dialog --title "Hinweis" --timeout 10 --msgbox "Dieses Fenster schließt sich in 10 Sekunden." 8 50

11.7.2.2 Ja/Nein-Abfrage (Yes/No Box)

#!/bin/bash

# Ja/Nein-Abfrage
dialog --title "Bestätigung" --yesno "Möchten Sie fortfahren?" 7 40
response=$?

case $response in
    0) echo "Benutzer hat Ja gewählt." ;;
    1) echo "Benutzer hat Nein gewählt." ;;
    255) echo "Dialog wurde abgebrochen." ;;
esac

11.7.2.3 Eingabefelder (Input Box)

#!/bin/bash

# Texteingabe
dialog --title "Benutzereingabe" --inputbox "Bitte geben Sie Ihren Namen ein:" 8 40 2> /tmp/input.txt
name=$(cat /tmp/input.txt)
echo "Eingegebener Name: $name"

# Passwort-Eingabe (verdeckt)
dialog --title "Passwort" --passwordbox "Bitte geben Sie Ihr Passwort ein:" 8 40 2> /tmp/password.txt
password=$(cat /tmp/password.txt)
echo "Passwort wurde eingegeben."

11.7.2.4 Menüs und Auswahllisten

#!/bin/bash

# Einfaches Menü
dialog --title "Hauptmenü" --menu "Bitte wählen Sie eine Option:" 15 50 5 \
    1 "Systeminfo anzeigen" \
    2 "Festplattenbelegung prüfen" \
    3 "Benutzer verwalten" \
    4 "Netzwerkeinstellungen" \
    5 "Beenden" 2> /tmp/menu_choice.txt

choice=$(cat /tmp/menu_choice.txt)

case $choice in
    1) echo "Systeminfo ausgewählt" ;;
    2) echo "Festplattenbelegung ausgewählt" ;;
    3) echo "Benutzerverwaltung ausgewählt" ;;
    4) echo "Netzwerkeinstellungen ausgewählt" ;;
    5) echo "Beenden ausgewählt" ;;
    *) echo "Keine Auswahl getroffen" ;;
esac

11.7.2.5 Checkbox-Listen

#!/bin/bash

# Checkbox-Liste für Mehrfachauswahl
dialog --title "Komponenten auswählen" --checklist "Wählen Sie zu installierende Komponenten:" 15 60 5 \
    "webserver" "Webserver (Apache)" ON \
    "database" "Datenbank (MySQL)" OFF \
    "php" "PHP" ON \
    "mail" "Mail-Server" OFF \
    "firewall" "Firewall-Konfiguration" ON 2> /tmp/checklist_result.txt

selected=$(cat /tmp/checklist_result.txt)
echo "Ausgewählte Komponenten: $selected"

11.7.2.6 Radiobox-Listen

#!/bin/bash

# Radiobox-Liste für Einzelauswahl
dialog --title "Betriebssystem wählen" --radiolist "Wählen Sie ein Betriebssystem:" 15 50 5 \
    "linux" "Linux" ON \
    "windows" "Windows" OFF \
    "macos" "macOS" OFF \
    "freebsd" "FreeBSD" OFF \
    "other" "Andere" OFF 2> /tmp/radio_result.txt

os=$(cat /tmp/radio_result.txt)
echo "Ausgewähltes Betriebssystem: $os"

11.7.2.7 Fortschrittsanzeigen

#!/bin/bash

# Einfacher Fortschrittsbalken
(
    for i in $(seq 1 100); do
        echo $i
        sleep 0.05
    done
) | dialog --title "Installation" --gauge "Installiere Pakete..." 8 50 0

# Fortschrittsbalken mit Textaktualisierung
(
    echo "10"; echo "XXX"; echo "Herunterladen..."; echo "XXX"; sleep 1
    echo "30"; echo "XXX"; echo "Entpacken..."; echo "XXX"; sleep 1
    echo "50"; echo "XXX"; echo "Installieren..."; echo "XXX"; sleep 1
    echo "70"; echo "XXX"; echo "Konfigurieren..."; echo "XXX"; sleep 1
    echo "90"; echo "XXX"; echo "Aufräumen..."; echo "XXX"; sleep 1
    echo "100"; echo "XXX"; echo "Abgeschlossen!"; echo "XXX"; sleep 0.5
) | dialog --title "Installation" --gauge "Starte Installation..." 8 50 0

11.7.3 Komplexe Dialog-Beispiele

11.7.3.1 Mehrstufiger Installationsassistent

#!/bin/bash

# Temporäre Dateien für Dialog-Ausgaben
temp_dir=$(mktemp -d)
CHOICE_FILE="$temp_dir/choice"
VALUES_FILE="$temp_dir/values"

# Aufräumen bei Beendigung
trap 'rm -rf "$temp_dir"' EXIT

# Willkommensbildschirm
dialog --title "Installationsassistent" --msgbox "Willkommen zum Installationsassistent.\n\nDieser Assistent führt Sie durch die Installation und Konfiguration der Anwendung." 10 60

# Schritt 1: Zielverzeichnis
dialog --title "Schritt 1: Installationsort" --inputbox "Bitte geben Sie das Installationsverzeichnis ein:" 8 60 "/opt/myapp" 2> "$VALUES_FILE"
INSTALL_DIR=$(cat "$VALUES_FILE")

# Schritt 2: Komponentenauswahl
dialog --title "Schritt 2: Komponenten" --checklist "Wählen Sie zu installierende Komponenten:" 15 60 5 \
    "core" "Kernanwendung (erforderlich)" ON \
    "docs" "Dokumentation" ON \
    "examples" "Beispiele" ON \
    "devel" "Entwicklertools" OFF \
    "extras" "Zusätzliche Funktionen" OFF 2> "$VALUES_FILE"
COMPONENTS=$(cat "$VALUES_FILE")

# Schritt 3: Konfigurationsoptionen
dialog --title "Schritt 3: Konfiguration" --menu "Wählen Sie eine Konfigurationsvorlage:" 15 60 3 \
    "minimal" "Minimale Installation" \
    "standard" "Standardinstallation" \
    "full" "Vollständige Installation" 2> "$VALUES_FILE"
CONFIG_TEMPLATE=$(cat "$VALUES_FILE")

# Schritt 4: Netzwerkeinstellungen
dialog --title "Schritt 4: Netzwerk" --form "Netzwerkeinstellungen:" 15 60 4 \
    "Port:"        1 1 "8080"     1 20 10 0 \
    "Hostname:"    2 1 "localhost" 2 20 20 0 \
    "Max. Verbindungen:" 3 1 "100"      3 20 5 0 \
    "Timeout (s):" 4 1 "30"       4 20 5 0 2> "$VALUES_FILE"
NETWORK_SETTINGS=$(cat "$VALUES_FILE")

# Werte in separate Variablen aufteilen
PORT=$(echo "$NETWORK_SETTINGS" | sed -n '1p')
HOSTNAME=$(echo "$NETWORK_SETTINGS" | sed -n '2p')
MAX_CONN=$(echo "$NETWORK_SETTINGS" | sed -n '3p')
TIMEOUT=$(echo "$NETWORK_SETTINGS" | sed -n '4p')

# Zusammenfassung anzeigen
dialog --title "Zusammenfassung" --msgbox "Installationsübersicht:
- Installationsverzeichnis: $INSTALL_DIR
- Komponenten: $COMPONENTS
- Konfiguration: $CONFIG_TEMPLATE
- Netzwerkeinstellungen:
  - Port: $PORT
  - Hostname: $HOSTNAME
  - Max. Verbindungen: $MAX_CONN
  - Timeout: $TIMEOUT Sekunden" 15 70

# Bestätigung vor der Installation
dialog --title "Installation starten" --yesno "Sind Sie bereit, die Installation zu starten?" 7 50
response=$?

if [ $response -eq 0 ]; then
    # Simulation einer Installation mit Fortschrittsbalken
    (
        echo "10"; echo "XXX"; echo "Vorbereitung der Installation..."; echo "XXX"; sleep 1
        echo "20"; echo "XXX"; echo "Erstelle Verzeichnisse..."; echo "XXX"; sleep 1
        echo "30"; echo "XXX"; echo "Extrahiere Dateien..."; echo "XXX"; sleep 2
        echo "50"; echo "XXX"; echo "Installiere Komponenten: $COMPONENTS"; echo "XXX"; sleep 2
        echo "70"; echo "XXX"; echo "Konfiguriere Anwendung..."; echo "XXX"; sleep 1
        echo "90"; echo "XXX"; echo "Führe abschließende Einrichtung durch..."; echo "XXX"; sleep 1
        echo "100"; echo "XXX"; echo "Installation abgeschlossen!"; echo "XXX"; sleep 0.5
    ) | dialog --title "Installation läuft" --gauge "Starte Installation..." 8 60 0
    
    # Abschlussbildschirm
    dialog --title "Installation abgeschlossen" --msgbox "Die Installation wurde erfolgreich abgeschlossen!\n\nSie können die Anwendung jetzt starten mit:\n$INSTALL_DIR/bin/myapp" 10 60
else
    dialog --title "Installation abgebrochen" --msgbox "Die Installation wurde abgebrochen." 7 40
fi

11.7.3.2 Systemverwaltungs-Tool

#!/bin/bash

# Temporäre Dateien
temp_dir=$(mktemp -d)
CHOICE_FILE="$temp_dir/choice"

# Aufräumen bei Beendigung
trap 'rm -rf "$temp_dir"' EXIT

# Hauptmenü-Funktion
show_main_menu() {
    dialog --clear --title "Systemverwaltung" --menu "Wählen Sie eine Option:" 15 60 8 \
        1 "Systeminfo anzeigen" \
        2 "Festplattenbelegung prüfen" \
        3 "Prozesse verwalten" \
        4 "Netzwerkstatus" \
        5 "Benutzer verwalten" \
        6 "Dienste verwalten" \
        7 "Systemprotokollierung" \
        0 "Beenden" 2> "$CHOICE_FILE"
    
    # Abbruch durch ESC oder Cancel
    if [ $? -ne 0 ]; then
        return 1
    fi
    
    choice=$(cat "$CHOICE_FILE")
    return 0
}

# Systeminfo anzeigen
show_system_info() {
    # Sammle Systeminformationen
    system_info=$(
        echo "Hostname: $(hostname)"
        echo "Kernel: $(uname -r)"
        echo "Betriebssystem: $(grep PRETTY_NAME /etc/os-release | cut -d= -f2 | tr -d '"')"
        echo "Uptime: $(uptime -p)"
        echo "CPU: $(grep 'model name' /proc/cpuinfo | head -1 | cut -d: -f2 | sed 's/^[ \t]*//')"
        echo "Speicher: $(free -h | grep Mem | awk '{print $3 " von " $2 " verwendet"}')"
        echo "Swap: $(free -h | grep Swap | awk '{print $3 " von " $2 " verwendet"}')"
    )
    
    dialog --title "Systeminformationen" --msgbox "$system_info" 15 70
}

# Festplattenbelegung anzeigen
show_disk_usage() {
    # Sammle Festplatteninformationen
    disk_info=$(df -h | grep -v "tmpfs" | grep -v "udev")
    
    dialog --title "Festplattenbelegung" --msgbox "$disk_info" 20 70
}

# Prozessverwaltung
manage_processes() {
    # Liste der Top-Prozesse nach CPU-Nutzung
    processes=$(ps aux --sort=-%cpu | head -11)
    
    dialog --title "Prozessverwaltung" --menu "Top-Prozesse nach CPU-Nutzung:" 20 70 10 \
        "refresh" "Liste aktualisieren" \
        "kill" "Prozess beenden (PID eingeben)" 2> "$CHOICE_FILE"
    
    if [ $? -ne 0 ]; then
        return
    fi
    
    action=$(cat "$CHOICE_FILE")
    
    case $action in
        refresh)
            manage_processes
            ;;
        kill)
            dialog --title "Prozess beenden" --inputbox "Geben Sie die PID des zu beendenden Prozesses ein:" 8 50 2> "$CHOICE_FILE"
            if [ $? -eq 0 ]; then
                pid=$(cat "$CHOICE_FILE")
                if [ -n "$pid" ] && [ "$pid" -eq "$pid" ] 2>/dev/null; then
                    kill -15 $pid 2>/dev/null
                    dialog --title "Prozessverwaltung" --msgbox "Signal an Prozess $pid gesendet." 6 50
                else
                    dialog --title "Fehler" --msgbox "Ungültige PID: $pid" 6 50
                fi
            fi
            manage_processes
            ;;
    esac
}

# Netzwerkstatus anzeigen
show_network_status() {
    # Sammle Netzwerkinformationen
    network_info=$(
        echo "=== Netzwerkschnittstellen ==="
        ip -br addr show
        echo
        echo "=== Offene Ports (tcp) ==="
        netstat -tuln | grep tcp
        echo
        echo "=== Offene Ports (udp) ==="
        netstat -tuln | grep udp
        echo
        echo "=== Aktive Verbindungen ==="
        netstat -tn | head -20
    )
    
    dialog --title "Netzwerkstatus" --msgbox "$network_info" 20 70
}

# Benutzerverwaltung
manage_users() {
    # Liste der Benutzer
    users=$(awk -F: '$3 >= 1000 && $3 < 65534 {print $1}' /etc/passwd | sort)
    
    # Erstelle Menüoptionen
    options=""
    for user in $users; do
        options="$options $user \"Benutzer $user verwalten\""
    done
    options="$options create \"Neuen Benutzer erstellen\""
    
    # Benutzermenü anzeigen
    eval "dialog --title \"Benutzerverwaltung\" --menu \"Wählen Sie einen Benutzer:\" 15 60 8 $options 2> \"$CHOICE_FILE\""
    
    if [ $? -ne 0 ]; then
        return
    fi
    
    selection=$(cat "$CHOICE_FILE")
    
    if [ "$selection" = "create" ]; then
        dialog --title "Neuer Benutzer" --inputbox "Benutzername:" 8 40 2> "$CHOICE_FILE"
        if [ $? -eq 0 ]; then
            new_user=$(cat "$CHOICE_FILE")
            dialog --title "Bestätigung" --yesno "Benutzer $new_user erstellen?" 6 40
            if [ $? -eq 0 ]; then
                dialog --title "In Arbeit" --msgbox "Benutzer $new_user würde jetzt erstellt werden.\n(Diese Funktion ist in diesem Demo-Skript nicht implementiert)" 8 50
            fi
        fi
    else
        dialog --title "Benutzerverwaltung" --menu "Optionen für Benutzer $selection:" 12 60 4 \
            1 "Passwort ändern" \
            2 "Gruppen anzeigen" \
            3 "Benutzer löschen" \
            4 "Zurück" 2> "$CHOICE_FILE"
        
        if [ $? -eq 0 ]; then
            user_action=$(cat "$CHOICE_FILE")
            case $user_action in
                1)
                    dialog --title "In Arbeit" --msgbox "Passwort für $selection würde jetzt geändert werden.\n(Diese Funktion ist in diesem Demo-Skript nicht implementiert)" 8 50
                    ;;
                2)
                    user_groups=$(groups $selection 2>/dev/null || echo "Fehler: Benutzergruppen konnten nicht abgerufen werden")
                    dialog --title "Gruppen für $selection" --msgbox "$user_groups" 8 60
                    ;;
                3)
                    dialog --title "Bestätigung" --yesno "Benutzer $selection wirklich löschen?" 6 50
                    if [ $? -eq 0 ]; then
                        dialog --title "In Arbeit" --msgbox "Benutzer $selection würde jetzt gelöscht werden.\n(Diese Funktion ist in diesem Demo-Skript nicht implementiert)" 8 50
                    fi
                    ;;
            esac
        fi
    fi
    
    manage_users
}

# Dienstverwaltung
manage_services() {
    # Liste der Systemd-Dienste
    services=$(systemctl list-units --type=service --all | grep ".service" | awk '{print $1}' | head -15)
    
    # Erstelle Menüoptionen
    options=""
    for service in $services; do
        status=$(systemctl is-active $service 2>/dev/null)
        if [ "$status" = "active" ]; then
            status="aktiv"
        else
            status="inaktiv"
        fi
        options="$options $service \"$service ($status)\""
    done
    
    # Dienstmenü anzeigen
    eval "dialog --title \"Dienstverwaltung\" --menu \"Wählen Sie einen Dienst:\" 20 70 15 $options 2> \"$CHOICE_FILE\""
    
    if [ $? -ne 0 ]; then
        return
    fi
    
    service=$(cat "$CHOICE_FILE")
    status=$(systemctl is-active $service 2>/dev/null)
    
    if [ "$status" = "active" ]; then
        action="stop"
        action_text="stoppen"
    else
        action="start"
        action_text="starten"
    fi
    
    dialog --title "Dienstverwaltung" --menu "Optionen für $service:" 12 60 4 \
        1 "$service $action_text" \
        2 "$service neustarten" \
        3 "Status von $service anzeigen" \
        4 "Zurück" 2> "$CHOICE_FILE"
    
    if [ $? -eq 0 ]; then
        service_action=$(cat "$CHOICE_FILE")
        case $service_action in
            1)
                dialog --title "In Arbeit" --msgbox "$service würde jetzt $action_text.\n(Diese Funktion ist in diesem Demo-Skript nicht implementiert)" 8 50
                ;;
            2)
                dialog --title "In Arbeit" --msgbox "$service würde jetzt neugestartet werden.\n(Diese Funktion ist in diesem Demo-Skript nicht implementiert)" 8 50
                ;;
            3)
                service_status=$(systemctl status $service 2>&1 || echo "Fehler: Status konnte nicht abgerufen werden")
                dialog --title "Status von $service" --msgbox "$service_status" 20 70
                ;;
        esac
    fi
    
    manage_services
}

# Systemprotokollierung
show_logs() {
    log_files="/var/log/syslog /var/log/auth.log /var/log/kern.log /var/log/dmesg"
    
    # Erstelle Menüoptionen
    options=""
    for log in $log_files; do
        if [ -f "$log" ]; then
            log_name=$(basename $log)
            options="$options $log \"$log_name anzeigen\""
        fi
    done
    
    # Logmenü anzeigen
    eval "dialog --title \"Systemprotokolle\" --menu \"Wählen Sie eine Protokolldatei:\" 15 60 6 $options 2> \"$CHOICE_FILE\""
    
    if [ $? -ne 0 ]; then
        return
    fi
    
    selected_log=$(cat "$CHOICE_FILE")
    
    if [ -f "$selected_log" ]; then
        log_content=$(tail -n 500 $selected_log 2>&1 || echo "Fehler: Protokoll konnte nicht gelesen werden")
        dialog --title "Inhalt von $selected_log" --textbox /dev/stdin 20 80 <<< "$log_content"
    else
        dialog --title "Fehler" --msgbox "Protokolldatei nicht gefunden: $selected_log" 6 50
    fi
    
    show_logs
}

# Hauptprogrammschleife
while true; do
    show_main_menu
    if [ $? -ne 0 ]; then
        break
    fi
    
    choice=$(cat "$CHOICE_FILE")
    
    case $choice in
        1) show_system_info ;;
        2) show_disk_usage ;;
        3) manage_processes ;;
        4) show_network_status ;;
        5) manage_users ;;
        6) manage_services ;;
        7) show_logs ;;
        0) break ;;
    esac
done

# Aufräumen und beenden
clear
echo "Programm beendet."

11.7.4 Anpassung des Erscheinungsbilds von Dialog

dialog bietet verschiedene Möglichkeiten zur Anpassung des Erscheinungsbilds:

#!/bin/bash

# Farben und Stil anpassen
export DIALOGRC="/path/to/custom/dialogrc"

# Oder temporäre Anpassungen vornehmen
dialog --colors \
       --title "\Z1\Zr\ZbRotes Titel mit Rahmen\Zn" \
       --backtitle "\Z2Grüner Hintergrundtitel\Zn" \
       --clear \
       --ok-label "Weiter" \
       --cancel-label "Abbrechen" \
       --msgbox "\Z3Blaue Nachricht\Zn\n\Z4Türkiser Text\Zn\n\Z5Magenta\Zn\n\Z6Cyan\Zn\n\Z7Weiß\Zn" 15 50

Eine benutzerdefinierte dialogrc-Datei könnte so aussehen:

# Dialog-Konfigurationsdatei
# Schattenstil
use_shadow = ON
# Grenzen verwenden
use_colors = ON
# Farbschema
screen_color = (BLUE,BLACK,ON)
shadow_color = (BLACK,BLACK,ON)
dialog_color = (WHITE,BLACK,OFF)
title_color = (YELLOW,BLACK,ON)
border_color = (WHITE,BLACK,ON)
button_active_color = (WHITE,BLUE,ON)
button_inactive_color = (WHITE,BLACK,OFF)
button_key_active_color = (WHITE,BLUE,ON)
button_key_inactive_color = (RED,BLACK,OFF)
button_label_active_color = (YELLOW,BLUE,ON)
button_label_inactive_color = (BLACK,BLACK,ON)
inputbox_color = (BLACK,WHITE,OFF)
inputbox_border_color = (BLACK,WHITE,OFF)
searchbox_color = (BLACK,WHITE,OFF)
searchbox_title_color = (BLUE,WHITE,ON)
searchbox_border_color = (WHITE,WHITE,ON)
position_indicator_color = (YELLOW,BLACK,ON)
menubox_color = (BLACK,BLACK,OFF)
menubox_border_color = (WHITE,BLACK,ON)
item_color = (WHITE,BLACK,OFF)
item_selected_color = (WHITE,BLUE,ON)
tag_color = (YELLOW,BLACK,ON)
tag_selected_color = (YELLOW,BLUE,ON)
tag_key_color = (RED,BLACK,OFF)
tag_key_selected_color = (RED,BLUE,ON)
check_color = (BLACK,WHITE,OFF)
check_selected_color = (WHITE,BLUE,ON)
uarrow_color = (GREEN,BLACK,ON)
darrow_color = (GREEN,BLACK,ON)

11.7.5 Alternativen zu Dialog

Neben dialog gibt es mehrere andere TUI-Werkzeuge, die für verschiedene Anwendungsfälle geeignet sein können:

11.7.5.1 Whiptail

whiptail ist eine leichtere Alternative zu dialog und ist auf vielen Systemen standardmäßig installiert:

#!/bin/bash

# Einfaches Menü mit whiptail
whiptail --title "Hauptmenü" --menu "Wählen Sie eine Option:" 15 60 5 \
    "1" "Option 1" \
    "2" "Option 2" \
    "3" "Option 3" 3>&1 1>&2 2>&3

11.7.5.2 YAD (Yet Another Dialog)

YAD bietet fortschrittlichere GTK-basierte Dialoge für Skripte:

#!/bin/bash

# Einfaches Formular mit YAD
yad --title "Benutzerdaten" --form \
    --field="Name" "" \
    --field="E-Mail" "" \
    --field="Geburtsdatum:DT" "" \
    --field="Geschlecht:CB" "Männlich!Weiblich!Divers" \
    --button="Abbrechen:1" --button="OK:0"

11.7.5.3 Zenity

Zenity ist ein GNOME-Tool für einfache GTK-Dialoge:

#!/bin/bash

# Einfacher Dateiauswahldialog mit Zenity
file=$(zenity --file-selection --title="Wählen Sie eine Datei")
if [ $? -eq 0 ]; then
    zenity --info --text="Sie haben $file ausgewählt."
else
    zenity --error --text="Keine Datei ausgewählt."
fi

11.7.5.4 Ncurses und Andere Bibliotheken

Für komplexere TUI-Anwendungen können Sie direkt mit Programmierbibliotheken wie Ncurses arbeiten:

#!/usr/bin/env python3
import curses

def main(stdscr):
    # Initialisiere Farben
    curses.start_color()
    curses.init_pair(1, curses.COLOR_BLUE, curses.COLOR_BLACK)
    curses.init_pair(2, curses.COLOR_GREEN, curses.COLOR_BLACK)
    
    # Setze den Bildschirm auf
    stdscr.clear()
    stdscr.refresh()
    
    # Erstelle ein Fenster
    height, width = stdscr.getmaxyx()
    win = curses.newwin(height-4, width-4, 2, 2)
    win.box()
    win.addstr(2, 2, "Willkommen bei Curses!", curses.color_pair(1))
    win.addstr(4, 2, "Drücken Sie eine Taste zum Beenden.", curses.color_pair(2))
    win.refresh()
    
    # Warte auf Tastendruck
    win.getch()

# Starte das Hauptprogramm
curses.wrapper(main)

11.7.6 Praktische Tipps und Best Practices

  1. Temporäre Dateien verwenden: Nutzen Sie mktemp zum Erstellen temporärer Dateien und stellen Sie sicher, dass diese beim Beenden des Skripts aufgeräumt werden.

  2. Fehlerbehandlung implementieren: Überprüfen Sie stets die Rückgabewerte von Dialog-Aufrufen, um Abbrüche durch den Benutzer zu erkennen.

  3. Benutzerfreundlichkeit: Bieten Sie klare Anweisungen, konsistente Navigationsmöglichkeiten und brechen Sie komplexe Aufgaben in überschaubare Schritte auf.

  4. Bildschirmgröße berücksichtigen: Passen Sie die Größe Ihrer Dialoge an, um auch auf kleineren Terminals gut auszusehen.

  5. Tastaturkürzel anbieten: Implementieren Sie Tastaturkürzel für häufig verwendete Funktionen.

  6. Internationalisierung: Wenn Ihr Skript in verschiedenen Sprachen verwendet werden soll, nutzen Sie Übersetzungsdateien.