9 Textverarbeitung und Datenmanipulation

9.1 Reguläre Ausdrücke in verschiedenen Kontexten

Reguläre Ausdrücke (Regular Expressions oder kurz RegEx) sind ein mächtiges Werkzeug für die Textverarbeitung und Mustersuche. Sie bilden eine Minisprachform, mit der komplexe Textmuster präzise beschrieben werden können. In der Shell-Programmierung kommen reguläre Ausdrücke in verschiedenen Kontexten zum Einsatz, wobei es wichtige Unterschiede in der Syntax und den Funktionen gibt, je nachdem, mit welchem Tool sie verwendet werden.

9.1.1 Grundlagen regulärer Ausdrücke

Bevor wir uns mit den verschiedenen Kontexten befassen, sollten wir die grundlegenden Elemente regulärer Ausdrücke verstehen:

9.1.1.1 Literale Zeichen und Metazeichen

Reguläre Ausdrücke bestehen aus:

9.1.1.2 Grundlegende Metazeichen und ihre Bedeutung

Metazeichen Bedeutung
. Entspricht einem beliebigen einzelnen Zeichen (außer Zeilenumbruch)
^ Entspricht dem Anfang einer Zeile
$ Entspricht dem Ende einer Zeile
* Entspricht 0 oder mehr Wiederholungen des vorherigen Elements
+ Entspricht 1 oder mehr Wiederholungen des vorherigen Elements
? Entspricht 0 oder 1 Vorkommen des vorherigen Elements
\ Escape-Zeichen; hebt die spezielle Bedeutung des folgenden Zeichens auf
| Alternation (“oder”)
() Gruppierung von Ausdrücken und Erfassung übereinstimmender Teilmuster
[] Zeichenklasse; entspricht einem der darin enthaltenen Zeichen
{} Quantifizierer; gibt die Anzahl der Wiederholungen an

9.1.2 Drei Typen von regulären Ausdrücken

In Unix-/Linux-Systemen gibt es drei hauptsächliche “Dialekte” oder Typen von regulären Ausdrücken:

  1. Basic Regular Expressions (BRE): Die ursprüngliche, einfachere Form
  2. Extended Regular Expressions (ERE): Eine erweiterte Version mit zusätzlichen Funktionen
  3. Perl-Compatible Regular Expressions (PCRE): Eine noch mächtigere Variante, die von der Programmiersprache Perl inspiriert ist

Die Hauptunterschiede liegen in der Syntax und darin, welche Metazeichen direkt verwendet werden können und welche mit einem Backslash (\) “escaped” werden müssen.

9.1.3 Reguläre Ausdrücke in verschiedenen Tools

9.1.3.1 grep und seine Varianten

Das Tool grep (Global Regular Expression Print) wird verwendet, um nach Textmustern in Dateien zu suchen:

# Basic Regular Expressions (Standard-grep)
grep "pattern" datei.txt

# Extended Regular Expressions
grep -E "pattern" datei.txt
# oder
egrep "pattern" datei.txt

# Perl-Compatible Regular Expressions
grep -P "pattern" datei.txt

Hier einige Beispiele, die die Unterschiede zwischen den verschiedenen Modi verdeutlichen:

# BRE: Um die Metazeichen +, ?, |, () zu verwenden, müssen sie mit \ escaped werden
grep "a\+" datei.txt         # Sucht nach "a" gefolgt von einem oder mehreren "a"

# ERE: Metazeichen wie +, ?, |, () können direkt verwendet werden
grep -E "a+" datei.txt       # Äquivalent zum obigen Befehl
egrep "a+" datei.txt         # Identisch zu grep -E

# PCRE: Fortgeschrittene Funktionen wie Lookaheads/Lookbehinds verfügbar
grep -P "a(?=b)" datei.txt   # Sucht nach "a", aber nur wenn es von "b" gefolgt wird

9.1.3.2 sed (Stream Editor)

sed ist ein mächtiges Werkzeug zur Transformation von Texteingaben und verwendet standardmäßig Basic Regular Expressions. Mit der Option -E kann auf Extended Regular Expressions umgeschaltet werden:

# Basic Regular Expressions (Standard)
sed 's/pattern/replacement/' datei.txt

# Extended Regular Expressions
sed -E 's/pattern/replacement/' datei.txt

Beispiele:

# BRE: Suchen und Ersetzen mit Gruppierung
sed 's/\(Hallo\) \(Welt\)/\2 \1/' datei.txt  # Tauscht "Hallo Welt" gegen "Welt Hallo"

# ERE: Gleiche Funktion, aber einfachere Syntax
sed -E 's/(Hallo) (Welt)/\2 \1/' datei.txt   # Äquivalent zum obigen Befehl

9.1.3.3 awk

awk ist eine komplette Programmiersprache für Textverarbeitung und verwendet standardmäßig Extended Regular Expressions:

awk '/pattern/ { aktion }' datei.txt

Beispiel:

# Sucht nach Zeilen, die mit "user" beginnen und endet mit einer Zahl
# und gibt die zweite Spalte aus
awk '/^user.*[0-9]$/ { print $2 }' benutzer.txt

9.1.3.4 Bash selbst (=~ Operator)

Innerhalb von Bash-Skripten können reguläre Ausdrücke direkt mit dem =~ Operator innerhalb einer [[ ]] Bedingung verwendet werden. Hierbei handelt es sich um Extended Regular Expressions:

if [[ "$string" =~ pattern ]]; then
    echo "Muster gefunden"
fi

Beispiel:

#!/bin/bash

email="benutzer@example.com"

# Überprüfen einer E-Mail-Adresse mit einem regulären Ausdruck
if [[ "$email" =~ ^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}$ ]]; then
    echo "Gültige E-Mail-Adresse"
else
    echo "Ungültige E-Mail-Adresse"
fi

Ein wichtiger Hinweis: Bei Verwendung des =~ Operators sollte der reguläre Ausdruck normalerweise nicht in Anführungszeichen gesetzt werden, es sei denn, Sie möchten, dass Variablenersetzung und andere Shell-Expansionen vermieden werden.

pattern="^[0-9]+$"

# Korrekt:
if [[ "123" =~ $pattern ]]; then
    echo "Übereinstimmung gefunden"
fi

# Auch korrekt, verhindert Shell-Expansionen im Pattern:
if [[ "123" =~ "^[0-9]+$" ]]; then
    echo "Übereinstimmung gefunden"
fi

9.1.4 Praktische Anwendungsfälle

9.1.4.1 Validierung von Benutzereingaben

#!/bin/bash

# Funktion zur Validierung einer IP-Adresse
validate_ip() {
    local ip=$1
    
    # IP-Adresse validieren mit einem regulären Ausdruck
    if [[ $ip =~ ^([0-9]{1,3}\.){3}[0-9]{1,3}$ ]]; then
        # Überprüfen, ob jede Zahl <= 255 ist
        IFS='.' read -r -a octets <<< "$ip"
        for octet in "${octets[@]}"; do
            if (( octet > 255 )); then
                return 1  # Ungültig
            fi
        done
        return 0  # Gültig
    else
        return 1  # Ungültig
    fi
}

# Benutzereingabe abfragen
read -p "Bitte geben Sie eine IP-Adresse ein: " ip_address

# Validierung aufrufen
if validate_ip "$ip_address"; then
    echo "Die IP-Adresse $ip_address ist gültig."
else
    echo "Die IP-Adresse $ip_address ist ungültig."
fi

9.1.4.2 Extrahieren von Informationen aus strukturierten Texten

#!/bin/bash

# Eine Log-Datei mit folgendem Format:
# [DATUM] [LEVEL] Nachricht
log_file="application.log"

# Extraktion aller ERROR-Meldungen mit grep und ERE
grep -E "^\[[0-9-]+\] \[ERROR\]" "$log_file"

# Extraktion der Zeitstempel und Nachrichteninhalte mit sed und ERE
sed -E 's/^\[([0-9-]+)\] \[([A-Z]+)\] (.*)$/Datum: \1, Level: \2, Nachricht: \3/' "$log_file"

# Extraktion und Verarbeitung mit awk
awk '
    /^\[[0-9-]+\] \[(ERROR|WARN)\]/ {
        gsub(/^\[|\]/, "", $1);  # Entfernt die eckigen Klammern aus dem Datum
        gsub(/^\[|\]/, "", $2);  # Entfernt die eckigen Klammern aus dem Level
        
        # Speichert Fehler und Warnungen nach Datum
        counts[$1][$2]++;
    }
    
    END {
        print "Zusammenfassung der Fehler und Warnungen nach Datum:";
        for (date in counts) {
            printf "%s: ", date;
            for (level in counts[date]) {
                printf "%s=%d ", level, counts[date][level];
            }
            printf "\n";
        }
    }
' "$log_file"

9.1.4.3 Suchen und Ersetzen mit komplexen Mustern

#!/bin/bash

# Eine HTML-Datei bearbeiten, um alle Links zu modifizieren
html_file="seite.html"

# Alle externen Links auf HTTPS umstellen
sed -E 's/(href="http:\/\/[^"]+)"/\1s"/' "$html_file" > "seite_https.html"

# Alle leeren HTML-Tags finden (z.B. <tag></tag> oder <tag />)
grep -E "<([a-zA-Z]+)[^>]*>([ \t]*)<\/\1>|<([a-zA-Z]+)[^>]*\/>" "$html_file"

9.1.5 Unterschiede und Fallstricke

Die Verwendung von regulären Ausdrücken in verschiedenen Tools und Kontexten kann aufgrund der Unterschiede in der Syntax und Funktionalität zu Verwirrung führen. Hier sind einige wichtige Unterschiede und Fallstricke:

9.1.5.1 Unterschiede zwischen BRE und ERE

Funktion BRE (grep, sed) ERE (grep -E, awk)
Alternation (a\|b) a\|b a\|b
Gruppierung \(ab\) (ab)
Ein oder mehrmals a\+ a+
Null oder einmal a\? a?
Exakte Anzahl a\{3\} a{3}
Bereich a\{2,4\} a{2,4}

9.1.5.2 Bash-Spezifische Besonderheiten

  1. Wortsplitting und Pfaderweiterung:

    # Vermeidung von Wortsplitting bei der Verwendung von grep
    grep "muster mit leerzeichen" datei.txt
    
    # Im Gegensatz zu (falsch):
    grep muster mit leerzeichen datei.txt  # Wird als 3 Argumente interpretiert
  2. Anführungszeichen und Variablenersetzung:

    pattern="Hallo Welt"
    
    # Mit doppelten Anführungszeichen wird die Variable ersetzt
    grep "$pattern" datei.txt
    
    # Mit einfachen Anführungszeichen wird der Literal-Text gesucht
    grep '$pattern' datei.txt  # Sucht den Text "$pattern", nicht "Hallo Welt"
  3. Capture Groups mit dem =~ Operator:

    string="Hallo Welt 2023"
    
    if [[ "$string" =~ ([A-Za-z]+)\ ([A-Za-z]+)\ ([0-9]+) ]]; then
        echo "Vollständige Übereinstimmung: ${BASH_REMATCH[0]}"
        echo "Erste Gruppe: ${BASH_REMATCH[1]}"
        echo "Zweite Gruppe: ${BASH_REMATCH[2]}"
        echo "Dritte Gruppe: ${BASH_REMATCH[3]}"
    fi

9.1.6 Weiterführende Konzepte

9.1.6.1 Lookaheads und Lookbehinds (nur in PCRE)

Diese fortgeschrittenen Konzepte ermöglichen es, nach einem Muster zu suchen, ohne dass es Teil des Ergebnisses ist:

# Positive Lookahead: Findet "a", aber nur wenn es von "b" gefolgt wird
grep -P "a(?=b)" datei.txt

# Negative Lookahead: Findet "a", aber nur wenn es NICHT von "b" gefolgt wird
grep -P "a(?!b)" datei.txt

# Positive Lookbehind: Findet "b", aber nur wenn es von "a" vorangestellt wird
grep -P "(?<=a)b" datei.txt

# Negative Lookbehind: Findet "b", aber nur wenn es NICHT von "a" vorangestellt wird
grep -P "(?<!a)b" datei.txt

9.1.6.2 Nicht-gierige (non-greedy) Wiederholungen

Standardmäßig sind Wiederholungsoperatoren wie * und + “gierig” und erfassen so viel wie möglich. Mit dem Zusatz ? nach dem Operator wird die Wiederholung “nicht-gierig”:

# Gierige Übereinstimmung (Standard)
grep -P "<.+>" datei.html  # Könnte mehrere Tags auf einmal erfassen

# Nicht-gierige Übereinstimmung
grep -P "<.+?>" datei.html  # Erfasst jeden Tag einzeln

9.2 Grundlegende Textverarbeitung mit grep, sed, awk

Die Unix-Philosophie betont die Entwicklung von Programmen, die eine Aufgabe gut erfüllen und mit anderen Programmen zusammenarbeiten können. Besonders deutlich wird dies bei den klassischen Textverarbeitungswerkzeugen grep, sed und awk. Jedes dieser Tools hat seinen eigenen Anwendungsbereich und seine Stärken, und in Kombination bilden sie ein mächtiges Arsenal für die Textverarbeitung in Shell-Skripten.

9.2.1 grep: Mustersuche in Texten

Das Werkzeug grep (Global Regular Expression Print) wurde entwickelt, um Zeilen aus einer Datei oder einer Standardeingabe zu finden, die auf ein bestimmtes Muster passen. Der Name leitet sich vom Befehl g/re/p des Editors ed ab, was bedeutet: “global suchen nach einem regulären Ausdruck und die gefundenen Zeilen ausgeben”.

9.2.1.1 Grundlegende Verwendung

grep [Optionen] Muster [Datei...]

Einfache Beispiele:

# Nach dem Wort "error" in einer Logdatei suchen
grep "error" logfile.txt

# Nach Zeilen suchen, die mit "user" beginnen, in mehreren Dateien
grep "^user" file1.txt file2.txt

