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.
Bevor wir uns mit den verschiedenen Kontexten befassen, sollten wir die grundlegenden Elemente regulärer Ausdrücke verstehen:
Reguläre Ausdrücke bestehen aus:
a, b, 1,
2)., *, +, ?,
^, $, \, |,
(, ), [, ],
{, })| 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 |
In Unix-/Linux-Systemen gibt es drei hauptsächliche “Dialekte” oder Typen von regulären Ausdrücken:
Die Hauptunterschiede liegen in der Syntax und darin, welche
Metazeichen direkt verwendet werden können und welche mit einem
Backslash (\) “escaped” werden müssen.
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.txtHier 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 wirdsed 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.txtBeispiele:
# 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 Befehlawk ist eine komplette Programmiersprache für
Textverarbeitung und verwendet standardmäßig Extended Regular
Expressions:
awk '/pattern/ { aktion }' datei.txtBeispiel:
# 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.txtInnerhalb 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"
fiBeispiel:
#!/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"
fiEin 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#!/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#!/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"#!/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"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:
| 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} |
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 interpretiertAnfü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"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]}"
fiDiese 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.txtStandardmäß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 einzelnDie 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.
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”.
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| 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/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"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.
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| 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) |
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# 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.txtFü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.txtawk 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.
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.txtEin 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 }
awk teilt die Eingabe standardmäßig in Datensätze (normalerweise Zeilen) und Felder (normalerweise durch Leerzeichen oder Tabulatoren getrennte Spalten) auf:
$0: Der gesamte aktuelle Datensatz$1, $2, …: Das erste, zweite, … Feld des
aktuellen DatensatzesNF: Die Anzahl der Felder im aktuellen DatensatzNR: Die Nummer des aktuellen Datensatzes| 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 |
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# 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#!/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.logDie 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.txtZum 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."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.
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.txtWichtige 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 -10uniq 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 -uWichtige 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 |
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/passwdWichtige 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 |
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.csvWichtige Optionen für paste:
| Option | Beschreibung |
|---|---|
-d LISTE |
Trennzeichen festlegen (zyklisch verwendet) |
-s |
Serial mode - fügt alle Zeilen einer Datei zusammen |
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.txtWichtige 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 |
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 |
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 |
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.txtfold 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.txtwc (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.txtWichtige 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 |
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.txtdiff 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.txtWichtige 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 |
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.patchDie wahre Stärke dieser Werkzeuge entfaltet sich bei der Integration in Shell-Skripte. Hier einige praktische Beispiele:
#!/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#!/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."#!/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 0Die 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.
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 weiterleitenUm 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 erzwingenDie 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.
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"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.
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"#!/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"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.txtHere-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
EOLDie Anführungszeichen um das Begrenzungswort 'EOL'
verhindern die Shell-Expansion innerhalb des Here-Documents, sodass
Zeichen wie $ und Backticks wörtlich übernommen werden.
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)
EOLFü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.txtEin 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"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-AnweisungUm 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"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"
fiMit 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")"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.
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 "-------------------"
doneObwohl 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))}'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.txtBei 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"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#!/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#!/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."#!/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 0In 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.
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:
Neben CSV gibt es weitere strukturierte Textformate:
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"
doneFü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"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.csvHä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 ""
doneEine 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."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."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."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."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."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."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
fiOft 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."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"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."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"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.
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)"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"
doneDie 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" ""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]}"
doneBei 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]}"
doneFü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[@]}"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.
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."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 ...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."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."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}')"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[@]}"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"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.
Bei der Textverarbeitung in Shell-Skripten können verschiedene Engpässe auftreten:
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.txtNoch 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.txtDer 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.txtJeder 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"
doneBesser 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
}
}'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/')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.txtFü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.txtOder effizienter mit Pipes:
# Pipes für direkte Datenübergabe ohne temporäre Dateien
grep "^ERROR" large_file.txt | sort -u > sorted_errors.txtAllgemeine 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)"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.txtOptimieren durch Vermeidung von unnötigem Quantifizieren und Alternativen:
# Schneller durch Einschränkung des Musters
grep -E "A[BCD]*E" large_file.txtFü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.txtFü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_filexargs 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" {} -lDas 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)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.txtOptimierung 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.txtUm 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.shFü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"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."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"