# Alle .conf-Dateien im /etc Verzeichnis nach einem Muster durchsuchen
grep "network" /etc/*.conf

9.2.1.2 Nützliche Optionen für grep

Option Beschreibung
-i Ignoriert Groß-/Kleinschreibung
-v Invertiert die Suche (zeigt Zeilen, die NICHT zum Muster passen)
-r, -R Rekursive Suche durch Verzeichnisse
-l Zeigt nur die Dateinamen an, nicht die gefundenen Zeilen
-n Zeigt die Zeilennummern der gefundenen Zeilen an
-c Zählt die passenden Zeilen anstatt sie anzuzeigen
-A n Zeigt auch n Zeilen nach dem Treffer an (After)
-B n Zeigt auch n Zeilen vor dem Treffer an (Before)
-C n Zeigt auch n Zeilen vor und nach dem Treffer an (Context)
-E Verwendet Extended Regular Expressions
-F Behandelt das Muster als feste Zeichenkette, nicht als regulären Ausdruck
-P Verwendet Perl-Compatible Regular Expressions

Beispiele mit Optionen:

# Fall-insensitive Suche
grep -i "error" logfile.txt

# Zeilen, die NICHT "success" enthalten
grep -v "success" logfile.txt

# Dateien im Systemlog finden, die Fehlermeldungen enthalten
grep -l "error" /var/log/*.log

# Zeilen mit Kontext anzeigen
grep -C 2 "critical failure" application.log

# Rekursive Suche nach einer IP-Adresse in einem Verzeichnis
grep -r "192\.168\.1\.[0-9]\+" /var/log/

9.2.1.3 grep in Pipelines

Eine der Stärken von Unix-Werkzeugen ist ihre Fähigkeit, in Pipelines zusammenzuarbeiten:

# Prozesse finden, die von einem bestimmten Benutzer ausgeführt werden
ps aux | grep "username"

# Anzahl der HTTP 404-Fehler in einem Apache-Zugriffslog zählen
cat access.log | grep "HTTP/1.1\" 404" | wc -l

# Alle SSH-Verbindungen aus einer bestimmten IP-Adresse finden
cat /var/log/auth.log | grep "sshd" | grep "192.168.1.10"

9.2.2 sed: Stream Editor für Textmanipulation

sed (Stream Editor) ist ein leistungsfähiges Werkzeug zur Transformation von Texteingaben. Es liest Eingaben (aus einer Datei oder einer Pipeline), führt Operationen aus und gibt den modifizierten Text aus.

9.2.2.1 Grundlegende Verwendung

sed [Optionen] 'Befehl' [Datei...]

Wobei Befehl eine Anweisung wie s/Muster/Ersetzung/ sein kann.

Einfache Beispiele:

# Ersetzen des ersten Vorkommens von "old" durch "new" in jeder Zeile
sed 's/old/new/' file.txt

# Ersetzen aller Vorkommen von "old" durch "new" in jeder Zeile
sed 's/old/new/g' file.txt

# Nur Zeilen 10-20 einer Datei anzeigen
sed -n '10,20p' file.txt

# Kommentarzeilen aus einer Konfigurationsdatei entfernen
sed '/^#/d' config.conf

9.2.2.2 Häufig verwendete sed-Befehle

Befehl Beschreibung
s/Muster/Ersetzung/ Suchen und Ersetzen
d Löschen von Zeilen
p Drucken von Zeilen (normalerweise mit -n verwendet)
i\Text Einfügen von Text vor der aktuellen Zeile
a\Text Einfügen von Text nach der aktuellen Zeile
c\Text Ersetzen der aktuellen Zeile durch Text
y/abc/ABC/ Zeichen-für-Zeichen-Ersetzung (wie tr)

9.2.2.3 Adressen in sed

Bei sed können Befehle auf bestimmte Zeilen angewendet werden, indem man Adressen angibt:

# Nur Zeile 5 bearbeiten
sed '5s/old/new/' file.txt

# Zeilen 5 bis 10 bearbeiten
sed '5,10s/old/new/' file.txt

# Vom Anfang bis zu einer Zeile, die "END" enthält
sed '1,/END/s/old/new/' file.txt

# Jede zweite Zeile auswählen
sed '0~2s/old/new/' file.txt

# Nur Zeilen, die einem Muster entsprechen
sed '/pattern/s/old/new/' file.txt

9.2.2.4 Praktische Beispiele für sed

# Hinzufügen einer Zeile nach jeder Zeile, die einem Muster entspricht
sed '/pattern/a\Neue Zeile nach dem Muster' file.txt

# Entfernen von Leerzeilen
sed '/^$/d' file.txt

# Entfernen von führenden und nachfolgenden Leerzeichen
sed 's/^[ \t]*//;s/[ \t]*$//' file.txt

# Extrahieren von Texten zwischen bestimmten Markern
sed -n '/START/,/END/p' file.txt

# In-Place-Bearbeitung einer Datei (Vorsicht!)
sed -i 's/old/new/g' file.txt

# Backup erstellen vor In-Place-Bearbeitung
sed -i.bak 's/old/new/g' file.txt

9.2.2.5 Verwendung von sed in Skripten

Für komplexere Transformationen kann sed mit einer Skriptdatei aufgerufen werden:

# sed-Skriptdatei erstellen
cat > transform.sed << 'EOF'
# Kommentarzeilen entfernen
/^#/d

# Leerzeilen entfernen
/^$/d

# "DEBUG" durch "INFO" ersetzen
s/DEBUG/INFO/g

# Zeilen mit "ERROR" markieren
/ERROR/s/^/*** /
EOF

# Anwenden des Skripts auf eine Datei
sed -f transform.sed logfile.txt

9.2.3 awk: Eine Programmiersprache für Textverarbeitung

awk ist eine vollständige Programmiersprache, die speziell für die Textverarbeitung entwickelt wurde. Sie ist besonders nützlich für spaltenbasierte Daten und enthält zahlreiche eingebaute Funktionen für Mathematik, Zeichenkettenverarbeitung und mehr.

9.2.3.1 Grundlegende Verwendung

awk [Optionen] 'Programm' [Datei...]

Wobei Programm die Form /Muster/ { Aktion } haben kann.

Einfache Beispiele:

# Zweite Spalte einer Datei ausgeben
awk '{ print $2 }' file.txt

# Zeilen ausgeben, deren erste Spalte größer als 100 ist
awk '$1 > 100' file.txt

# Summe der Werte in der dritten Spalte berechnen
awk '{ sum += $3 } END { print sum }' file.txt

9.2.3.2 awk-Programmstruktur

Ein awk-Programm besteht aus einer Reihe von Muster-Aktions-Paaren:

BEGIN { Aktionen vor der Verarbeitung }
/Muster1/ { Aktionen für Zeilen, die Muster1 entsprechen }
/Muster2/ { Aktionen für Zeilen, die Muster2 entsprechen }
END { Aktionen nach der Verarbeitung }

9.2.3.3 Felder und Datensätze

awk teilt die Eingabe standardmäßig in Datensätze (normalerweise Zeilen) und Felder (normalerweise durch Leerzeichen oder Tabulatoren getrennte Spalten) auf:

9.2.3.4 Eingebaute Variablen in awk

Variable Beschreibung
FS Field Separator (Standard: Leerzeichen oder Tabulator)
RS Record Separator (Standard: Newline)
OFS Output Field Separator (Standard: Leerzeichen)
ORS Output Record Separator (Standard: Newline)
NF Number of Fields in current record
NR Number of the current Record
FNR File Number of the current Record
FILENAME Name der aktuellen Eingabedatei

9.2.3.5 Operatoren und Kontrollstrukturen

awk bietet die üblichen Operatoren und Kontrollstrukturen von Programmiersprachen:

# Arithmetische Operatoren: +, -, *, /, %, ^
# Vergleichsoperatoren: ==, !=, <, >, <=, >=
# Logische Operatoren: &&, ||, !

# Bedingte Anweisung
if (Bedingung) {
    Aktionen
} else if (andere_Bedingung) {
    Andere_Aktionen
} else {
    Standard_Aktionen
}

# Schleifen
for (i = 1; i <= 10; i++) {
    Aktionen
}

while (Bedingung) {
    Aktionen
}

do {
    Aktionen
} while (Bedingung)

# break und continue sind ebenfalls verfügbar

9.2.3.6 Praktische Beispiele für awk

# CSV-Datei mit benutzerdefiniertem Trennzeichen verarbeiten
awk -F';' '{ print $1 ", " $3 }' data.csv

# Summe und Durchschnitt einer numerischen Spalte berechnen
awk '{ sum += $1; count++ } END { print "Summe:", sum, "Durchschnitt:", sum/count }' zahlen.txt

# Spaltenüberschriften hinzufügen
awk 'BEGIN { print "Name\tAlter\tStadt" } { print $1 "\t" $2 "\t" $3 }' personen.txt

# Daten filtern und formatieren
awk '$3 > 30 { printf "%-20s %-10s %d\n", $1, $2, $3 }' data.txt

# Gruppieren und Aggregieren von Daten
awk '{ users[$1]++ } END { for (user in users) print user, users[user] }' access.log

# Zählen der Wörter in einer Datei
awk '{ for (i = 1; i <= NF; i++) words[$i]++ } 
     END { for (w in words) print w, words[w] }' text.txt

9.2.3.7 Komplexeres awk-Beispiel: Logfile-Analyse

#!/bin/bash

# Analyse eines Apache-Zugriffslogfiles mit awk
cat << 'EOF' > analyze_log.awk
BEGIN {
    FS = " "
    print "=== Apache Log Analysis ==="
    print "IP Address\t\tRequests\t404s\tBytes Sent"
    print "---------------------------------------------------"
}

{
    # Sammeln von Statistiken
    ip_count[$1]++
    
    # Zählen von 404-Fehlern
    if ($9 == 404) {
        ip_404[$1]++
    }
    
    # Summe der gesendeten Bytes
    if ($10 ~ /^[0-9]+$/) {
        ip_bytes[$1] += $10
    }
}

END {
    # Ausgabe der Ergebnisse
    for (ip in ip_count) {
        printf "%-15s\t%d\t\t%d\t%d\n", ip, ip_count[ip], ip_404[ip] ? ip_404[ip] : 0, ip_bytes[ip] ? ip_bytes[ip] : 0
    }
    
    print "\n=== Summary ==="
    print "Total Requests:", NR
    print "Unique IP Addresses:", length(ip_count)
    
    # Finden der IP mit den meisten Anfragen
    max_requests = 0
    max_ip = ""
    for (ip in ip_count) {
        if (ip_count[ip] > max_requests) {
            max_requests = ip_count[ip]
            max_ip = ip
        }
    }
    print "Most Active IP:", max_ip, "with", max_requests, "requests"
}
EOF

# Anwenden des awk-Skripts auf eine Logdatei
awk -f analyze_log.awk access.log

9.2.4 Kombination der Werkzeuge

Die wahre Kraft dieser Werkzeuge liegt in ihrer Kombination, insbesondere in Pipelines:

# Gemeinsame Zeilen in zwei Dateien finden
grep -Fx -f file1.txt file2.txt

# Alle Prozesse eines Benutzers beenden
ps -ef | grep username | grep -v grep | awk '{ print $2 }' | xargs kill

# Top 10 IP-Adressen in einem Apache-Zugriffslog
awk '{ print $1 }' access.log | sort | uniq -c | sort -nr | head -10

# CSV-Daten verarbeiten und nach einer bestimmten Spalte filtern
cat data.csv | awk -F';' '$3 > 1000 { print $1 "," $2 "," $3 }' | sed 's/,/;/g' > filtered.csv

# Tägliche Zusammenfassung aus einem Logfile extrahieren
grep "$(date +%Y-%m-%d)" system.log | \
    sed 's/^.*ERROR: \(.*\)$/\1/' | \
    awk '!seen[$0]++' | \
    sed 's/^/- /' > daily_error_summary.txt

9.2.5 Skriptbeispiel: Logfile-Analysator

Zum Abschluss ein komplettes Beispielskript, das alle drei Werkzeuge kombiniert, um eine einfache Logfile-Analyse durchzuführen:

#!/bin/bash

# Logfile-Analysator mit grep, sed und awk

if [ $# -ne 1 ]; then
    echo "Verwendung: $0 <logfile>"
    exit 1
fi

LOGFILE="$1"

if [ ! -f "$LOGFILE" ]; then
    echo "Fehler: Datei '$LOGFILE' nicht gefunden."
    exit 2
fi

echo "=== Logfile-Analyse für $LOGFILE ==="
echo ""

# 1. Anzahl der Einträge
TOTAL_ENTRIES=$(wc -l < "$LOGFILE")
echo "Gesamtanzahl der Einträge: $TOTAL_ENTRIES"

# 2. Verteilung nach Schweregrad mit grep und wc
echo -e "\nVerteilung nach Schweregrad:"
echo "---------------------------"
ERRORS=$(grep -c "ERROR" "$LOGFILE")
WARNINGS=$(grep -c "WARNING" "$LOGFILE")
INFO=$(grep -c "INFO" "$LOGFILE")
DEBUG=$(grep -c "DEBUG" "$LOGFILE")

echo "ERROR:   $ERRORS"
echo "WARNING: $WARNINGS"
echo "INFO:    $INFO"
echo "DEBUG:   $DEBUG"

# 3. Häufigste Fehlermeldungen mit grep, sed und sort
echo -e "\nTop 5 häufigste Fehlermeldungen:"
echo "------------------------------"
grep "ERROR" "$LOGFILE" | \
    sed -E 's/^.*ERROR[^:]*: (.*)/\1/' | \
    sort | uniq -c | sort -nr | head -5 | \
    awk '{ printf("%3d: %s\n", $1, substr($0, index($0, $2))) }'

# 4. Zeitliche Verteilung mit awk
echo -e "\nZeitliche Verteilung:"
echo "-------------------"
grep -E "^[0-9]{4}-[0-9]{2}-[0-9]{2}" "$LOGFILE" | \
    awk '{
        # Extrahieren des Datums (angenommen Format: YYYY-MM-DD HH:MM:SS)
        split($1, date_parts, "-")
        year = date_parts[1]
        month = date_parts[2]
        
        # Zählen nach Monat
        counts[month]++
    }
    END {
        for (month = 1; month <= 12; month++) {
            m = sprintf("%02d", month)
            count = counts[m] ? counts[m] : 0
            bar = ""
            # Erstellen eines einfachen Balkengraphen
            for (i = 0; i < count / 100; i++) bar = bar "#"
            printf("Monat %s: %5d %s\n", m, count, bar)
        }
    }'

# 5. Systemkomponenten mit den meisten Fehlern
echo -e "\nSystemkomponenten mit den meisten Problemen:"
echo "------------------------------------------"
grep -E "ERROR|WARNING" "$LOGFILE" | \
    sed -E 's/^.*\[([^]]+)\].*$/\1/' | \
    sort | uniq -c | sort -nr | head -5 | \
    awk '{ printf("%3d: %s\n", $1, substr($0, index($0, $2))) }'

echo -e "\nAnalyse abgeschlossen."

9.3 Textfilterung und -transformation

Während grep, sed und awk das Fundament der Textverarbeitung in Unix/Linux bilden, steht eine Vielzahl weiterer Kommandozeilenwerkzeuge zur Verfügung, die sich auf bestimmte Aspekte der Textfilterung und -transformation spezialisiert haben. Diese Tools folgen dem Unix-Prinzip, eine Aufgabe gut zu erledigen und mit anderen Werkzeugen über Standardein- und -ausgabe zu interagieren. In diesem Abschnitt betrachten wir wichtige Werkzeuge jenseits des klassischen Trios, die jeder Shell-Programmierer kennen sollte.

9.3.1 Sortieren und Einzigartigkeit

9.3.1.1 sort: Flexible Textsortierfunktionen

sort nimmt Textzeilen als Eingabe und gibt sie in sortierter Reihenfolge aus:

# Einfache alphabetische Sortierung
sort datei.txt

# Numerische Sortierung
sort -n zahlen.txt

# Umgekehrte Sortierung
sort -r datei.txt

# Sortieren nach der dritten Spalte (numerisch)
sort -k3,3n daten.txt

# Zufällige Reihenfolge (shuffle)
sort -R datei.txt

Wichtige Optionen für sort:

Option Beschreibung
-n Numerische Sortierung
-h “Human numeric” Sortierung (z.B. 1K, 2M, 3G)
-r Umgekehrte Reihenfolge
-k POS1[,POS2] Schlüsselspezifikation für Spalten
-t TRENNZEICHEN Feldtrennzeichen festlegen
-f Ignoriere Groß-/Kleinschreibung
-u Nur eindeutige Zeilen ausgeben (wie sort + uniq)
-R Zufällige Sortierung
-o DATEI Ausgabe in eine Datei umleiten

Anwendungsbeispiele:

# CSV-Datei nach der zweiten Spalte (numerisch) sortieren
sort -t';' -k2,2n daten.csv

# Sortieren einer Datei mit IP-Adressen
sort -t. -k1,1n -k2,2n -k3,3n -k4,4n ips.txt

# Top 10 der am häufigsten vorkommenden Wörter in einer Datei
cat textdatei.txt | tr -s ' ' '\n' | sort | uniq -c | sort -nr | head -10

9.3.1.2 uniq: Identifizierung und Zählung eindeutiger Zeilen

uniq entfernt (oder zählt) aufeinanderfolgende, identische Zeilen. Es wird typischerweise nach sort verwendet, um alle doppelten Einträge zu behandeln:

# Duplikate entfernen
sort datei.txt | uniq

# Zählen, wie oft jede Zeile vorkommt
sort datei.txt | uniq -c

# Nur Zeilen anzeigen, die mehrfach vorkommen
sort datei.txt | uniq -d

# Nur Zeilen anzeigen, die nur einmal vorkommen
sort datei.txt | uniq -u

Wichtige Optionen für uniq:

Option Beschreibung
-c Zeigt die Anzahl der Vorkommen vor jeder Zeile an
-d Zeigt nur Zeilen an, die mehrfach vorkommen
-u Zeigt nur Zeilen an, die nicht wiederholt werden
-i Ignoriert Groß-/Kleinschreibung
-f N Überspringt die ersten N Felder
-s N Überspringt die ersten N Zeichen

9.3.2 Schneiden, Teilen und Zusammenführen von Text

9.3.2.1 cut: Extrahieren von Spalten oder Zeichenbereichen

cut wird verwendet, um bestimmte Teile jeder Zeile einer Datei auszuschneiden:

# Extrahieren von Zeichen 3-8 aus jeder Zeile
cut -c3-8 datei.txt

# Extrahieren der ersten und dritten Spalte (durch Tabulator getrennt)
cut -f1,3 daten.tsv

# Extrahieren der zweiten Spalte (durch Doppelpunkt getrennt)
cut -d':' -f2 /etc/passwd

Wichtige Optionen für cut:

Option Beschreibung
-c LISTE Zeichen nach Position auswählen
-f LISTE Felder auswählen (standardmäßig TAB-getrennt)
-d TRENNZEICHEN Benutzerdefiniertes Feldtrennzeichen
-s Zeilen ohne Trennzeichen unterdrücken
--complement Das Komplemente der ausgewählten Bytes/Zeichen/Felder

9.3.2.2 paste: Spaltenweise Zusammenführung von Dateien

paste fügt die Zeilen mehrerer Dateien horizontal zusammen:

# Zwei Dateien Zeile für Zeile zusammenfügen (TAB-getrennt)
paste datei1.txt datei2.txt

# Mit benutzerdefiniertem Trennzeichen
paste -d':' datei1.txt datei2.txt

# Eine Datei in mehrere Spalten umwandeln
paste -d',' - - - < eineSpalte.txt > dreiSpalten.csv

Wichtige Optionen für paste:

Option Beschreibung
-d LISTE Trennzeichen festlegen (zyklisch verwendet)
-s Serial mode - fügt alle Zeilen einer Datei zusammen

9.3.2.3 join: Verknüpfen von Dateien basierend auf einem gemeinsamen Feld

join funktioniert ähnlich wie ein SQL-JOIN und verbindet Zeilen aus zwei Dateien basierend auf einem gemeinsamen Schlüsselfeld:

# Verknüpfen zweier Dateien über das erste Feld
join file1.txt file2.txt

# Verknüpfen über das zweite Feld in der ersten Datei und das erste Feld in der zweiten Datei
join -1 2 -2 1 file1.txt file2.txt

# Ausgabe spezifischer Felder
join -o 1.1,1.3,2.2 file1.txt file2.txt

Wichtige Optionen für join:

Option Beschreibung
-t CHAR Feldtrennzeichen festlegen
-1 FELD Verbindungsfeld in der ersten Datei
-2 FELD Verbindungsfeld in der zweiten Datei
-a DATEINR Auch ungepaarte Zeilen aus der angegebenen Datei ausgeben
-o FELDLISTE Format der Ausgabezeile
-e STRING Ersatztext für fehlende Eingabefelder

9.3.2.4 split: Aufteilung großer Dateien in kleinere Teile

split teilt eine Datei in mehrere kleinere Dateien auf:

# Aufteilen einer Datei in 1000-Zeilen-Stücke
split -l 1000 large_file.txt chunk_

# Aufteilen in Stücke bestimmter Größe
split -b 10M large_file.dat piece_

# Aufteilen mit angepassten Suffixen
split -l 100 -d data.csv data_part_

Wichtige Optionen für split:

Option Beschreibung
-l N Teilung nach N Zeilen
-b SIZE Teilung nach Größe (z.B. 10K, 5M, 1G)
-d Numerische Suffixe statt alphabetischer verwenden
-a LÄNGE Suffixlänge festlegen
--additional-suffix=SUFFIX Zusätzliches Suffix anhängen

9.3.3 Textmanipulationen und Ersetzungen

9.3.3.1 tr: Zeichenweise Ersetzung oder Löschung

tr übersetzt oder löscht Zeichen aus der Eingabe:

# Umwandlung von Kleinbuchstaben in Großbuchstaben
cat file.txt | tr 'a-z' 'A-Z'

# Löschen bestimmter Zeichen
cat file.txt | tr -d '\n\r\t'

# Komprimieren von wiederholten Zeichen
echo "viele    Leerzeichen" | tr -s ' '

Wichtige Optionen für tr:

Option Beschreibung
-d Löschen angegebener Zeichen
-s Wiederholte Zeichen auf ein einziges komprimieren
-c Die Komplementmenge der angegebenen Zeichen verwenden

9.3.3.2 expand und unexpand: Umwandlung zwischen Tabs und Leerzeichen

Diese Befehle konvertieren zwischen Tabulatoren und Leerzeichen:

# Tabs in Leerzeichen umwandeln
expand tabbed_file.txt > spaces_file.txt

# Leerzeichen in Tabs umwandeln
unexpand -a spaces_file.txt > tabbed_file.txt

9.3.3.3 fold: Umbruch von Textzeilen auf bestimmte Breite

fold bricht lange Textzeilen auf eine bestimmte Breite um:

# Umbruch nach 80 Zeichen
fold -w 80 long_line_file.txt

# Umbruch nach 40 Zeichen an Leerzeichen
fold -w 40 -s long_line_file.txt

9.3.4 Zählen und Statistik

9.3.4.1 wc: Zeilen, Wörter und Bytes zählen

wc (word count) wird verwendet, um Zeilen, Wörter und Bytes in einer Datei zu zählen:

# Vollständige Statistik
wc file.txt

# Nur Zeilen zählen
wc -l file.txt

# Nur Wörter zählen
wc -w file.txt

# Nur Bytes zählen
wc -c file.txt

Wichtige Optionen für wc:

Option Beschreibung
-l Zählt Zeilen
-w Zählt Wörter
-c Zählt Bytes
-m Zählt Zeichen
-L Die Länge der längsten Zeile anzeigen

9.3.5 Fortgeschrittene Filterung

9.3.5.1 comm: Vergleich sortierter Dateien zeilenweise

comm vergleicht zwei sortierte Dateien und zeigt Zeilen, die nur in der ersten, nur in der zweiten oder in beiden Dateien vorkommen:

# Ausgabe in drei Spalten (exklusiv-1, exklusiv-2, gemeinsam)
comm file1.txt file2.txt

# Nur Zeilen zeigen, die in beiden Dateien vorkommen
comm -12 file1.txt file2.txt

# Nur Zeilen zeigen, die ausschließlich in der ersten Datei vorkommen
comm -23 file1.txt file2.txt

9.3.5.2 diff: Unterschiede zwischen Dateien anzeigen

diff zeigt die Unterschiede zwischen Dateien zeilenweise an:

# Standardausgabe
diff file1.txt file2.txt

# Seite-an-Seite-Vergleich
diff -y file1.txt file2.txt

# Zusammenfassende Statistik
diff -s file1.txt file2.txt

# Kontextdiff mit 3 Zeilen Kontext
diff -c3 file1.txt file2.txt

Wichtige Optionen für diff:

Option Beschreibung
-u Unified-Format (für Patches)
-c Kontext-Format
-y Seite-an-Seite-Format
-i Groß-/Kleinschreibung ignorieren
-b Änderungen in der Anzahl von Leerzeichen ignorieren
-w Alle Leerzeichen ignorieren
-B Änderungen ignorieren, die nur leere Zeilen betreffen

9.3.5.3 patch: Anwenden von diff-Ausgaben auf Dateien

patch wendet die von diff erzeugten Änderungen auf Dateien an:

# Patch von diff-Ausgabe erstellen
diff -u original.txt modified.txt > changes.patch

# Patch auf eine Datei anwenden
patch original.txt < changes.patch

# Rückgängig machen eines Patches
patch -R original.txt < changes.patch

9.3.6 Integration in Shell-Skripte

Die wahre Stärke dieser Werkzeuge entfaltet sich bei der Integration in Shell-Skripte. Hier einige praktische Beispiele:

9.3.6.1 Beispiel: Dubletten in großen Datensätzen finden

#!/bin/bash
# Findet und meldet doppelte Einträge in einer Datendatei

if [ $# -ne 1 ]; then
    echo "Verwendung: $0 <datendatei>"
    exit 1
fi

DATAFILE=$1

if [ ! -f "$DATAFILE" ]; then
    echo "Fehler: Datei nicht gefunden: $DATAFILE"
    exit 2
fi

# Spalte festlegen, nach der gesucht werden soll (z.B. Email-Adressen in Spalte 3)
COLUMN=3
DELIMITER=","

echo "Suche nach doppelten Einträgen in Spalte $COLUMN..."

# Extrahieren der Spalte, sortieren, zählen und filtern
cut -d"$DELIMITER" -f"$COLUMN" "$DATAFILE" | sort | uniq -c | sort -nr | grep -v "^ *1 " > duplicates.tmp

if [ -s duplicates.tmp ]; then
    COUNT=$(wc -l < duplicates.tmp)
    echo "Gefunden: $COUNT doppelte Einträge."
    echo "Häufigste Duplikate:"
    head -10 duplicates.tmp

    # Extraktion der tatsächlichen doppelten Werte
    awk '{ $1=""; print substr($0,2) }' duplicates.tmp > duplicate_values.tmp
    
    echo -e "\nBeispiele von betroffenen Zeilen:"
    # Für jeden doppelten Wert einige Beispielzeilen ausgeben
    COUNT=0
    while read -r value && [ $COUNT -lt 5 ]; do
        echo -e "\nDuplikat: $value"
        grep "$value" "$DATAFILE" | head -3
        COUNT=$((COUNT + 1))
    done < duplicate_values.tmp
else
    echo "Keine doppelten Einträge gefunden."
fi

# Aufräumen
rm -f duplicates.tmp duplicate_values.tmp

exit 0

9.3.6.2 Beispiel: Datenextraktion und -transformation

#!/bin/bash
# Extrahiert und transformiert Daten aus einer CSV-Datei für einen Bericht

INPUT_FILE=$1
OUTPUT_FILE="report_$(date +%Y%m%d).csv"
TEMP_DIR=$(mktemp -d)

if [ ! -f "$INPUT_FILE" ]; then
    echo "Fehler: Eingabedatei nicht gefunden."
    exit 1
fi

echo "Verarbeite Datei: $INPUT_FILE"
echo "Ausgabe wird gespeichert in: $OUTPUT_FILE"

# 1. Extrahieren relevanter Spalten
echo "1. Extrahieren relevanter Spalten..."
cut -d',' -f1,3,5,7 "$INPUT_FILE" > "$TEMP_DIR/step1.csv"

# 2. Filtern nach bestimmten Kriterien
echo "2. Filtern der Daten..."
grep -v "^#" "$TEMP_DIR/step1.csv" | grep "ACTIVE" > "$TEMP_DIR/step2.csv"

# 3. Sortieren nach der ersten Spalte
echo "3. Sortieren der Daten..."
sort -t',' -k1,1 "$TEMP_DIR/step2.csv" > "$TEMP_DIR/step3.csv"

# 4. Hinzufügen einer Kopfzeile
echo "4. Formatieren der Ausgabe..."
echo "ID,Name,Status,Wert" > "$OUTPUT_FILE"
cat "$TEMP_DIR/step3.csv" >> "$OUTPUT_FILE"

# 5. Zusammenfassung erstellen
echo "5. Zusammenfassung erstellen..."
TOTAL_RECORDS=$(wc -l < "$TEMP_DIR/step3.csv")
TOTAL_VALUE=$(cut -d',' -f4 "$TEMP_DIR/step3.csv" | tr -d '$' | awk '{ sum += $1 } END { print sum }')

echo -e "\nBerichtszusammenfassung:"
echo "------------------------"
echo "Datensätze gesamt: $TOTAL_RECORDS"
echo "Gesamtwert: \$$TOTAL_VALUE"

# Säubern
rm -rf "$TEMP_DIR"

echo "Verarbeitung abgeschlossen."

9.3.6.3 Beispiel: Logdatei-Rotation mit Backup

#!/bin/bash
# Rotiert Logdateien und erstellt komprimierte Backups

LOG_DIR="/var/log/myapp"
MAX_LOGS=5
TIMESTAMP=$(date +%Y%m%d-%H%M%S)

# Prüfen, ob das Verzeichnis existiert
if [ ! -d "$LOG_DIR" ]; then
    echo "Fehler: Log-Verzeichnis nicht gefunden: $LOG_DIR"
    exit 1
fi

# Aktive Logdatei rotieren
if [ -f "$LOG_DIR/current.log" ]; then
    echo "Rotiere aktive Logdatei..."
    
    # Kopfzeilen (erste 10 Zeilen) extrahieren
    head -10 "$LOG_DIR/current.log" > "$LOG_DIR/header.tmp"
    
    # Zusammenfassung erstellen
    echo "Erstelle Zusammenfassung..."
    TOTAL_LINES=$(wc -l < "$LOG_DIR/current.log")
    ERROR_LINES=$(grep -c "ERROR" "$LOG_DIR/current.log")
    WARNING_LINES=$(grep -c "WARNING" "$LOG_DIR/current.log")
    
    # Extrahieren der letzten 100 Fehler für das Inhaltsverzeichnis
    grep "ERROR" "$LOG_DIR/current.log" | tail -100 > "$LOG_DIR/errors.tmp"
    
    # Zusammenfassungsdatei erstellen
    cat > "$LOG_DIR/summary_$TIMESTAMP.txt" << EOF
LOG ROTATION SUMMARY - $TIMESTAMP
===============================
Datei: current.log
Zeilen gesamt: $TOTAL_LINES
Fehler: $ERROR_LINES
Warnungen: $WARNING_LINES

TOP ERRORS:
-----------
$(sort "$LOG_DIR/errors.tmp" | uniq -c | sort -nr | head -10)

FIRST ENTRIES:
-------------
$(head -5 "$LOG_DIR/current.log")

LAST ENTRIES:
------------
$(tail -5 "$LOG_DIR/current.log")
EOF
    
    # Komprimieren und archivieren der aktuellen Logdatei
    echo "Komprimiere Logdatei..."
    cat "$LOG_DIR/header.tmp" "$LOG_DIR/summary_$TIMESTAMP.txt" > "$LOG_DIR/index_$TIMESTAMP.txt"
    tar -czf "$LOG_DIR/archive/log_$TIMESTAMP.tar.gz" -C "$LOG_DIR" current.log index_$TIMESTAMP.txt
    
    # Aktive Logdatei leeren
    echo "Datei gesäubert: $(date)" > "$LOG_DIR/current.log"
    
    # Aufräumen
    rm -f "$LOG_DIR/header.tmp" "$LOG_DIR/errors.tmp"
    
    # Alte Archivdateien rotieren
    echo "Rotiere alte Archive..."
    ls -t "$LOG_DIR/archive/"*.tar.gz | tail -n +$((MAX_LOGS+1)) | xargs -r rm
    
    echo "Logrotation abgeschlossen."
else
    echo "Keine aktive Logdatei gefunden."
fi

exit 0

9.4 Dateioperationen: Lesen, Schreiben, Anhängen

Die Fähigkeit, effektiv mit Dateien zu arbeiten, ist ein grundlegender Aspekt der Shell-Programmierung. In diesem Abschnitt betrachten wir, wie man in Bash-Skripten Dateien lesen, schreiben und manipulieren kann, ohne auf externe Programme wie sed oder awk zurückgreifen zu müssen. Wir behandeln die eingebauten Methoden der Bash, Dateiumleitungen und spezielle Techniken für den effizienten Umgang mit Dateiinhalten.

9.4.1 Dateiein- und -ausgabe in der Bash

9.4.1.1 Dateiumleitungen

Bash bietet drei Standarddateihandles: - stdin (0): Standardeingabe - stdout (1): Standardausgabe - stderr (2): Standardfehlerausgabe

Mit Umleitungsoperatoren können diese Handles umgelenkt werden:

# Ausgabeumleitungen
command > file        # Stdout in Datei umleiten (überschreibt)
command >> file       # Stdout an Datei anhängen
command 2> file       # Stderr in Datei umleiten
command &> file       # Stdout und Stderr in Datei umleiten
command > file 2>&1   # Stdout und Stderr in Datei umleiten (alternativer Syntax)

# Eingabeumleitungen
command < file        # Stdin aus Datei lesen
command << EOL        # Here-Document (Eingabe bis zum Begrenzungswort)
...
EOL
command <<< "string"  # Here-String (Eingabe aus dem angegebenen String)

# Pipes
command1 | command2   # Stdout von command1 an stdin von command2 weiterleiten

9.4.1.2 Verhindern des Überschreibens existierender Dateien

Um zu verhindern, dass existierende Dateien versehentlich überschrieben werden, kann die Option noclobber aktiviert werden:

# Aktivieren der noclobber-Option
set -o noclobber

# Versuch, eine existierende Datei zu überschreiben
echo "Neuer Inhalt" > datei.txt    # Fehlermeldung, wenn die Datei existiert

# Erzwingen des Überschreibens trotz noclobber
echo "Neuer Inhalt" >| datei.txt   # Überschreiben erzwingen

9.4.2 Lesen von Dateien

9.4.2.1 Zeilenweise Lesen mit der read-Anweisung

Die read-Anweisung ist die primäre Methode zum Einlesen von Daten in ein Bash-Skript:

#!/bin/bash

# Einfaches zeilenweises Lesen einer Datei
while IFS= read -r line; do
    echo "Verarbeite Zeile: $line"
done < "datei.txt"

Der Parameter IFS= (Internal Field Separator) sorgt dafür, dass führende und nachfolgende Leerzeichen erhalten bleiben. Die Option -r verhindert, dass Backslash-Zeichen als Escape-Zeichen interpretiert werden.

9.4.2.2 Lesen von Dateien mit spezifischen Trennzeichen

Oft müssen CSV-Dateien oder andere strukturierte Daten gelesen werden:

#!/bin/bash

# CSV-Datei lesen und nach Spalten aufteilen
while IFS=',' read -r spalte1 spalte2 spalte3 rest; do
    echo "Spalte 1: $spalte1"
    echo "Spalte 2: $spalte2"
    echo "Spalte 3: $spalte3"
    echo "Rest: $rest"
    echo "-------------------"
done < "daten.csv"

Wenn die Anzahl der Spalten variiert oder unbekannt ist, kann der read-Befehl mit einem Array verwendet werden:

#!/bin/bash

# CSV-Datei lesen und in ein Array einlesen
while IFS=',' read -r -a felder; do
    echo "Anzahl der Felder: ${#felder[@]}"
    echo "Erstes Feld: ${felder[0]}"
    echo "Zweites Feld: ${felder[1]}"
    
    # Alle Felder durchlaufen
    for i in "${!felder[@]}"; do
        echo "Feld $i: ${felder[$i]}"
    done
    echo "-------------------"
done < "daten.csv"

9.4.2.3 Gesamte Datei auf einmal einlesen

In manchen Fällen ist es sinnvoll, eine gesamte Datei auf einmal einzulesen:

#!/bin/bash

# Gesamte Datei in eine Variable einlesen
inhalt=$(<datei.txt)
echo "Dateiinhalt: $inhalt"

# Alternative Methode (funktioniert auch in älteren Bash-Versionen)
inhalt=$(cat datei.txt)
echo "Dateiinhalt: $inhalt"

Vorsicht: Diese Methode ist nur für kleine Dateien geeignet, da der gesamte Inhalt im Speicher gehalten wird.

9.4.2.4 Lesen von Dateien mit Zeilennummern

Manchmal ist es nützlich, während des Lesens Zeilennummern anzuzeigen:

#!/bin/bash

zeilennr=0
while IFS= read -r line; do
    ((zeilennr++))
    echo "$zeilennr: $line"
done < "datei.txt"

9.4.2.5 Ausgewählte Zeilen lesen

#!/bin/bash

# Nur bestimmte Zeilen lesen (z.B. Zeilen 5-10)
start_line=5
end_line=10
current_line=0

while IFS= read -r line && [ $current_line -le $end_line ]; do
    ((current_line++))
    
    if [ $current_line -ge $start_line ]; then
        echo "Zeile $current_line: $line"
    fi
done < "datei.txt"

9.4.3 Schreiben in Dateien

9.4.3.1 Einfaches Schreiben mit echo und Umleitungen

Der einfachste Weg, in eine Datei zu schreiben, ist die Verwendung von echo mit Umleitungen:

#!/bin/bash

# Datei erstellen/überschreiben
echo "Zeile 1" > datei.txt

# An Datei anhängen
echo "Zeile 2" >> datei.txt
echo "Zeile 3" >> datei.txt

9.4.3.2 Mehrere Zeilen mit Here-Documents schreiben

Here-Documents (<<) bieten eine elegante Möglichkeit, mehrere Zeilen in eine Datei zu schreiben:

#!/bin/bash

# Mehrere Zeilen in eine Datei schreiben
cat > konfiguration.ini << 'EOL'
[Abschnitt1]
Option1=Wert1
Option2=Wert2

[Abschnitt2]
Option3=Wert3
# Dies ist ein Kommentar
Option4=Wert4
EOL

Die Anführungszeichen um das Begrenzungswort 'EOL' verhindern die Shell-Expansion innerhalb des Here-Documents, sodass Zeichen wie $ und Backticks wörtlich übernommen werden.

9.4.3.3 Dynamische Inhalte mit Here-Documents

Here-Documents können auch Variablen und Befehlssubstitutionen enthalten:

#!/bin/bash

name="Max Mustermann"
datum=$(date +"%Y-%m-%d")
version="1.0"

# Template mit dynamischem Inhalt erstellen
cat > bericht.txt << EOL
Bericht generiert von: $name
Datum: $datum
Version: $version

Zusammenfassung:
---------------
$(ls -la | head -5)

Systeminfos:
-----------
$(uname -a)
EOL

9.4.3.4 Schreiben mit printf

Für präzisere Formatierung kann der printf-Befehl verwendet werden:

#!/bin/bash

# Formatierte Ausgabe in eine Datei schreiben
printf "%-20s %10s %8s\n" "Name" "Wert" "Status" > tabelle.txt
printf "%-20s %10d %8s\n" "Parameter1" 42 "aktiv" >> tabelle.txt
printf "%-20s %10.2f %8s\n" "Parameter2" 3.14159 "inaktiv" >> tabelle.txt

9.4.4 Dateien modifizieren

9.4.4.1 In-Place-Bearbeitung mit temporären Dateien

Ein häufiges Muster in Bash-Skripten ist die Bearbeitung von Dateien mithilfe einer temporären Datei:

#!/bin/bash

datei="konfiguration.txt"
tempfile=$(mktemp)

# Datei ändern und in temporäre Datei schreiben
while IFS= read -r line; do
    # Zeile modifizieren (Beispiel: "DEBUG" durch "INFO" ersetzen)
    modified_line=${line//DEBUG/INFO}
    echo "$modified_line" >> "$tempfile"
done < "$datei"

# Temporäre Datei zurück in die Originaldatei verschieben
mv "$tempfile" "$datei"

9.4.4.2 Sicheres Arbeiten mit temporären Dateien

Bei der Arbeit mit temporären Dateien ist es wichtig, diese ordnungsgemäß zu behandeln:

#!/bin/bash

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

# Aufräumen bei Beendigung sicherstellen
trap 'rm -f "$tempfile"' EXIT

# Mit der temporären Datei arbeiten
echo "Daten" > "$tempfile"
# ... weitere Operationen

# Aufräumen passiert automatisch durch die trap-Anweisung

9.4.4.3 Einfügen in bestimmte Positionen in einer Datei

Um Inhalte an bestimmten Positionen in einer Datei einzufügen, kann man ebenfalls temporäre Dateien verwenden:

#!/bin/bash

datei="dokument.txt"
tempfile=$(mktemp)
zeile_zum_einfügen="Dieser Text wird nach Zeile 5 eingefügt."
position=5

# Zeilenweise lesen und modifizieren
zeilennr=0
while IFS= read -r line; do
    ((zeilennr++))
    echo "$line" >> "$tempfile"
    
    # Nach der angegebenen Position den neuen Inhalt einfügen
    if [ $zeilennr -eq $position ]; then
        echo "$zeile_zum_einfügen" >> "$tempfile"
    fi
done < "$datei"

# Temporäre Datei zurück in die Originaldatei verschieben
mv "$tempfile" "$datei"

9.4.5 Arbeiten mit Dateiattributen

9.4.5.1 Prüfen von Dateieigenschaften

Bash bietet verschiedene Testoperatoren zum Prüfen von Dateieigenschaften:

#!/bin/bash

datei="/pfad/zur/datei.txt"

# Existenz prüfen
if [ -e "$datei" ]; then
    echo "Datei existiert"
fi

# Prüfen, ob es sich um eine reguläre Datei handelt
if [ -f "$datei" ]; then
    echo "Es ist eine reguläre Datei"
fi

# Prüfen, ob es sich um ein Verzeichnis handelt
if [ -d "$datei" ]; then
    echo "Es ist ein Verzeichnis"
fi

# Lese-, Schreib- und Ausführungsrechte prüfen
if [ -r "$datei" ]; then
    echo "Datei ist lesbar"
fi

if [ -w "$datei" ]; then
    echo "Datei ist beschreibbar"
fi

if [ -x "$datei" ]; then
    echo "Datei ist ausführbar"
fi

# Dateigröße prüfen
if [ -s "$datei" ]; then
    echo "Datei ist nicht leer"
fi

9.4.5.2 Metadaten von Dateien abrufen

Mit dem Befehl stat können detaillierte Informationen über Dateien abgerufen werden:

#!/bin/bash

datei="/pfad/zur/datei.txt"

# Grundlegende Metadaten abrufen
echo "Dateigröße in Bytes: $(stat -c %s "$datei")"
echo "Letzte Änderung: $(stat -c %y "$datei")"
echo "Berechtigungen (Oktal): $(stat -c %a "$datei")"
echo "Eigentümer: $(stat -c %U "$datei")"
echo "Gruppe: $(stat -c %G "$datei")"

9.4.6 Fortgeschrittene Techniken

9.4.6.1 Prozesssubstitution

Prozesssubstitution erlaubt es, die Ausgabe eines Befehls wie eine temporäre Datei zu behandeln:

#!/bin/bash

# Vergleich zweier Befehlsausgaben
diff <(ls -la /dir1) <(ls -la /dir2)

# Lesen aus der Ausgabe eines Befehls
while read -r line; do
    echo "Verarbeite: $line"
done < <(grep "wichtig" logfile.txt)

Die Syntax <(command) erzeugt einen Dateideskriptor, der wie ein Dateiname verwendet werden kann.

9.4.6.2 Mehrere Dateien parallel verarbeiten

Mit Prozesssubstitution und paste können mehrere Dateien parallel verarbeitet werden:

#!/bin/bash

# Parallel durch mehrere Dateien iterieren
paste <(cat datei1.txt) <(cat datei2.txt) <(cat datei3.txt) | while read -r col1 col2 col3; do
    echo "Zeile aus Datei 1: $col1"
    echo "Zeile aus Datei 2: $col2"
    echo "Zeile aus Datei 3: $col3"
    echo "-------------------"
done

9.4.6.3 Binärdateien verarbeiten

Obwohl die Bash primär für Textverarbeitung ausgelegt ist, kann sie auch mit binären Daten umgehen, indem sie externe Tools verwendet:

#!/bin/bash

# Hexadezimale Darstellung einer Binärdatei erzeugen
hexdump -C binärdatei.bin > hexdarstellung.txt

# Ersten 10 Bytes einer Datei lesen (in Hexadezimal)
head -c 10 binärdatei.bin | xxd

# Binärdateien vergleichen
cmp -l datei1.bin datei2.bin | gawk '{printf "%08X %02X %02X\n", $1, strtonum(sprintf("0x%02X", $2)), strtonum(sprintf("0x%02X", $3))}'

9.4.7 Leistungsüberlegungen

9.4.7.1 Vermeiden häufiger Dateioperationen

Häufige Dateioperationen, insbesondere bei großen Dateien, können die Leistung erheblich beeinträchtigen:

#!/bin/bash

# INEFFIZIENT: Wiederholtes Anhängen an eine Datei in einer Schleife
for i in {1..1000}; do
    echo "Zeile $i" >> ausgabe.txt
done

# BESSER: Ausgabe sammeln und einmal schreiben
{
    for i in {1..1000}; do
        echo "Zeile $i"
    done
} > ausgabe.txt

9.4.7.2 Speichereffizienz beim Lesen großer Dateien

Bei sehr großen Dateien ist es wichtig, speichereffizient zu arbeiten:

#!/bin/bash

# INEFFIZIENT für große Dateien: Alles auf einmal einlesen
inhalt=$(<große_datei.txt)

# BESSER: Zeilenweise verarbeiten
while IFS= read -r line; do
    # Verarbeite jede Zeile einzeln
    process_line "$line"
done < "große_datei.txt"

9.4.7.3 IO-Buffer und -Caching

Die Performance kann durch geschicktes Buffering verbessert werden:

#!/bin/bash

# Effizienter: Mehrere Zeilen auf einmal schreiben
{
    echo "Zeile 1"
    echo "Zeile 2"
    echo "Zeile 3"
    # ... weitere Ausgaben
} > ausgabe.txt

9.4.8 Praktische Beispiele

9.4.8.1 Beispiel: CSV-Datei lesen und bestimmte Spalten extrahieren

#!/bin/bash
# Extrahiert bestimmte Spalten aus einer CSV-Datei

input_file="daten.csv"
output_file="extrahiert.csv"
columns=(1 3 5)  # Spalten, die extrahiert werden sollen (1-basiert)

# Header überprüfen und Spaltennamen ausgeben
if [[ -f "$input_file" ]]; then
    header=$(head -1 "$input_file")
    IFS=',' read -r -a header_fields <<< "$header"
    
    echo "Verfügbare Spalten:"
    for i in "${!header_fields[@]}"; do
        echo "$((i+1)): ${header_fields[$i]}"
    done
    
    # Extrahierten Header erstellen
    extracted_header=""
    for col in "${columns[@]}"; do
        idx=$((col-1))
        if [[ $idx -lt ${#header_fields[@]} ]]; then
            if [[ -n "$extracted_header" ]]; then
                extracted_header="$extracted_header,${header_fields[$idx]}"
            else
                extracted_header="${header_fields[$idx]}"
            fi
        fi
    done
    
    # Header in die Ausgabedatei schreiben
    echo "$extracted_header" > "$output_file"
    
    # Zeilenweise die Datei lesen und extrahieren
    tail -n +2 "$input_file" | while IFS=',' read -r -a fields; do
        line=""
        for col in "${columns[@]}"; do
            idx=$((col-1))
            if [[ $idx -lt ${#fields[@]} ]]; then
                if [[ -n "$line" ]]; then
                    line="$line,${fields[$idx]}"
                else
                    line="${fields[$idx]}"
                fi
            fi
        done
        echo "$line" >> "$output_file"
    done
    
    echo "Extraktion abgeschlossen. Ausgabe in $output_file"
else
    echo "Fehler: Eingabedatei $input_file nicht gefunden!"
    exit 1
fi

9.4.8.2 Beispiel: Konfigurationsdatei lesen und modifizieren

#!/bin/bash
# Liest und modifiziert eine Konfigurationsdatei im INI-Format

config_file="config.ini"
temp_file=$(mktemp)
section=""

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

# Funktion zum Setzen eines Wertes in der Konfigurationsdatei
update_config() {
    local target_section="$1"
    local key="$2"
    local value="$3"
    local found=false
    local section_found=false
    
    # Datei lesen und modifizieren
    while IFS= read -r line; do
        # Abschnitt erkennen
        if [[ "$line" =~ ^\[([^]]+)\]$ ]]; then
            section="${BASH_REMATCH[1]}"
            if [[ "$section" == "$target_section" ]]; then
                section_found=true
            fi
        # Schlüssel/Wert-Paar in richtigem Abschnitt erkennen und ändern
        elif [[ "$section" == "$target_section" && "$line" =~ ^($key)[[:space:]]*=[[:space:]]*(.*)$ ]]; then
            echo "$key = $value"
            found=true
        else
            echo "$line"
        fi
    done < "$config_file" > "$temp_file"
    
    # Wenn Abschnitt existiert, aber der Schlüssel nicht gefunden wurde
    if $section_found && ! $found; then
        # Zurück zum Abschnitt gehen und den Schlüssel hinzufügen
        section=""
        while IFS= read -r line; do
            echo "$line"
            if [[ "$line" =~ ^\[([^]]+)\]$ ]]; then
                section="${BASH_REMATCH[1]}"
                if [[ "$section" == "$target_section" ]]; then
                    echo "$key = $value"
                fi
            fi
        done < "$temp_file" > "$temp_file.2"
        mv "$temp_file.2" "$temp_file"
    fi
    
    # Wenn weder Abschnitt noch Schlüssel gefunden wurden
    if ! $section_found; then
        {
            echo ""
            echo "[$target_section]"
            echo "$key = $value"
        } >> "$temp_file"
    fi
    
    # Temporäre Datei in die Konfigurationsdatei kopieren
    mv "$temp_file" "$config_file"
}

# Funktion zum Auslesen eines Werts aus der Konfigurationsdatei
read_config() {
    local target_section="$1"
    local key="$2"
    local default_value="${3:-}"
    local value="$default_value"
    local current_section=""
    
    if [[ -f "$config_file" ]]; then
        while IFS= read -r line; do
            # Leere Zeilen und Kommentare überspringen
            [[ -z "$line" || "$line" =~ ^[[:space:]]*[#;] ]] && continue
            
            # Abschnitt erkennen
            if [[ "$line" =~ ^\[([^]]+)\]$ ]]; then
                current_section="${BASH_REMATCH[1]}"
                continue
            fi
            
            # Schlüssel/Wert-Paar im richtigen Abschnitt erkennen
            if [[ "$current_section" == "$target_section" && "$line" =~ ^($key)[[:space:]]*=[[:space:]]*(.*)$ ]]; then
                value="${BASH_REMATCH[2]}"
                # Anführungszeichen entfernen, falls vorhanden
                value="${value#\"}"
                value="${value%\"}"
                value="${value#\'}"
                value="${value%\'}"
                break
            fi
        done < "$config_file"
    fi
    
    echo "$value"
}

# Beispiel: Konfigurationswerte lesen
server_ip=$(read_config "Server" "ip" "localhost")
port=$(read_config "Server" "port" "8080")
username=$(read_config "Credentials" "username" "admin")

echo "Aktuelle Konfiguration:"
echo "Server IP: $server_ip"
echo "Port: $port"
echo "Username: $username"

# Beispiel: Konfigurationswerte setzen
update_config "Server" "port" "9090"
update_config "Logging" "level" "DEBUG"

echo "Konfiguration aktualisiert."

9.4.8.3 Beispiel: Logdatei verarbeiten und Statistiken erstellen

#!/bin/bash
# Verarbeitet eine Logdatei und erstellt eine statistische Auswertung

log_file="/var/log/apache2/access.log"
output_file="log_stats_$(date +%Y%m%d).txt"

if [ ! -f "$log_file" ]; then
    echo "Fehler: Logdatei $log_file nicht gefunden!"
    exit 1
fi

# Temporäres Verzeichnis für die Verarbeitung
temp_dir=$(mktemp -d)
trap 'rm -rf "$temp_dir"' EXIT

echo "Verarbeite Logdatei: $log_file"
echo "Ausgabe wird gespeichert in: $output_file"

# IP-Adressen extrahieren
awk '{print $1}' "$log_file" > "$temp_dir/ips.txt"

# HTTP-Statuscodes extrahieren
awk '{print $9}' "$log_file" > "$temp_dir/status_codes.txt"

# Anfrage-URLs extrahieren
awk '{print $7}' "$log_file" > "$temp_dir/urls.txt"

# Zeitstempel extrahieren
awk '{print $4 $5}' "$log_file" | tr -d '[]' > "$temp_dir/timestamps.txt"

# Statistik erstellen
{
    echo "=== LOGDATEI-STATISTIK ==="
    echo "Generiert am: $(date)"
    echo "Datei: $log_file"
    echo "Zeitraum: $(head -1 "$temp_dir/timestamps.txt") bis $(tail -1 "$temp_dir/timestamps.txt")"
    echo ""
    
    echo "=== ALLGEMEINE INFORMATIONEN ==="
    echo "Gesamtzahl der Einträge: $(wc -l < "$log_file")"
    echo "Eindeutige IP-Adressen: $(sort "$temp_dir/ips.txt" | uniq | wc -l)"
    echo ""
    
    echo "=== TOP 10 IP-ADRESSEN ==="
    sort "$temp_dir/ips.txt" | uniq -c | sort -nr | head -10
    echo ""
    
    echo "=== HTTP-STATUSCODE-VERTEILUNG ==="
    sort "$temp_dir/status_codes.txt" | uniq -c | sort -nr
    echo ""
    
    echo "=== TOP 10 ANFRAGE-URLS ==="
    sort "$temp_dir/urls.txt" | uniq -c | sort -nr | head -10
    echo ""
    
    echo "=== STÜNDLICHE VERTEILUNG ==="
    awk '{print substr($4,2,2)}' "$log_file" | sort | uniq -c | sort -k2,2n | awk '{printf "Stunde %s: %s Anfragen\n", $2, $1}'
    echo ""
    
    echo "=== FEHLERSTATISTIK ==="
    grep -E ' (4[0-9]{2}|5[0-9]{2}) ' "$log_file" | awk '{print $9}' | sort | uniq -c | sort -nr
    echo ""
    
    echo "=== DURCHSCHNITTLICHE ANTWORTGRÖSSE ==="
    awk '{sum+=$10; count++} END {printf "%.2f Bytes pro Anfrage\n", sum/count}' "$log_file"
    echo ""
    
    echo "=== ABSCHLUSS ==="
    echo "Auswertung abgeschlossen."
    echo "Weitere Informationen finden Sie in der ausführlichen Analyse."
} > "$output_file"

echo "Statistik wurde erstellt in: $output_file"
exit 0

9.5 Arbeiten mit CSV und strukturierten Textdaten

In der Praxis arbeiten Systemadministratoren und Entwickler häufig mit strukturierten Textdaten, insbesondere im CSV-Format (Comma-Separated Values) oder ähnlichen Formaten wie TSV (Tab-Separated Values). Diese Formate werden für Datenaustausch, Berichterstellung, Konfigurationsdateien und Protokollierung verwendet. In diesem Abschnitt betrachten wir, wie Shell-Skripte effektiv mit solchen Formaten umgehen können, ohne auf externe Datenverarbeitungstools zurückgreifen zu müssen.

9.5.1 Grundlagen von CSV und anderen strukturierten Textformaten

9.5.1.1 CSV-Format

CSV-Dateien sind einfache Textdateien, bei denen Werte durch Kommas (oder andere Trennzeichen) getrennt sind:

Name,Alter,Stadt
Max Mustermann,42,Berlin
Anna Schmidt,35,Hamburg
Klaus Meyer,28,München

Obwohl das Format einfach erscheint, gibt es verschiedene Varianten und potenzielle Fallstricke:

9.5.1.2 Andere strukturierte Textformate

Neben CSV gibt es weitere strukturierte Textformate:

9.5.2 Lesen und Verarbeiten von CSV-Dateien

9.5.2.1 Einfaches CSV-Parsing mit Bash

Für einfache CSV-Dateien ohne komplexe Formatierung kann die Bash direkt verwendet werden:

#!/bin/bash

# Festlegen des Feldtrennzeichens für `read`
IFS=','

# Überspringen der Kopfzeile (optional)
read -r header < "daten.csv"
echo "Spaltenüberschriften: $header"

# Zeilenweise durch die Datei iterieren (ohne Kopfzeile)
tail -n +2 "daten.csv" | while read -r name alter stadt; do
    echo "Name: $name, Alter: $alter, Stadt: $stadt"
done

9.5.2.2 Robustes CSV-Parsing mit dynamischer Spaltenzahl

Für Dateien mit variabler Spaltenzahl bietet sich die Verwendung von Arrays an:

#!/bin/bash

# CSV-Datei mit dynamischer Spaltenzahl verarbeiten
while IFS=',' read -r -a fields; do
    echo "Zeile enthält ${#fields[@]} Felder:"
    for i in "${!fields[@]}"; do
        echo "  Feld $i: ${fields[$i]}"
    done
done < "daten.csv"

9.5.2.3 Umgang mit Anführungszeichen und Escape-Sequenzen

Die eingebauten Bash-Funktionen stoßen bei komplexeren CSV-Formaten an ihre Grenzen. Ein pragmatischer Ansatz ist die Verwendung von awk oder anderen Hilfsprogrammen:

#!/bin/bash

# CSV mit Anführungszeichen und Escape-Sequenzen verarbeiten
awk -F, '
BEGIN {
    FPAT = "([^,]+)|(\"[^\"]+\")"  # Pattern für Felder: entweder ohne Komma oder in Anführungszeichen
}
{
    for (i = 1; i <= NF; i++) {
        # Anführungszeichen entfernen und maskierte Anführungszeichen entfernen
        gsub(/^"|"$/, "", $i)
        gsub(/""/, "\"", $i)
        printf "Feld %d: %s\n", i, $i
    }
    printf "\n"
}' komplexe_daten.csv

9.5.2.4 CSV-Dateien mit Kopfzeilen verarbeiten

Häufig haben CSV-Dateien Kopfzeilen, die die Bedeutung der Spalten angeben:

#!/bin/bash

# CSV mit Kopfzeilen verarbeiten
csv_file="daten.csv"

# Kopfzeile lesen und als Spaltennamen verwenden
IFS=',' read -r -a headers < "$csv_file"

# Durch die Datenzeilen iterieren
tail -n +2 "$csv_file" | while IFS=',' read -r -a values; do
    echo "Datensatz:"
    for i in "${!headers[@]}"; do
        if [ $i -lt ${#values[@]} ]; then
            echo "  ${headers[$i]}: ${values[$i]}"
        fi
    done
    echo ""
done

9.5.3 Transformation und Filterung von CSV-Daten

9.5.3.1 Filtern von Zeilen nach Kriterien

Eine häufige Aufgabe ist das Filtern von Zeilen basierend auf bestimmten Kriterien:

#!/bin/bash

# Filtern von CSV-Zeilen nach Kriterien
csv_file="mitarbeiter.csv"
filter_city="Berlin"
min_age=30

# Kopfzeile beibehalten
head -1 "$csv_file" > "gefiltert.csv"

# Filtern und Anhängen der passenden Zeilen
tail -n +2 "$csv_file" | while IFS=',' read -r name alter stadt; do
    # Nur Mitarbeiter aus Berlin, die mindestens 30 Jahre alt sind
    if [[ "$stadt" == "$filter_city" && $alter -ge $min_age ]]; then
        echo "$name,$alter,$stadt" >> "gefiltert.csv"
    fi
done

echo "Gefilterte Daten wurden in gefiltert.csv gespeichert."

9.5.3.2 Spalten auswählen und neu anordnen

Das Extrahieren bestimmter Spalten und ihre Neuanordnung ist eine weitere gängige Operation:

#!/bin/bash

# Spalten auswählen und neu anordnen
input_csv="quelldaten.csv"
output_csv="zieldaten.csv"
columns=(3 1 4)  # Indizes der gewünschten Spalten (1-basiert)

# Kopfzeile entsprechend den ausgewählten Spalten umordnen
IFS=',' read -r -a headers < "$input_csv"
new_header=""
for col in "${columns[@]}"; do
    index=$((col - 1))
    if [ $index -lt ${#headers[@]} ]; then
        if [ -n "$new_header" ]; then
            new_header+=","
        fi
        new_header+="${headers[$index]}"
    fi
done
echo "$new_header" > "$output_csv"

# Datenzeilen entsprechend transformieren
tail -n +2 "$input_csv" | while IFS=',' read -r -a fields; do
    new_line=""
    for col in "${columns[@]}"; do
        index=$((col - 1))
        if [ $index -lt ${#fields[@]} ]; then
            if [ -n "$new_line" ]; then
                new_line+=","
            fi
            new_line+="${fields[$index]}"
        fi
    done
    echo "$new_line" >> "$output_csv"
done

echo "Spalten wurden neu angeordnet und in $output_csv gespeichert."

9.5.3.3 Aggregation und Zusammenfassung

Oft müssen CSV-Daten zusammengefasst oder aggregiert werden, z.B. für Berichte:

#!/bin/bash

# Aggregation von CSV-Daten nach einer Gruppierungsspalte
csv_file="verkaufsdaten.csv"
group_column=2  # Region (Spaltenindex, 1-basiert)
value_column=3  # Verkaufsbetrag (Spaltenindex, 1-basiert)

# Überspringen der Kopfzeile
tail -n +2 "$csv_file" | {
    # Arrays für die Aggregation
    declare -A sum
    declare -A count
    
    # Einlesen und Aggregieren der Daten
    while IFS=',' read -r -a fields; do
        group="${fields[$((group_column-1))]}"
        value="${fields[$((value_column-1))]}"
        
        # Numerische Validierung
        if [[ "$value" =~ ^[0-9]+(\.[0-9]+)?$ ]]; then
            sum["$group"]=$(echo "${sum["$group"]:=0} + $value" | bc)
            ((count["$group"]++))
        fi
    done
    
    # Ausgabe der Ergebnisse
    echo "Region,Anzahl,Summe,Durchschnitt"
    for group in "${!sum[@]}"; do
        avg=$(echo "scale=2; ${sum["$group"]} / ${count["$group"]}" | bc)
        echo "$group,${count["$group"]},${sum["$group"]},$avg"
    done
} | sort -t',' -k3,3nr > "zusammenfassung.csv"

echo "Zusammenfassung wurde in zusammenfassung.csv gespeichert."

9.5.4 Konvertierung zwischen verschiedenen Formaten

9.5.4.1 Von CSV zu TSV konvertieren

Die Konvertierung zwischen Formaten mit unterschiedlichen Trennzeichen ist oft notwendig:

#!/bin/bash

# CSV zu TSV konvertieren
csv_file="daten.csv"
tsv_file="${csv_file%.csv}.tsv"

# Einfache Konvertierung für Dateien ohne komplexe Formatierung
sed 's/,/\t/g' "$csv_file" > "$tsv_file"

echo "Datei wurde nach $tsv_file konvertiert."

9.5.4.2 Von CSV zu JSON konvertieren

Für komplexere Konvertierungen, wie von CSV zu JSON, kann ein ausführlicheres Skript verwendet werden:

#!/bin/bash

# CSV zu JSON konvertieren
csv_file="daten.csv"
json_file="${csv_file%.csv}.json"

# Kopfzeile lesen
IFS=',' read -r -a headers < "$csv_file"

# JSON-Array öffnen
echo "[" > "$json_file"

# Durch die Datenzeilen iterieren und JSON-Objekte erstellen
row_count=0
tail -n +2 "$csv_file" | while IFS=',' read -r -a values; do
    ((row_count++))
    
    # JSON-Objekt öffnen
    echo "  {" >> "$json_file"
    
    # Felder als JSON-Eigenschaften ausgeben
    for i in "${!headers[@]}"; do
        if [ $i -lt ${#values[@]} ]; then
            # Letztes Feld ohne Komma
            if [ $i -eq $(( ${#headers[@]} - 1 )) ]; then
                echo "    \"${headers[$i]}\": \"${values[$i]}\"" >> "$json_file"
            else
                echo "    \"${headers[$i]}\": \"${values[$i]}\"," >> "$json_file"
            fi
        fi
    done
    
    # JSON-Objekt schließen, mit oder ohne Komma
    if [ $row_count -eq $(( $(wc -l < "$csv_file") - 1 )) ]; then
        echo "  }" >> "$json_file"
    else
        echo "  }," >> "$json_file"
    fi
done

# JSON-Array schließen
echo "]" >> "$json_file"

echo "Datei wurde nach $json_file konvertiert."

9.5.4.3 Fixed-Width zu CSV konvertieren

Fixed-Width-Dateien sind in älteren Systemen noch verbreitet und müssen oft in ein moderneres Format konvertiert werden:

#!/bin/bash

# Fixed-Width zu CSV konvertieren
fixed_file="daten.txt"
csv_file="${fixed_file%.txt}.csv"

# Definition der Spaltenbreiten
widths=(10 5 20 15)  # Beispiel: Name (10), Alter (5), Stadt (20), Land (15)

# Verarbeitung der Datei
while IFS= read -r line; do
    # Felder basierend auf festen Breiten extrahieren
    pos=0
    csv_line=""
    
    for width in "${widths[@]}"; do
        field="${line:$pos:$width}"
        field=$(echo "$field" | sed 's/^ *//;s/ *$//')  # Leerzeichen am Anfang und Ende entfernen
        
        if [ -n "$csv_line" ]; then
            csv_line+=","
        fi
        
        # Wenn das Feld Kommas enthält, in Anführungszeichen einschließen
        if [[ "$field" == *,* ]]; then
            # Anführungszeichen im Feld maskieren
            field="${field//\"/\"\"}"
            csv_line+="\"$field\""
        else
            csv_line+="$field"
        fi
        
        pos=$((pos + width))
    done
    
    echo "$csv_line" >> "$csv_file"
done < "$fixed_file"

echo "Datei wurde nach $csv_file konvertiert."

9.5.5 Validierung und Bereinigung von CSV-Daten

9.5.5.1 Grundlegende Validierung

Bevor CSV-Daten verarbeitet werden, sollten sie auf Korrektheit geprüft werden:

#!/bin/bash

# CSV-Datei validieren
csv_file="daten.csv"
expected_columns=4  # Erwartete Anzahl von Spalten

# Kopfzeile prüfen
IFS=',' read -r -a headers < "$csv_file"
if [ ${#headers[@]} -ne $expected_columns ]; then
    echo "Fehler: Kopfzeile enthält ${#headers[@]} Spalten, erwartet wurden $expected_columns"
    exit 1
fi

# Datenzeilen prüfen
line_number=1
errors=0
tail -n +2 "$csv_file" | while IFS=',' read -r -a fields; do
    ((line_number++))
    
    if [ ${#fields[@]} -ne $expected_columns ]; then
        echo "Fehler in Zeile $line_number: ${#fields[@]} Spalten gefunden, erwartet wurden $expected_columns"
        ((errors++))
    fi
    
    # Prüfung auf leere Pflichtfelder (z.B. Spalten 1 und 3)
    if [ -z "${fields[0]}" ] || [ -z "${fields[2]}" ]; then
        echo "Fehler in Zeile $line_number: Pflichtfelder fehlen"
        ((errors++))
    fi
    
    # Numerische Validierung (z.B. Spalte 2 sollte eine Zahl sein)
    if ! [[ "${fields[1]}" =~ ^[0-9]+(\.[0-9]+)?$ ]]; then
        echo "Fehler in Zeile $line_number: Spalte 2 ist keine gültige Zahl: ${fields[1]}"
        ((errors++))
    fi
done

if [ $errors -eq 0 ]; then
    echo "CSV-Datei ist valide."
else
    echo "CSV-Datei enthält $errors Fehler."
    exit 1
fi

9.5.5.2 Bereinigung von CSV-Daten

Oft müssen CSV-Daten vor der eigentlichen Verarbeitung bereinigt werden:

#!/bin/bash

# CSV-Daten bereinigen
input_csv="rohdaten.csv"
output_csv="bereinigt.csv"

# Kopfzeile kopieren
head -1 "$input_csv" > "$output_csv"

# Datenzeilen bereinigen
tail -n +2 "$input_csv" | while IFS=',' read -r -a fields; do
    clean_line=""
    
    for i in "${!fields[@]}"; do
        # Feld bereinigen
        field="${fields[$i]}"
        
        # Leerzeichen am Anfang und Ende entfernen
        field=$(echo "$field" | sed 's/^ *//;s/ *$//')
        
        # Nicht-druckbare Zeichen entfernen
        field=$(echo "$field" | tr -cd '[:print:]')
        
        # Spezielle Bereinigungen je nach Spaltentyp
        case $i in
            0)  # Name: Nur Buchstaben, Leerzeichen und Bindestriche erlauben
                field=$(echo "$field" | sed 's/[^a-zA-Z \-]//g')
                ;;
            1)  # Alter: Nur Zahlen erlauben
                field=$(echo "$field" | sed 's/[^0-9]//g')
                # Standardwert, wenn leer
                if [ -z "$field" ]; then
                    field="0"
                fi
                ;;
            2)  # E-Mail: Auf gültiges Format prüfen und zu Kleinbuchstaben konvertieren
                field=$(echo "$field" | tr '[:upper:]' '[:lower:]')
                if ! [[ "$field" =~ ^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$ ]]; then
                    field="invalid@example.com"
                fi
                ;;
        esac
        
        # Anführungszeichen hinzufügen, wenn nötig
        if [[ "$field" == *,* || "$field" == *\"* ]]; then
            field="${field//\"/\"\"}"  # Anführungszeichen verdoppeln
            field="\"$field\""
        fi
        
        # Feld zur bereinigten Zeile hinzufügen
        if [ -n "$clean_line" ]; then
            clean_line+=","
        fi
        clean_line+="$field"
    done
    
    echo "$clean_line" >> "$output_csv"
done

echo "Bereinigte Daten wurden in $output_csv gespeichert."

9.5.6 Arbeit mit komplexeren strukturierten Textformaten

9.5.6.1 Verarbeiten von JSON mit jq

Für JSON-Dateien ist das Werkzeug jq besonders nützlich:

#!/bin/bash

# JSON-Daten mit jq verarbeiten
json_file="daten.json"

# Prüfen, ob jq installiert ist
if ! command -v jq &> /dev/null; then
    echo "Fehler: jq ist nicht installiert. Bitte installieren Sie es mit 'apt-get install jq' oder ähnlichem."
    exit 1
fi

# Beispiele für jq-Operationen

# 1. Alle Namen aus einem Array von Objekten extrahieren
echo "Namen:"
jq -r '.[].name' "$json_file"

# 2. Filtern nach einem Kriterium
echo -e "\nPersonen über 30:"
jq -r '.[] | select(.age > 30) | .name + ", " + (.age | tostring)' "$json_file"

# 3. Aggregation und Statistik
echo -e "\nDurchschnittsalter:"
jq -r '[.[].age] | add / length' "$json_file"

# 4. Transformation in ein anderes Format (z.B. CSV)
echo -e "\nExport als CSV:"
jq -r '(.[0] | keys_unsorted) as $keys | ($keys | join(",")) + "\n" + (.[] | [.[$keys[]]] | join(","))' "$json_file"

# 5. Komplexe Transformationen
echo -e "\nGruppierung nach Stadt:"
jq -r 'group_by(.city) | map({city: .[0].city, count: length, persons: [.[].name]})' "$json_file"

9.5.6.2 Verarbeiten von XML mit xmlstarlet

Für XML-Dateien bietet sich xmlstarlet als leistungsfähiges Kommandozeilenwerkzeug an:

#!/bin/bash

# XML-Daten mit xmlstarlet verarbeiten
xml_file="daten.xml"

# Prüfen, ob xmlstarlet installiert ist
if ! command -v xmlstarlet &> /dev/null; then
    echo "Fehler: xmlstarlet ist nicht installiert. Bitte installieren Sie es mit 'apt-get install xmlstarlet' oder ähnlichem."
    exit 1
fi

# Beispiele für xmlstarlet-Operationen

# 1. Elemente auswählen
echo "Namen:"
xmlstarlet sel -t -v "//person/name" -n "$xml_file"

# 2. Filtern nach einem Attribut
echo -e "\nPersonen in Berlin:"
xmlstarlet sel -t -v "//person[@city='Berlin']/name" -n "$xml_file"

# 3. Bedingte Ausgabe mit Formatierung
echo -e "\nPersonen über 30:"
xmlstarlet sel -t -v "//person[age > 30]/name" -o ", " -v "//person[age > 30]/age" -n "$xml_file"

# 4. Transformation nach CSV
echo -e "\nExport als CSV:"
echo "Name,Alter,Stadt" > personen.csv
xmlstarlet sel -t -m "//person" -v "name" -o "," -v "age" -o "," -v "@city" -n "$xml_file" >> personen.csv
echo "CSV-Datei erstellt: personen.csv"

# 5. XML-Änderungen vornehmen
echo -e "\nÄnderungen am XML durchführen:"
xmlstarlet ed -L -u "//person[name='Max Mustermann']/@city" -v "Hamburg" "$xml_file"
echo "Stadt von Max Mustermann wurde zu Hamburg geändert."

9.5.6.3 Verarbeiten von YAML mit yq

Für YAML-Dateien wird oft yq verwendet, ein ähnliches Werkzeug wie jq für JSON:

#!/bin/bash

# YAML-Daten mit yq verarbeiten
yaml_file="config.yaml"

# Prüfen, ob yq installiert ist
if ! command -v yq &> /dev/null; then
    echo "Fehler: yq ist nicht installiert."
    exit 1
fi

# Beispiele für yq-Operationen

# 1. Werte auslesen
echo "Server-Konfiguration:"
yq eval '.server.host' "$yaml_file"
yq eval '.server.port' "$yaml_file"

# 2. Liste von Werten durchlaufen
echo -e "\nBenutzer:"
yq eval '.users[]' "$yaml_file"

# 3. Filterung
echo -e "\nAktive Benutzer:"
yq eval '.users[] | select(.active == true)' "$yaml_file"

# 4. Änderungen vornehmen und speichern
echo -e "\nPort ändern:"
yq eval '.server.port = 9090' -i "$yaml_file"
echo "Port wurde zu 9090 geändert."

# 5. Konvertierung in ein anderes Format
echo -e "\nKonvertierung nach JSON:"
yq eval -j '.' "$yaml_file" > "${yaml_file%.yaml}.json"
echo "Konvertiert nach ${yaml_file%.yaml}.json"

9.6 Textformatierung für Berichte und Ausgaben

Die Fähigkeit, gut formatierte und leicht lesbare Berichte und Ausgaben zu erstellen, ist ein wichtiger Aspekt der Shell-Programmierung. Eine klare und übersichtliche Präsentation von Informationen hilft Benutzern, Ergebnisse schnell zu erfassen und zu interpretieren. In diesem Abschnitt werden verschiedene Techniken zur Textformatierung in Shell-Skripten behandelt, von einfachen Tabulationen bis hin zu komplexen Tabellen und farbigen Ausgaben.

9.6.1 Grundlegende Textformatierung

9.6.1.1 Zeilenumbrüche und Einrückungen

Die einfachste Form der Textformatierung besteht aus Zeilenumbrüchen und Einrückungen:

#!/bin/bash

echo "Statusbericht"
echo "============"
echo ""
echo "System:     $(uname -s)"
echo "Hostname:   $(hostname)"
echo "Datum:      $(date)"
echo ""
echo "Zusammenfassung:"
echo "  - Speichernutzung: $(free -h | awk '/^Mem:/ {print $3 "/" $2}')"
echo "  - Festplattennutzung: $(df -h / | awk 'NR==2 {print $5}')"
echo "  - Laufzeit: $(uptime -p)"

9.6.1.2 Ausrichtung mit printf

Der printf-Befehl bietet deutlich mehr Kontrolle über die Formatierung als echo. Er verwendet ein Format ähnlich der C-Programmiersprache:

#!/bin/bash

# Formatierte Tabelle mit printf
printf "%-20s %-10s %-8s\n" "Name" "Größe" "Datum"
printf "%-20s %-10s %-8s\n" "--------------------" "----------" "--------"

for file in /etc/*.conf; do
    name=$(basename "$file")
    size=$(du -h "$file" | cut -f1)
    date=$(date -r "$file" "+%Y-%m-%d")
    printf "%-20s %-10s %-8s\n" "$name" "$size" "$date"
done

Die Formatspezifikationen in printf haben folgende Struktur: - %: Beginn der Formatspezifikation - Optionaler Modifikator wie - (linke Ausrichtung) oder 0 (Nullen voranstellen) - Optionale Breitenangabe (z.B. 20 für 20 Zeichen Breite) - Optionale Präzisionsangabe (z.B. .2f für 2 Dezimalstellen) - Typspezifikation (s für Zeichenketten, d für Ganzzahlen, f für Fließkommazahlen, etc.)

Weitere nützliche printf-Beispiele:

# Zahlen mit fester Anzahl von Dezimalstellen
printf "Wert: %.2f\n" 123.456789  # Ausgabe: "Wert: 123.46"

# Zahlen mit führenden Nullen
printf "ID: %04d\n" 42            # Ausgabe: "ID: 0042"

# Zentrierte Ausgabe durch Kombination von linker und rechter Ausrichtung
width=20
title="Bericht"
padding=$(( (width - ${#title}) / 2 ))
printf "%${padding}s%s%${padding}s\n" "" "$title" ""

9.6.2 Erstellung von Tabellen

9.6.2.1 Einfache Tabellen mit fester Breite

Für einfache tabellarische Ausgaben kann man printf mit festen Spaltenbreiten verwenden:

#!/bin/bash

# Daten für die Tabelle
users=("alice" "bob" "charlie" "dave")
disk_usage=("1.2G" "3.5G" "800M" "5.1G")
processes=(5 12 3 8)

# Tabellenkopf mit Trennlinie
printf "%-10s %-10s %-10s\n" "Benutzer" "Speicher" "Prozesse"
printf "%-10s %-10s %-10s\n" "----------" "----------" "----------"

# Tabelleninhalt
for i in "${!users[@]}"; do
    printf "%-10s %-10s %-10d\n" "${users[$i]}" "${disk_usage[$i]}" "${processes[$i]}"
done

9.6.2.2 Dynamische Tabellen mit angepassten Spaltenbreiten

Bei variablen Inhalten kann es sinnvoll sein, die Spaltenbreiten dynamisch anzupassen:

#!/bin/bash

# Funktion zur Berechnung der Spaltenbreite basierend auf dem Inhalt
calc_width() {
    local max_width=$1
    shift
    
    for item in "$@"; do
        if [ ${#item} -gt $max_width ]; then
            max_width=${#item}
        fi
    done
    
    echo $max_width
}

# Daten für die Tabelle
packages=("apache2" "mysql-server" "postgresql-12" "nginx")
versions=("2.4.41" "8.0.23" "12.6" "1.18.0")
status=("aktiv" "inaktiv" "gestoppt" "aktiv")

# Spaltenbreiten berechnen
pkg_width=$(calc_width 8 "Paket" "${packages[@]}")
ver_width=$(calc_width 7 "Version" "${versions[@]}")
stat_width=$(calc_width 6 "Status" "${status[@]}")

# Tabellenkopf mit Trennlinie
printf "%-${pkg_width}s  %-${ver_width}s  %-${stat_width}s\n" "Paket" "Version" "Status"
printf "%-${pkg_width}s  %-${ver_width}s  %-${stat_width}s\n" \
    "$(printf '%*s' $pkg_width | tr ' ' '-')" \
    "$(printf '%*s' $ver_width | tr ' ' '-')" \
    "$(printf '%*s' $stat_width | tr ' ' '-')"

# Tabelleninhalt
for i in "${!packages[@]}"; do
    printf "%-${pkg_width}s  %-${ver_width}s  %-${stat_width}s\n" \
        "${packages[$i]}" "${versions[$i]}" "${status[$i]}"
done

9.6.2.3 Tabellen mit Rahmen

Für eine noch professionellere Darstellung können Tabellen mit vollständigen Rahmen erstellt werden:

#!/bin/bash

# Funktion zum Zeichnen einer horizontalen Linie
draw_line() {
    local cols=("$@")
    local line="+"
    
    for width in "${cols[@]}"; do
        line+="$(printf '%*s' "$width" | tr ' ' '-')+"
    done
    
    echo "$line"
}

# Daten für die Tabelle
header=("Name" "Alter" "Position" "Abteilung")
data=(
    "Max Mustermann" "42" "Teamleiter" "Entwicklung"
    "Anna Schmidt" "35" "Projektmanager" "Marketing"
    "Klaus Meyer" "28" "Entwickler" "IT"
    "Sophie Weber" "31" "Designer" "Kreativ"
)

# Spaltenbreiten berechnen und speichern
num_cols=${#header[@]}
declare -a widths

for i in $(seq 0 $((num_cols-1))); do
    max_width=${#header[$i]}
    
    for j in $(seq 0 $((${#data[@]}/num_cols-1))); do
        idx=$((j*num_cols+i))
        if [ ${#data[$idx]} -gt $max_width ]; then
            max_width=${#data[$idx]}
        fi
    done
    
    # 2 Zeichen Padding hinzufügen
    widths[$i]=$((max_width+2))
done

# Obere Rahmenlinie zeichnen
draw_line "${widths[@]}"

# Header ausgeben
echo -n "|"
for i in $(seq 0 $((num_cols-1))); do
    printf " %-$((widths[$i]-1))s|" "${header[$i]}"
done
echo ""

# Trennlinie nach Header zeichnen
draw_line "${widths[@]}"

# Zeilen ausgeben
for i in $(seq 0 $((${#data[@]}/num_cols-1))); do
    echo -n "|"
    for j in $(seq 0 $((num_cols-1))); do
        idx=$((i*num_cols+j))
        printf " %-$((widths[$j]-1))s|" "${data[$idx]}"
    done
    echo ""
done

# Untere Rahmenlinie zeichnen
draw_line "${widths[@]}"

9.6.3 Farbige Ausgaben und Textformatierung

9.6.3.1 Verwendung von ANSI-Escape-Sequenzen

ANSI-Escape-Sequenzen ermöglichen es, Text in verschiedenen Farben und Stilen darzustellen:

#!/bin/bash

# ANSI-Farbcodes
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[0;33m'
BLUE='\033[0;34m'
MAGENTA='\033[0;35m'
CYAN='\033[0;36m'
BOLD='\033[1m'
UNDERLINE='\033[4m'
RESET='\033[0m'

# Farbige Ausgaben
echo -e "${RED}Fehler:${RESET} Datei nicht gefunden."
echo -e "${GREEN}Erfolg:${RESET} Operation abgeschlossen."
echo -e "${YELLOW}Warnung:${RESET} Wenig Speicherplatz verfügbar."
echo -e "${BLUE}Info:${RESET} System wird aktualisiert."
echo -e "${BOLD}Wichtige Mitteilung:${RESET} Bitte beachten Sie..."
echo -e "${UNDERLINE}Besonders wichtig:${RESET} Nicht vergessen!"

Die ANSI-Escape-Sequenzen haben folgende Struktur: - \033[ oder \e[: Beginn der Escape-Sequenz - Optionale Style-Codes (z.B. 1 für fett, 4 für unterstrichen) - Optional ein Farbcode (z.B. 31 für rot, 32 für grün) - m: Abschluss der Escape-Sequenz

Weitere Style-Codes: - 0: Alle Attribute zurücksetzen - 1: Fett - 2: Dünn - 3: Kursiv - 4: Unterstrichen - 5: Blinkend (langsam) - 7: Invers (Vorder- und Hintergrundfarbe tauschen)

Farbcodes für den Vordergrund: - 30: Schwarz - 31: Rot - 32: Grün - 33: Gelb - 34: Blau - 35: Magenta - 36: Cyan - 37: Weiß

Und für den Hintergrund fügt man 10 hinzu: - 40: Schwarzer Hintergrund - 41: Roter Hintergrund - usw.

9.6.3.2 Funktion für einheitliche farbige Ausgaben

Es ist nützlich, Funktionen für konsistente farbige Ausgaben zu definieren:

#!/bin/bash

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

# Nachrichtenfunktionen
info() {
    echo -e "${BLUE}[INFO]${RESET} $*"
}

success() {
    echo -e "${GREEN}[ERFOLG]${RESET} $*"
}

warning() {
    echo -e "${YELLOW}[WARNUNG]${RESET} $*" >&2
}

error() {
    echo -e "${RED}[FEHLER]${RESET} $*" >&2
}

# Verwendung
info "Starte Backup-Prozess..."
success "Backup erfolgreich abgeschlossen."
warning "Wenig Speicherplatz verfügbar (15% frei)."
error "Backup fehlgeschlagen: Zielverzeichnis nicht beschreibbar."

9.6.3.3 Prüfen der Terminalunterstützung für Farben

Nicht alle Terminals unterstützen Farben, daher sollte man prüfen, ob das aktuelle Terminal Farben unterstützt:

#!/bin/bash

# Farben nur verwenden, wenn das Terminal sie unterstützt
if [ -t 1 ] && [ -n "$TERM" ] && [ "$TERM" != "dumb" ]; then
    RED='\033[0;31m'
    GREEN='\033[0;32m'
    YELLOW='\033[0;33m'
    BLUE='\033[0;34m'
    RESET='\033[0m'
else
    RED=""
    GREEN=""
    YELLOW=""
    BLUE=""
    RESET=""
fi

# Nachrichtenfunktionen mit Farbunterstützungsprüfung
info() {
    echo -e "${BLUE}[INFO]${RESET} $*"
}

# ... weitere Funktionen wie oben ...

9.6.4 Fortgeschrittene Textformatierung

9.6.4.1 Statusanzeigen und Fortschrittsbalken

Eine einfache Statusanzeige kann durch Überschreiben der aktuellen Zeile erstellt werden:

#!/bin/bash

echo "Verarbeite Dateien..."
for i in {1..20}; do
    printf "Fortschritt: %d/20 [%3d%%]\r" $i $((i*5))
    sleep 0.2
done
echo -e "Fortschritt: 20/20 [100%] - Abgeschlossen\n"

Ein grafischer Fortschrittsbalken kann mit ASCII-Zeichen erstellt werden:

#!/bin/bash

# Funktion für Fortschrittsbalken
progress_bar() {
    local width=50
    local percent=$1
    local completed=$((width * percent / 100))
    local remaining=$((width - completed))
    
    # Fortschrittsbalken erstellen
    printf "["
    printf "%${completed}s" | tr ' ' '#'
    printf "%${remaining}s" | tr ' ' ' '
    printf "] %3d%%\r" $percent
}

echo "Starte Übertragung..."
for i in {0..100..5}; do
    progress_bar $i
    sleep 0.1
done
echo -e "\nÜbertragung abgeschlossen."

9.6.4.2 Mehrzeilige Ausgaben mit Positionierung

Mit speziellen ANSI-Escape-Sequenzen können komplexere Anzeigen erstellt werden, bei denen mehrere Zeilen gleichzeitig aktualisiert werden:

#!/bin/bash

# Cursor-Steuerungssequenzen
cursor_up() {
    echo -en "\033[${1}A"
}

cursor_down() {
    echo -en "\033[${1}B"
}

cursor_save() {
    echo -en "\033[s"
}

cursor_restore() {
    echo -en "\033[u"
}

clear_line() {
    echo -en "\033[2K"
}

# Mehrere Status gleichzeitig anzeigen
echo "CPU-Auslastung: Wird geladen..."
echo "Speichernutzung: Wird geladen..."
echo "Festplattenaktivität: Wird geladen..."
echo "Netzwerkaktivität: Wird geladen..."
echo ""

for i in {1..10}; do
    # Cursor 5 Zeilen nach oben bewegen
    cursor_up 5
    
    # Zeilen einzeln aktualisieren
    clear_line
    echo "CPU-Auslastung: $((RANDOM % 100))%"
    clear_line
    echo "Speichernutzung: $((RANDOM % 8 + 2))GB von 16GB"
    clear_line
    echo "Festplattenaktivität: $((RANDOM % 50))MB/s"
    clear_line
    echo "Netzwerkaktivität: $((RANDOM % 100))Mbps"
    clear_line
    echo "Aktualisierung in $((10-i)) Sekunden..."
    
    sleep 1
done

echo "Überwachung beendet."

9.6.4.3 Boxen und Rahmen

Ausgaben können in dekorative Boxen oder Rahmen eingebettet werden:

#!/bin/bash

# Funktion zum Erstellen einer Box mit Titel
draw_box() {
    local title="$1"
    shift
    local lines=("$@")
    
    # Breite der Box berechnen
    local width=${#title}
    for line in "${lines[@]}"; do
        if [ ${#line} -gt $width ]; then
            width=${#line}
        fi
    done
    
    # Box-Rahmen zeichnen
    local width_plus_4=$((width + 4))
    local top_border=$(printf '%*s' $width_plus_4 | tr ' ' '═')
    local bottom_border=$(printf '%*s' $width_plus_4 | tr ' ' '═')
    
    echo "╔${top_border}╗"
    printf "║ %-${width}s ║\n" "$title"
    echo "╠$(printf '%*s' $width_plus_4 | tr ' ' '═')╣"
    
    for line in "${lines[@]}"; do
        printf "║ %-${width}s ║\n" "$line"
    done
    
    echo "╚${bottom_border}╝"
}

# Beispiel für die Verwendung
draw_box "Systemstatus" \
    "Hostname: $(hostname)" \
    "Datum: $(date +"%Y-%m-%d %H:%M:%S")" \
    "Uptime: $(uptime -p)" \
    "Benutzer: $(whoami)" \
    "Freier Speicher: $(free -h | awk '/^Mem:/ {print $4}')"

9.6.4.4 Mehrspaltige Ausgaben

Für komplexere Layouts können mehrere Spalten nebeneinander dargestellt werden:

#!/bin/bash

# Funktion zum Anzeigen von Ausgaben in zwei Spalten
two_columns() {
    local col1=("$1")
    shift
    local col1_data=("$@")
    shift $#
    local col2=("$1")
    shift
    local col2_data=("$@")
    
    # Spaltenbreiten berechnen
    local col1_width=0
    local col2_width=0
    
    for item in "${col1_data[@]}" "$col1"; do
        if [ ${#item} -gt $col1_width ]; then
            col1_width=${#item}
        fi
    done
    
    for item in "${col2_data[@]}" "$col2"; do
        if [ ${#item} -gt $col2_width ]; then
            col2_width=${#item}
        fi
    done
    
    # Padding hinzufügen
    col1_width=$((col1_width + 2))
    
    # Header ausgeben
    printf "%-${col1_width}s | %s\n" "$col1" "$col2"
    printf "%-${col1_width}s | %s\n" "$(printf '%*s' ${#col1} | tr ' ' '-')" "$(printf '%*s' ${#col2} | tr ' ' '-')"
    
    # Daten ausgeben (so viele Zeilen wie die längere Spalte hat)
    local rows=$(( ${#col1_data[@]} > ${#col2_data[@]} ? ${#col1_data[@]} : ${#col2_data[@]} ))
    
    for i in $(seq 0 $((rows-1))); do
        local val1="${col1_data[$i]:-}"
        local val2="${col2_data[$i]:-}"
        printf "%-${col1_width}s | %s\n" "$val1" "$val2"
    done
}

# Beispiel für die Verwendung
system_info=(
    "Hostname: $(hostname)"
    "Betriebssystem: $(uname -s)"
    "Kernel-Version: $(uname -r)"
    "Architektur: $(uname -m)"
)

user_info=(
    "Benutzer: $(whoami)"
    "Home-Verzeichnis: $HOME"
    "Shell: $SHELL"
    "Letzte Anmeldung: $(lastlog -u $(whoami) | tail -1 | awk '{print $4, $5, $6, $7, $8}')"
    "Gruppen: $(groups)"
)

two_columns "Systeminformationen" "${system_info[@]}" "Benutzerinformationen" "${user_info[@]}"

9.6.5 Strukturierte Berichte und Dokumente

9.6.5.1 Erstellung von Markdown-Berichten

Markdown ist ein leichtgewichtiges Auszeichnungsformat, das auch als Klartext gut lesbar ist:

#!/bin/bash

# Funktion zum Erstellen eines Markdown-Berichts
generate_markdown_report() {
    local title="$1"
    local outfile="$2"
    
    # Erstellen des Berichts in Markdown-Format
    {
        echo "# $title"
        echo ""
        echo "Erstellt am: $(date)"
        echo "Von: $(whoami) auf $(hostname)"
        echo ""
        
        echo "## Systemübersicht"
        echo ""
        echo "- **Betriebssystem:** $(uname -s) $(uname -r)"
        echo "- **Kernel-Version:** $(uname -v)"
        echo "- **Hostname:** $(hostname)"
        echo "- **Uptime:** $(uptime -p)"
        echo ""
        
        echo "## Ressourcennutzung"
        echo ""
        echo "### CPU-Auslastung"
        echo ""
        echo '```'
        top -b -n 1 | head -10
        echo '```'
        echo ""
        
        echo "### Speichernutzung"
        echo ""
        echo '```'
        free -h
        echo '```'
        echo ""
        
        echo "### Festplattennutzung"
        echo ""
        echo '```'
        df -h
        echo '```'
        echo ""
        
        echo "## Netzwerkstatus"
        echo ""
        echo "### Netzwerkschnittstellen"
        echo ""
        echo '```'
        ip addr | grep -E "^[0-9]+:|inet "
        echo '```'
        echo ""
        
        echo "### Offene Ports"
        echo ""
        echo '```'
        ss -tuln
        echo '```'
        echo ""
        
        echo "## Protokolle"
        echo ""
        echo "### Letzte Systemereignisse"
        echo ""
        echo '```'
        journalctl -n 10 --no-pager 2>/dev/null || tail -10 /var/log/syslog 2>/dev/null || tail -10 /var/log/messages 2>/dev/null
        echo '```'
    } > "$outfile"
    
    echo "Bericht wurde erstellt: $outfile"
}

# Bericht generieren
generate_markdown_report "Systemstatusbericht" "system_report_$(date +%Y%m%d).md"

9.7 Performance-Überlegungen bei der Textverarbeitung

Bei der Verarbeitung von Textdaten in Shell-Skripten können schnell Performance-Probleme auftreten, insbesondere wenn große Datenmengen verarbeitet werden müssen. In diesem Abschnitt betrachten wir verschiedene Techniken und Best Practices, um die Effizienz von Textverarbeitungsoperationen zu verbessern und Ressourcen zu schonen.

9.7.1 Verstehen der Performance-Engpässe

Bei der Textverarbeitung in Shell-Skripten können verschiedene Engpässe auftreten:

  1. CPU-Auslastung: Komplexe Musterabgleiche und Ersetzungen, insbesondere mit regulären Ausdrücken, können CPU-intensiv sein.
  2. I/O-Operationen: Das wiederholte Lesen und Schreiben von Dateien kann langsam sein, besonders bei mechanischen Festplatten.
  3. Speicherverbrauch: Das Laden großer Dateien in den Speicher kann zu Speichermangel führen.
  4. Prozessaufwand: Jeder Aufruf eines externen Befehls erzeugt einen neuen Prozess und verursacht Overhead.

9.7.2 Effiziente Datenverarbeitung mit Stream-Verarbeitung

9.7.2.1 Vermeiden von mehrfachen Dateidurchläufen

Eine häufige Ineffizienz ist, dieselbe Datei mehrmals zu durchlaufen:

# Ineffizient: Mehrere Durchläufe durch die Datei
total_lines=$(wc -l < large_file.txt)
error_count=$(grep "ERROR" large_file.txt | wc -l)
warning_count=$(grep "WARNING" large_file.txt | wc -l)

echo "Gesamtzeilen: $total_lines"
echo "Fehler: $error_count"
echo "Warnungen: $warning_count"

Eine effizientere Lösung verwendet einen einzigen Durchlauf:

# Effizient: Ein Durchlauf durch die Datei
{
    total_lines=0
    error_count=0
    warning_count=0
    
    while IFS= read -r line; do
        ((total_lines++))
        if [[ "$line" == *ERROR* ]]; then
            ((error_count++))
        elif [[ "$line" == *WARNING* ]]; then
            ((warning_count++))
        fi
    done
    
    echo "Gesamtzeilen: $total_lines"
    echo "Fehler: $error_count"
    echo "Warnungen: $warning_count"
} < large_file.txt

Noch besser wäre die Verwendung spezialisierter Tools wie awk:

# Sehr effizient: Verwendung von awk für einen einzigen Durchlauf
awk '
    BEGIN { total=0; errors=0; warnings=0 }
    { 
        total++
        if ($0 ~ /ERROR/) errors++
        else if ($0 ~ /WARNING/) warnings++
    }
    END {
        print "Gesamtzeilen:", total
        print "Fehler:", errors
        print "Warnungen:", warnings
    }
' large_file.txt

9.7.2.2 Puffern von Ein- und Ausgabeoperationen

Der Befehl stdbuf kann verwendet werden, um das Pufferverhalten von Befehlen zu steuern:

# Deaktivieren des Ausgabepuffers für eine flüssigere Ausgabe in Echtzeit
tail -f log.txt | stdbuf -o0 grep "ERROR"

# Reduzieren des Eingabepuffers für eine schnellere Verarbeitung großer Dateien
stdbuf -i1k -o1k sort large_file.txt > sorted_file.txt

9.7.3 Reduzieren des Prozessoverheads

9.7.3.1 Minimieren von externen Befehlsaufrufen

Jeder externe Befehlsaufruf erzeugt einen neuen Prozess, was zu Overhead führt:

# Ineffizient: Viele Prozesse für einfache Operationen
for file in *.txt; do
    lines=$(wc -l < "$file")
    echo "$file: $lines Zeilen"
done

Besser ist die Verwendung eingebauter Bash-Funktionen oder die Bündelung von Operationen:

# Effizienter: Ein Prozess für alle Dateien
wc -l *.txt | awk '{
    if (NR < NF) {
        printf "%s: %d Zeilen\n", $2, $1
    }
}'

9.7.3.2 Verwendung von integrierten Bash-Funktionen anstelle externer Befehle

Die Bash bietet viele integrierte Funktionen, die externe Befehle ersetzen können:

# Ineffizient: Externe Befehle
filename=$(basename "$path")
directory=$(dirname "$path")

# Effizienter: Bash-Parameterexpansion
filename=${path##*/}
directory=${path%/*}

Weitere Beispiele für Bash-interne Funktionen:

# Externe Befehle vermeiden
length=${#string}           # statt $(echo -n "$string" | wc -c)
upper=${string^^}           # statt $(echo "$string" | tr '[:lower:]' '[:upper:]')
lower=${string,,}           # statt $(echo "$string" | tr '[:upper:]' '[:lower:]')
substring=${string:1:5}     # statt $(echo "$string" | cut -c2-6)
replacement=${string/old/new} # statt $(echo "$string" | sed 's/old/new/')

9.7.4 Speichereffiziente Verarbeitung großer Dateien

9.7.4.1 Zeilenweise Verarbeitung anstatt Laden der gesamten Datei

Bei großen Dateien sollte vermieden werden, den gesamten Inhalt in den Speicher zu laden:

# Problematisch bei sehr großen Dateien: Gesamte Datei in den Speicher laden
content=$(<large_file.txt)

Stattdessen sollte zeilenweise verarbeitet werden:

# Speichereffizient: Zeilenweise Verarbeitung
while IFS= read -r line; do
    # Verarbeitung der einzelnen Zeile
    process_line "$line"
done < large_file.txt

9.7.4.2 Verwendung von temporären Dateien und Pipes

Für komplexere Verarbeitungsschritte können temporäre Dateien oder Pipes genutzt werden:

# Temporäre Datei für Zwischenergebnisse
temp_file=$(mktemp)
trap 'rm -f "$temp_file"' EXIT

grep "^ERROR" large_file.txt > "$temp_file"
sort -u "$temp_file" > sorted_errors.txt

Oder effizienter mit Pipes:

# Pipes für direkte Datenübergabe ohne temporäre Dateien
grep "^ERROR" large_file.txt | sort -u > sorted_errors.txt

9.7.5 Optimieren von regulären Ausdrücken

9.7.5.1 Verwendung von spezifischen Mustern

Allgemeine reguläre Ausdrücke können langsam sein. Je spezifischer das Muster, desto schneller die Ausführung:

# Langsam: Unspezifisches Muster mit mehreren Alternativen
grep -E "(error|failure|failed|crash|exception)" large_log.txt

# Schneller: Vorfilterung mit einem einfachen String, dann genauere Prüfung
grep -i "err" large_log.txt | grep -E "(error|failure|failed|crash|exception)"

9.7.5.2 Vermeidung von Backtracking

Komplexe reguläre Ausdrücke mit vielen Quantifizierern (*, +, ?) können zu exponentieller Laufzeit durch Backtracking führen:

# Potenziell langsam wegen Backtracking bei komplexen Ausdrücken
grep -E "A(B|C|D)*E" large_file.txt

Optimieren durch Vermeidung von unnötigem Quantifizieren und Alternativen:

# Schneller durch Einschränkung des Musters
grep -E "A[BCD]*E" large_file.txt

9.7.5.3 Verwenden von egrep/grep -E für komplexe Muster

Für komplexe reguläre Ausdrücke ist grep -E (oder egrep) oft effizienter als normales grep:

# Weniger effizient (Basic Regular Expressions)
grep "1\{2,4\}" file.txt

# Effizienter (Extended Regular Expressions)
grep -E "1{2,4}" file.txt

9.7.6 Parallelisierung von Operationen

9.7.6.1 Verwendung von GNU Parallel

Für die parallele Verarbeitung mehrerer Dateien kann GNU Parallel verwendet werden:

# Installieren von GNU Parallel
# apt-get install parallel  # Debian/Ubuntu
# yum install parallel      # CentOS/RHEL

# Parallele Verarbeitung von Dateien
find logs/ -name "*.log" | parallel 'grep "ERROR" {} > {}.errors'

# Parallele Ausführung einer Funktion für jeden Eingabewert
process_file() {
    echo "Verarbeite $1"
    grep "ERROR" "$1" | wc -l
}
export -f process_file

find logs/ -name "*.log" | parallel process_file

9.7.6.2 Ausnutzen mehrerer CPU-Kerne mit xargs

xargs mit der -P-Option kann ebenfalls für parallele Verarbeitung genutzt werden:

# Parallele Verarbeitung mit xargs (4 Prozesse gleichzeitig)
find logs/ -name "*.log" | xargs -P 4 -I {} grep "ERROR" {} -l

9.7.7 Optimierung häufiger Textverarbeitungsaufgaben

9.7.7.1 Schnellere Sortierung und Datendeduplizierung

Das Sortieren großer Dateien und das Entfernen von Duplikaten kann optimiert werden:

# Optimierung der Sortierleistung durch Festlegen des Puffergrößen
sort -S 1G large_file.txt > sorted_file.txt

# Parallelisierung der Sortierung mit mehreren Threads
sort --parallel=4 -S 1G large_file.txt > sorted_file.txt

# Effiziente Deduplizierung einer bereits sortierten Datei
sort -u large_file.txt > unique_lines.txt      # Sortieren und Deduplizieren
sort large_file.txt | uniq > unique_lines.txt  # Alternativ (bei bereits sortierter Datei)

9.7.7.2 Schnelleres Suchen und Ersetzen

Für schnelles Suchen und Ersetzen in großen Dateien:

# sed kann langsam sein bei großen Dateien und komplexen Ersetzungen
sed 's/pattern/replacement/g' large_file.txt > new_file.txt

# Alternativen für bessere Performance:
# 1. awk (oft schneller als sed für komplexe Muster)
awk '{gsub(/pattern/, "replacement"); print}' large_file.txt > new_file.txt

# 2. tr (sehr schnell für einfache Zeichenersetzungen)
tr 'a-z' 'A-Z' < large_file.txt > uppercase_file.txt

9.7.7.3 Zeilen zählen, filtern und extrahieren

Optimierung häufiger Aufgaben:

# Ineffizient: Zählen von Zeilen mit wc nach grep
grep "pattern" large_file.txt | wc -l

# Effizienter: Direktes Zählen mit grep
grep -c "pattern" large_file.txt

# Noch effizienter für einfache Zählung: Verwendung von awk in einem Durchlauf
awk '/pattern/ {count++} END {print count}' large_file.txt

9.7.8 Benchmarking und Profiling

9.7.8.1 Zeitmessung für Skripte und Befehle

Um die Performance zu messen und zu optimieren, ist Timing wichtig:

# Einfache Zeitmessung
time my_script.sh

# Detailliertere Timing-Informationen
/usr/bin/time -v my_script.sh

9.7.8.2 Schrittweises Benchmarking

Für komplexere Skripte kann der Timing-Overhead für einzelne Teile gemessen werden:

#!/bin/bash

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

# Teil 1
echo "Teil 1 startet..."
sort large_file.txt > sorted_file.txt
part1_end=$(date +%s.%N)
part1_duration=$(echo "$part1_end - $start_time" | bc)
echo "Teil 1 beendet. Dauer: $part1_duration Sekunden"

# Teil 2
echo "Teil 2 startet..."
uniq sorted_file.txt > unique_file.txt
part2_end=$(date +%s.%N)
part2_duration=$(echo "$part2_end - $part1_end" | bc)
echo "Teil 2 beendet. Dauer: $part2_duration Sekunden"

# Gesamtzeit
total_duration=$(echo "$part2_end - $start_time" | bc)
echo "Gesamtdauer: $total_duration Sekunden"

9.7.9 Best Practices und Beispiele

9.7.9.1 Optimierung eines Logfile-Analyse-Skripts

Hier ein Beispiel für die Optimierung eines Skripts zur Analyse von Logdateien:

#!/bin/bash
# Optimierte Logfile-Analyse

if [ $# -ne 1 ]; then
    echo "Verwendung: $0 <logfile>"
    exit 1
fi

logfile="$1"

if [ ! -f "$logfile" ]; then
    echo "Fehler: Datei '$logfile' nicht gefunden."
    exit 2
fi

echo "Analysiere Logfile: $logfile"
echo "------------------------"

# Einmaliges Lesen der Datei und Zählen verschiedener Muster
# Vermeidet multiple Durchläufe
echo "Statistische Auswertung wird erstellt..."
awk '
    BEGIN {
        errors = 0;
        warnings = 0;
        infos = 0;
        total = 0;
    }
    /ERROR/ { 
        errors++; 
        error_lines[errors] = $0;  # Speichern für spätere Verwendung
    }
    /WARNING/ { warnings++; }
    /INFO/ { infos++; }
    { total++; }
    END {
        print "Gesamtzeilen: " total;
        print "Fehler: " errors " (" (errors/total*100) "%)";
        print "Warnungen: " warnings " (" (warnings/total*100) "%)";
        print "Infos: " infos " (" (infos/total*100) "%)";
        
        print "\nTop 5 häufigste Fehlermeldungen:";
        for (i=1; i<=errors && i<=5; i++) {
            print " - " error_lines[i];
        }
    }
' "$logfile"

# Zeitmuster-Analyse mit einem einzigen sed/awk-Aufruf
echo -e "\nZeitliche Verteilung der Fehler:"
grep "ERROR" "$logfile" | awk '
    # Extrahieren der Stundeninformation (Format: [YYYY-MM-DD HH:MM:SS])
    {
        if (match($0, /[0-9]{4}-[0-9]{2}-[0-9]{2} ([0-9]{2}):[0-9]{2}:[0-9]{2}/, arr)) {
            hour = arr[1]
            counts[hour]++
        }
    }
    END {
        for (hour=0; hour<24; hour++) {
            h = sprintf("%02d", hour)
            count = counts[h] ? counts[h] : 0
            printf "%02d:00-%02d:59: %5d %s\n", hour, hour, count, replicate("#", int(count/5))
        }
    }
    
    function replicate(str, count) {
        result = ""
        for (i=0; i<count; i++) {
            result = result str
        }
        return result
    }
'

echo -e "\nAnalyse abgeschlossen."

9.7.9.2 Optimierung eines Textverarbeitungsskripts für große Dateien

Ein optimiertes Skript zur Verarbeitung sehr großer Textdateien:

#!/bin/bash
# Hochoptimiertes Skript für die Verarbeitung großer Textdateien

input_file="$1"
output_file="$2"
search_pattern="$3"
replace_pattern="$4"
temp_dir=$(mktemp -d)

# Aufräumen bei Beendigung
trap 'rm -rf "$temp_dir"; exit' EXIT INT TERM

# Parameter prüfen
if [ $# -ne 4 ]; then
    echo "Verwendung: $0 <eingabedatei> <ausgabedatei> <suchmuster> <ersetzungsmuster>"
    exit 1
fi

if [ ! -f "$input_file" ]; then
    echo "Fehler: Eingabedatei nicht gefunden."
    exit 2
fi

# Dateigröße prüfen und Strategie entsprechend wählen
file_size=$(stat -c %s "$input_file")
echo "Dateigröße: $file_size Bytes"

# Für sehr große Dateien: Aufteilung und parallele Verarbeitung
if [ $file_size -gt 1073741824 ]; then  # > 1GB
    echo "Große Datei erkannt, verwende parallele Verarbeitung..."
    
    # Datei in Teilstücke aufteilen
    split_size=$((file_size / $(nproc) + 1))
    echo "Teile Datei in Stücke von ca. $split_size Bytes..."
    split -b "$split_size" "$input_file" "$temp_dir/part_"
    
    # Parallele Verarbeitung der Teile
    find "$temp_dir" -name "part_*" | parallel "sed 's/$search_pattern/$replace_pattern/g' {} > {}.processed"
    
    # Teile wieder zusammenfügen
    find "$temp_dir" -name "part_*.processed" | sort | xargs cat > "$output_file"
    
# Für mittelgroße Dateien: Stream-Verarbeitung
else
    echo "Verarbeite Datei in einem Durchlauf..."
    LC_ALL=C sed 's/'"$search_pattern"'/'"$replace_pattern"'/g' "$input_file" > "$output_file"
fi

echo "Verarbeitung abgeschlossen. Ergebnis in $output_file"

# Statistik ausgeben
original_lines=$(wc -l < "$input_file")
processed_lines=$(wc -l < "$output_file")
changes=$(grep -c "$replace_pattern" "$output_file")

echo -e "\nStatistik:"
echo "Original Zeilen: $original_lines"
echo "Verarbeitete Zeilen: $processed_lines"
echo "Vorgenommene Änderungen: $changes"