Bevor Shell-Skripte optimiert werden können, müssen zunächst die Leistungsengpässe (Bottlenecks) identifiziert werden. In diesem Abschnitt werden verschiedene Techniken und Werkzeuge vorgestellt, mit denen Performance-Probleme in Shell-Skripten erkannt und analysiert werden können.
Shell-Skripte werden häufig für Automatisierungsaufgaben, Systemadministration und Datenverarbeitung eingesetzt. Obwohl sie nicht für rechenintensive Aufgaben konzipiert sind, kann die Performance in bestimmten Szenarien entscheidend sein:
Eine schlechte Performance kann zu längeren Wartezeiten, höherer Systemlast und in extremen Fällen sogar zum Ausfall von Diensten führen.
Die einfachste Methode zur Identifizierung von Leistungsengpässen ist die Zeitmessung des gesamten Skripts oder einzelner Abschnitte.
timeDer time-Befehl misst die Ausführungszeit eines
Programms oder Skripts:
$ time ./mein_skript.sh
real 0m3.456s
user 0m2.123s
sys 0m0.876sDie Ausgabe zeigt: - real: Die tatsächlich verstrichene
Zeit (Wanduhr-Zeit) - user: Die Zeit, die der Prozess im
Benutzer-Modus verbracht hat - sys: Die Zeit, die der
Prozess im Kernel-Modus verbracht hat
Um einzelne Abschnitte innerhalb eines Skripts zu messen, können Zeitstempel vor und nach dem zu messenden Code gesetzt werden:
#!/bin/bash
# Funktion zur Zeitmessung
measure_time() {
local start_time=$1
local end_time=$(date +%s.%N)
local elapsed=$(echo "$end_time - $start_time" | bc)
echo "Ausführungszeit: $elapsed Sekunden"
}
# Start der Messung
start_time=$(date +%s.%N)
# Zu messender Code-Block
for i in {1..1000}; do
echo $i > /dev/null
done
# Ende der Messung und Ausgabe
measure_time $start_timeFür präzisere Messungen in Bash 4+ kann auch die eingebaute Variable
SECONDS verwendet werden:
#!/bin/bash
# Start der Messung
SECONDS=0
# Zu messender Code-Block
for i in {1..1000}; do
echo $i > /dev/null
done
# Ende der Messung und Ausgabe
duration=$SECONDS
echo "Ausführungszeit: $duration Sekunden"Profiling gibt einen detaillierteren Einblick in die Performance-Engpässe eines Skripts.
PS4 und set -xDie Trace-Funktionalität der Shell (set -x) in
Kombination mit einer angepassten PS4-Variable kann für
einfaches Profiling genutzt werden:
#!/bin/bash
# Angepasste PS4-Variable für Profiling
export PS4='+ $(date "+%s.%N") ${BASH_SOURCE}:${LINENO}: '
# Trace aktivieren
set -x
# Zu analysierender Code
for i in {1..100}; do
echo $i > /dev/null
done
grep "pattern" /etc/passwd > /dev/null
# Weitere Befehle...
# Trace deaktivieren
set +xDie Ausgabe dieses Skripts enthält Zeitstempel für jede ausgeführte Zeile, was die Identifizierung langsamer Abschnitte ermöglicht.
Für selektiveres Profiling können Bash-Traps verwendet werden:
#!/bin/bash
# Profiling-Funktion
profile_cmd() {
local start_time=$(date +%s.%N)
trap 'local end_time=$(date +%s.%N); \
local duration=$(echo "$end_time - $start_time" | bc); \
echo "Befehl \"$BASH_COMMAND\" dauerte $duration Sekunden" >&2' DEBUG
}
# Profiling für einen bestimmten Abschnitt aktivieren
profile_cmd
# Zu messender Code-Block
for i in {1..1000}; do
echo $i > /dev/null
done
# Profiling deaktivieren
trap - DEBUGNeben den eingebauten Shell-Funktionen gibt es spezialisierte Tools für tiefergehende Analysen.
shellcheckshellcheck ist primär ein Werkzeug zur statischen
Analyse von Shell-Skripten, kann aber auch Performance-Probleme
identifizieren:
$ shellcheck mein_skript.shEs erkennt häufige Fehler wie: - Unnötige Subshell-Aufrufe - Ineffiziente Schleifenkonstrukte - Problematische Dateizugriffsmuster
perfFür tiefergehende Analysen, insbesondere bei CPU-intensiven Skripten,
kann das Linux-perf-Tool verwendet werden:
$ perf record -g ./mein_skript.sh
$ perf reportDies zeigt, welche Funktionen und Systemaufrufe die meiste Zeit beanspruchen.
iotop und iostatWenn Skripte viele Dateioperationen durchführen, können
iotop und iostat helfen, I/O-Engpässe zu
identifizieren:
$ iotop -p $(pgrep -f "mein_skript.sh")oder
$ iostat -dx 1Nachdem die grundlegenden Messwerkzeuge vorgestellt wurden, konzentrieren wir uns auf die Identifizierung spezifischer Performance-Probleme in Shell-Skripten.
Externe Befehlsaufrufe: Jeder externe Befehlsaufruf erzeugt eine neue Prozess-Instanz, was ressourcenintensiv ist.
# Ineffizient: Viele externe Befehlsaufrufe in einer Schleife
for i in {1..1000}; do
echo "$i" >> output.txt # Jeder Aufruf erzeugt einen neuen Prozess
doneIneffiziente Schleifen: Besonders bei der Verarbeitung großer Datenmengen können Schleifen in Bash langsam sein.
Wiederholte Dateioperationen: Häufiges Öffnen und Schließen von Dateien verursacht I/O-Overhead.
Übermäßige Verwendung von Pipelines: Jede Pipeline erzeugt zusätzliche Prozesse.
Ineffiziente Textverarbeitung: Komplexe Textmanipulationen in Bash können ineffizient sein.
Um Leistungsengpässe systematisch zu identifizieren:
Isolieren Sie Abschnitte: Teilen Sie das Skript in logische Abschnitte und messen Sie jeden einzeln.
#!/bin/bash
echo "Messung Abschnitt 1:"
time {
# Abschnitt 1
for i in {1..1000}; do
: # Platzhalter für Code
done
}
echo "Messung Abschnitt 2:"
time {
# Abschnitt 2
grep "pattern" /var/log/syslog > /dev/null
}Analysieren Sie Ressourcenverbrauch: Überwachen Sie CPU-, Speicher- und I/O-Nutzung während der Ausführung.
$ top -b -n 1 -p $(pgrep -f "mein_skript.sh")Überprüfen Sie externe Abhängigkeiten: Langsame Netzwerkverbindungen oder externe Dienste können die Performance beeinträchtigen.
Betrachten wir ein Skript, das große Logdateien verarbeitet:
#!/bin/bash
# log_analyzer.sh - Analysiert Apache-Logdateien
output_file="report.txt"
echo "Log-Analyse gestartet: $(date)" > "$output_file"
# Anzahl der HTTP-Status-Codes zählen
echo "HTTP-Status-Verteilung:" >> "$output_file"
# Potenzieller Leistungsengpass: Mehrfache Dateiverarbeitung
for status_code in 200 301 302 304 403 404 500; do
count=$(grep -c " $status_code " access.log)
echo "Status $status_code: $count" >> "$output_file"
done
# Top 10 IP-Adressen finden
echo "Top 10 IP-Adressen:" >> "$output_file"
# Potenzieller Leistungsengpass: Komplexe Pipeline
grep -o "[0-9]\+\.[0-9]\+\.[0-9]\+\.[0-9]\+" access.log | sort | uniq -c | sort -nr | head -10 >> "$output_file"
echo "Log-Analyse beendet: $(date)" >> "$output_file"Um die Leistungsengpässe in diesem Skript zu identifizieren:
Messen Sie die Gesamtlaufzeit:
$ time ./log_analyzer.shInstrumentieren Sie das Skript zur Identifizierung langsamer Abschnitte:
#!/bin/bash
# log_analyzer.sh mit Zeitmessung
output_file="report.txt"
echo "Log-Analyse gestartet: $(date)" > "$output_file"
# Zeitmessung für HTTP-Status-Codes
echo "Messung der Status-Code-Analyse:"
time {
echo "HTTP-Status-Verteilung:" >> "$output_file"
for status_code in 200 301 302 304 403 404 500; do
count=$(grep -c " $status_code " access.log)
echo "Status $status_code: $count" >> "$output_file"
done
}
# Zeitmessung für IP-Analyse
echo "Messung der IP-Analyse:"
time {
echo "Top 10 IP-Adressen:" >> "$output_file"
grep -o "[0-9]\+\.[0-9]\+\.[0-9]\+\.[0-9]\+" access.log | sort | uniq -c | sort -nr | head -10 >> "$output_file"
}
echo "Log-Analyse beendet: $(date)" >> "$output_file"Analysieren Sie die I/O-Last während der Ausführung:
$ iostat -dx 1Betrachten wir ein Backup-Skript mit mehreren Operationen:
#!/bin/bash
# backup_script.sh - Sichert wichtige Verzeichnisse
# Konfiguration
source_dirs=("/home/user/documents" "/var/www" "/etc")
backup_dir="/backup/$(date +%Y-%m-%d)"
log_file="/var/log/backup.log"
# Backup-Verzeichnis erstellen
mkdir -p "$backup_dir"
# Sicherung durchführen
echo "Backup gestartet: $(date)" > "$log_file"
for dir in "${source_dirs[@]}"; do
echo "Sichere $dir..." >> "$log_file"
# Potenzieller Leistungsengpass: Einzelne Dateien werden komprimiert
find "$dir" -type f -name "*.txt" | while read -r file; do
gzip -c "$file" > "$backup_dir/$(basename "$file").gz"
done
# Potenzieller Leistungsengpass: Separate tar-Prozesse
tar -czf "$backup_dir/$(basename "$dir").tar.gz" "$dir"
done
# Alte Backups bereinigen (älter als 30 Tage)
find /backup -type d -mtime +30 -exec rm -rf {} \; 2>/dev/null || true
echo "Backup abgeschlossen: $(date)" >> "$log_file"Um die Leistungsengpässe in diesem Skript zu identifizieren:
Fügen Sie detaillierte Zeitmessungen hinzu:
#!/bin/bash
# backup_script.sh mit Zeitmessung
# ... [Konfiguration] ...
# Funktion zur Zeitmessung
measure_operation() {
local description="$1"
local start_time=$(date +%s)
shift
echo "[$description gestartet]"
# Ausführen des übergebenen Befehls
"$@"
local end_time=$(date +%s)
local duration=$((end_time - start_time))
echo "[$description abgeschlossen - Dauer: $duration Sekunden]"
}
# ... [Rest des Skripts mit measure_operation-Aufrufen] ...
for dir in "${source_dirs[@]}"; do
echo "Sichere $dir..." >> "$log_file"
measure_operation "Komprimierung einzelner Dateien in $dir" bash -c "
find \"$dir\" -type f -name \"*.txt\" | while read -r file; do
gzip -c \"\$file\" > \"$backup_dir/\$(basename \"\$file\").gz\"
done
"
measure_operation "Tar-Archivierung von $dir" \
tar -czf "$backup_dir/$(basename "$dir").tar.gz" "$dir"
done
measure_operation "Bereinigung alter Backups" \
find /backup -type d -mtime +30 -exec rm -rf {} \; 2>/dev/null || trueÜberwachen Sie die Systemressourcen während der Ausführung:
$ top -b -p $(pgrep -f "backup_script.sh")Analysieren Sie die I/O-Aktivität, da Backup-Operationen oft I/O-intensiv sind:
$ iotop -aoP | grep -E "backup_script|tar|gzip|find"Die Wahl geeigneter Algorithmen und Datenstrukturen hat einen erheblichen Einfluss auf die Performance von Shell-Skripten. Obwohl Shell-Skripting nicht primär für komplexe algorithmische Aufgaben konzipiert ist, können effiziente Ansätze die Ausführungszeit deutlich verbessern. In diesem Abschnitt werden wir untersuchen, wie man durch die Auswahl optimaler Algorithmen und Datenstrukturen die Leistung von Shell-Skripten steigern kann.
Die Bash und andere Shell-Umgebungen bieten einige grundlegende Datenstrukturen, deren effiziente Nutzung die Performance verbessern kann.
Arrays sind eine der wichtigsten Datenstrukturen in Bash und können in vielen Szenarien effizienter sein als alternative Ansätze:
#!/bin/bash
# Ineffizient: String mit Trennzeichen
files_string="file1.txt file2.txt file3.txt"
for file in $files_string; do
process_file "$file"
done
# Effizienter: Array verwenden
files_array=("file1.txt" "file2.txt" "file3.txt")
for file in "${files_array[@]}"; do
process_file "$file"
doneBesonders bei Dateinamen mit Leerzeichen oder Sonderzeichen bieten Arrays eine sicherere und effizientere Alternative.
Seit Bash 4 stehen assoziative Arrays (Dictionaries/Hashmaps) zur Verfügung, die Nachschlagevorgänge erheblich beschleunigen können:
#!/bin/bash
# Ineffizient: Lineare Suche in einer Liste
users=("alice" "bob" "charlie" "dave" "eve")
permissions=("read" "write" "read" "admin" "read")
get_permission() {
local user=$1
for i in "${!users[@]}"; do
if [[ "${users[$i]}" == "$user" ]]; then
echo "${permissions[$i]}"
return
fi
done
echo "none"
}
# Effizienter: Assoziatives Array für O(1) Zugriff
declare -A user_permissions
user_permissions["alice"]="read"
user_permissions["bob"]="write"
user_permissions["charlie"]="read"
user_permissions["dave"]="admin"
user_permissions["eve"]="read"
get_permission_fast() {
local user=$1
echo "${user_permissions[$user]:-none}"
}
# Vergleich der Ausführungszeit
time get_permission "eve"
time get_permission_fast "eve"Die Zeitkomplexität verbessert sich von O(n) bei einer linearen Suche zu O(1) bei Verwendung eines assoziativen Arrays.
Für wiederkehrende Berechnungen kann die Speicherung vorberechneter Ergebnisse die Performance erheblich verbessern:
#!/bin/bash
# Array für die Speicherung bereits berechneter Ergebnisse
declare -A fibonacci_cache
# Recursive Fibonacci with memoization
fibonacci() {
local n=$1
# Basisfall
if [[ $n -le 1 ]]; then
echo $n
return
fi
# Prüfen, ob das Ergebnis bereits berechnet wurde
if [[ -n "${fibonacci_cache[$n]}" ]]; then
echo "${fibonacci_cache[$n]}"
return
fi
# Berechnung durchführen
local a=$(fibonacci $((n-1)))
local b=$(fibonacci $((n-2)))
local result=$((a + b))
# Ergebnis zwischenspeichern
fibonacci_cache[$n]=$result
echo $result
}
# Beispielverwendung
time fibonacci 30Ohne Memoization hätte dieser rekursive Ansatz eine exponentielle Zeitkomplexität von O(2^n). Mit Memoization reduziert sich die Komplexität auf O(n).
Das Sortieren und Filtern großer Datenmengen kann zeitaufwändig sein. Die richtigen Tools und Ansätze können die Performance deutlich verbessern:
#!/bin/bash
# Ineffizient: Sortieren und dann Unique in separaten Schritten
sort large_file.txt > sorted.txt
uniq sorted.txt > unique.txt
rm sorted.txt
# Effizienter: Pipeline verwenden, um temporäre Dateien zu vermeiden
sort large_file.txt | uniq > unique.txt
# Noch effizienter: sort mit -u Option (sortiert und entfernt Duplikate)
sort -u large_file.txt > unique.txtBei großen Dateien können weitere Optimierungen helfen:
# Parallele Sortierung mit mehreren Threads (auf modernen Systemen)
sort --parallel=4 -u large_file.txt > unique.txt
# Bei sehr großen Dateien: temporäre Dateien im Arbeitsspeicher halten
sort -u --buffer-size=1G large_file.txt > unique.txtDie Textsuche ist eine häufige Aufgabe in Shell-Skripten. Hier einige Optimierungen:
#!/bin/bash
# Ineffizient: Mehrfacher Durchlauf durch die Datei
grep "pattern1" large_file.txt > matches1.txt
grep "pattern2" large_file.txt > matches2.txt
grep "pattern3" large_file.txt > matches3.txt
# Effizienter: Einmaliger Durchlauf mit mehreren Patterns
grep -e "pattern1" -e "pattern2" -e "pattern3" large_file.txt > all_matches.txt
# Alternative: awk für komplexere Muster
awk '/pattern1/ {print > "matches1.txt"}
/pattern2/ {print > "matches2.txt"}
/pattern3/ {print > "matches3.txt"}' large_file.txtFür besonders große Dateien kann der Einsatz von egrep
(oder grep -E) und optimierten regulären Ausdrücken die
Suchgeschwindigkeit verbessern:
# Effizienter: Optimierter regulärer Ausdruck
grep -E 'pattern1|pattern2|pattern3' large_file.txt > all_matches.txtWenn die Daten bereits sortiert sind, kann eine binäre Suche wesentlich effizienter sein als eine lineare Suche:
#!/bin/bash
# Binäre Suche in einer sortierten Liste
binary_search() {
local needle=$1
shift
local haystack=("$@")
local low=0
local high=$((${#haystack[@]} - 1))
while [[ $low -le $high ]]; do
local mid=$(( (low + high) / 2 ))
local mid_val=${haystack[$mid]}
if [[ "$mid_val" == "$needle" ]]; then
echo "Gefunden an Position $mid"
return 0
elif [[ "$mid_val" < "$needle" ]]; then
low=$((mid + 1))
else
high=$((mid - 1))
fi
done
echo "Nicht gefunden"
return 1
}
# Beispiel mit einer sortierten Liste
sorted_array=($(for i in {1..1000}; do echo $i; done))
time binary_search 750 "${sorted_array[@]}"
# Vergleich mit linearer Suche
linear_search() {
local needle=$1
shift
local haystack=("$@")
for i in "${!haystack[@]}"; do
if [[ "${haystack[$i]}" == "$needle" ]]; then
echo "Gefunden an Position $i"
return 0
fi
done
echo "Nicht gefunden"
return 1
}
time linear_search 750 "${sorted_array[@]}"Die Zeitkomplexität verbessert sich von O(n) bei linearer Suche zu O(log n) bei binärer Suche.
Die Erzeugung neuer Prozesse ist in der Shell relativ kostspielig und sollte optimiert werden.
Jede Verwendung von $(), Backticks oder Pipelines
erzeugt Subshells, die Performance-Kosten verursachen:
#!/bin/bash
# Ineffizient: Viele Subshell-Aufrufe
for i in {1..1000}; do
result=$(echo $i | grep -o "[0-9]")
# Weitere Verarbeitung...
done
# Effizienter: Bult-in-Funktionen nutzen
for i in {1..1000}; do
if [[ $i =~ [0-9] ]]; then
result=$BASH_REMATCH
# Weitere Verarbeitung...
fi
doneDie Verwendung interner Shell-Funktionen anstelle externer Befehle kann die Performance erheblich verbessern:
#!/bin/bash
# Ineffizient: Externe Befehle
file_size=$(stat -c "%s" "$file")
if [ "$file_size" -gt 1024 ]; then
echo "Datei ist größer als 1 KB"
fi
# Effizienter: Interne Funktionen verwenden (Bash 4+)
if [[ -f "$file" ]]; then
read -r file_size < <(stat -c "%s" "$file") # Nur ein externer Aufruf
if (( file_size > 1024 )); then
echo "Datei ist größer als 1 KB"
fi
fiBei der Verarbeitung großer Datensätze ist ein datensatzorientierter Ansatz oft effizienter als ein zeichenorientierter Ansatz.
Für große Dateien ist die Stream-Verarbeitung (Zeile für Zeile) effizienter als das Laden der gesamten Datei in den Speicher:
#!/bin/bash
# Ineffizient: Gesamte Datei in den Speicher laden
lines=$(cat large_file.txt)
for line in $lines; do
# Verarbeitung...
done
# Effizienter: Zeilenweise Verarbeitung
while IFS= read -r line; do
# Verarbeitung...
done < large_file.txtDie Vermeidung mehrfacher Durchläufe durch dieselbe Datei kann die Performance erheblich verbessern:
#!/bin/bash
# Ineffizient: Mehrere Durchläufe durch dieselbe Datei
total_lines=$(wc -l < data.txt)
echo "Anzahl der Zeilen: $total_lines"
matched_lines=$(grep "pattern" data.txt | wc -l)
echo "Zeilen mit Treffern: $matched_lines"
# Effizienter: Einmaliger Durchlauf mit awk
awk '
BEGIN { matches = 0; total = 0; }
{ total++; }
/pattern/ { matches++; }
END { print "Anzahl der Zeilen: " total; print "Zeilen mit Treffern: " matches; }
' data.txtFür wiederholt genutzte Datensätze kann eine Vorverarbeitung oder Indizierung die Performance verbessern.
#!/bin/bash
# Vorverarbeitung: Erstellen einer Lookup-Tabelle
create_lookup_table() {
local input_file=$1
local output_file=$2
# Index erstellen: ID als Schlüssel, Rest als Wert
awk -F, '{print $1 ":" $0}' "$input_file" > "$output_file"
# Sortieren für binäre Suche
sort -t: -k1,1 -o "$output_file" "$output_file"
}
# Beispiel
create_lookup_table "large_data.csv" "indexed_data.txt"
# Schnelles Nachschlagen mittels binärer Suche (mit grep)
lookup_record() {
local key=$1
local index_file=$2
# Binäre Suche mit grep
grep -m1 "^$key:" "$index_file" | cut -d: -f2-
}
# Nutzung
record=$(lookup_record "12345" "indexed_data.txt")
echo "$record"Für den schnellen Zugriff auf bestimmte Spalten in großen Datensätzen:
#!/bin/bash
# Vorverarbeitung: Extrahieren relevanter Spalten
extract_columns() {
local input_file=$1
local column_numbers=$2 # Kommagetrennte Liste von Spaltennummern
local output_prefix=$3
# Für jede angegebene Spalte
for col in $(echo "$column_numbers" | tr ',' ' '); do
cut -d, -f"$col" "$input_file" > "${output_prefix}_col${col}.txt"
done
}
# Beispiel
extract_columns "large_csv.csv" "1,3,5" "extracted"
# Schneller Zugriff auf eine bestimmte Spalte
column_data=$(cat "extracted_col3.txt")Ein typisches Szenario ist die Analyse großer Logdateien, um Muster zu erkennen und Statistiken zu erstellen:
#!/bin/bash
# log_analysis_optimized.sh - Optimierte Analyse von Webserver-Logs
LOG_FILE="access.log"
TEMP_DIR=$(mktemp -d)
trap 'rm -rf "$TEMP_DIR"' EXIT
# Effiziente Extraktion relevanter Daten in einem Durchlauf
echo "Extrahiere relevante Daten..."
awk '{
# IP-Adresse (1. Feld)
print $1 > "'"$TEMP_DIR"'/ips.txt";
# HTTP-Status (9. Feld)
print $9 > "'"$TEMP_DIR"'/status_codes.txt";
# URL (7. Feld)
print $7 > "'"$TEMP_DIR"'/urls.txt";
# Speichern der Zeilen mit Fehlern (5xx Status)
if ($9 ~ /^5[0-9][0-9]$/) {
print $0 > "'"$TEMP_DIR"'/errors.txt";
}
}' "$LOG_FILE"
# Parallele Verarbeitung der extrahierten Daten
echo "Analysiere Daten..."
# IP-Statistik
{
echo "Top 10 IP-Adressen:"
sort "$TEMP_DIR/ips.txt" | uniq -c | sort -nr | head -10
} > "ip_stats.txt" &
# Status-Code-Statistik
{
echo "Verteilung der HTTP-Status-Codes:"
sort "$TEMP_DIR/status_codes.txt" | uniq -c | sort -nr
} > "status_stats.txt" &
# URL-Statistik
{
echo "Top 10 URLs:"
sort "$TEMP_DIR/urls.txt" | uniq -c | sort -nr | head -10
} > "url_stats.txt" &
# Auf Abschluss der Hintergrundprozesse warten
wait
echo "Analyse abgeschlossen. Ergebnisse in:"
echo "- ip_stats.txt"
echo "- status_stats.txt"
echo "- url_stats.txt"
if [[ -f "$TEMP_DIR/errors.txt" ]]; then
cp "$TEMP_DIR/errors.txt" "error_log_entries.txt"
echo "- error_log_entries.txt"
fiDieses optimierte Skript: 1. Extrahiert in einem einzigen Durchlauf alle relevanten Daten 2. Verarbeitet diese Daten parallel in Hintergrundprozessen 3. Verwendet effiziente Sortier- und Zähloperationen 4. Vermeidet mehrfache Durchläufe durch die große Logdatei
Ein weiteres häufiges Szenario ist die Deduplizierung großer Datensätze:
#!/bin/bash
# data_deduplication.sh - Effiziente Deduplizierung großer Datensätze
INPUT_FILE="$1"
OUTPUT_FILE="$2"
CHUNK_SIZE=100000 # Anzahl der Zeilen pro Chunk
TEMP_DIR=$(mktemp -d)
trap 'rm -rf "$TEMP_DIR"' EXIT
if [[ $# -ne 2 ]]; then
echo "Verwendung: $0 <eingabedatei> <ausgabedatei>"
exit 1
fi
# Effizienz durch Chunking: Datei in handhabbare Teile zerlegen
echo "Teile Datei in Chunks..."
split -l "$CHUNK_SIZE" "$INPUT_FILE" "$TEMP_DIR/chunk_"
# Jedes Chunk parallel deduplizieren
echo "Dedupliziere Chunks..."
for chunk in "$TEMP_DIR"/chunk_*; do
sort -u "$chunk" > "${chunk}.sorted" &
# Begrenze die Anzahl paralleler Prozesse
if [[ $(jobs -r | wc -l) -ge $(nproc) ]]; then
wait -n
fi
done
# Auf Abschluss aller Hintergrundprozesse warten
wait
# Zusammenführen der sortierten Chunks mit effizientem Merge-Sort
echo "Führe deduplizierte Chunks zusammen..."
sort -m -u "$TEMP_DIR"/chunk_*.sorted > "$OUTPUT_FILE"
# Statistik ausgeben
original_lines=$(wc -l < "$INPUT_FILE")
dedup_lines=$(wc -l < "$OUTPUT_FILE")
removed=$((original_lines - dedup_lines))
percent=$((removed * 100 / original_lines))
echo "Deduplizierung abgeschlossen:"
echo "- Ursprüngliche Zeilen: $original_lines"
echo "- Deduplizierte Zeilen: $dedup_lines"
echo "- Entfernte Duplikate: $removed ($percent%)"Dieses Skript verwendet mehrere Optimierungen: 1. Chunking der großen Datei, um Speichereffizienz zu verbessern 2. Parallele Verarbeitung der Chunks 3. Effizientes Merge-Sort für die Zusammenführung 4. Vermeidung des Ladens der gesamten Datei in den Speicher
Die Wahl effizienter Algorithmen und Datenstrukturen kann die Performance von Shell-Skripten entscheidend verbessern:
Diese Optimierungstechniken können die Ausführungszeit von Shell-Skripten um Größenordnungen verbessern, besonders bei der Verarbeitung großer Datenmengen. Durch die Kombination effizienter Algorithmen und Datenstrukturen mit den im nächsten Abschnitt behandelten Techniken zur Reduzierung von Prozessaufrufen können Shell-Skripte auch für anspruchsvollere Aufgaben performant gestaltet werden.
Die Erzeugung neuer Prozesse und Subshells gehört zu den ressourcenintensivsten Operationen in Shell-Skripten und kann die Performance erheblich beeinträchtigen. In diesem Abschnitt werden Strategien vorgestellt, um unnötige Prozessaufrufe zu reduzieren und die Leistung von Shell-Skripten zu verbessern.
Wenn ein Shell-Skript einen externen Befehl aufruft, einen
Pipeline-Operator (|) verwendet oder eine
Befehlssubstitution ($() oder Backticks) einsetzt, wird ein
neuer Prozess oder eine Subshell erzeugt. Diese Operationen verursachen
Overhead:
Bei einzelnen Aufrufen fällt dieser Overhead kaum ins Gewicht, aber in Schleifen oder bei häufigen Aufrufen kann er die Performance deutlich beeinträchtigen.
Der erste Schritt zur Optimierung ist die Identifizierung unnötiger Prozessaufrufe:
#!/bin/bash
# Skript mit vielen Prozessaufrufen analysieren
PS4='+ $LINENO: ' bash -x mein_skript.sh 2> trace.log
# Anzahl der Prozessaufrufe im Log zählen
grep -c "+" trace.log
# Alternative: strace zur Analyse von Systemaufrufen
strace -c -f bash mein_skript.shBesonders achten sollte man auf: - Häufige Aufrufe externer Befehle in Schleifen - Wiederholte Befehlssubstitutionen für gleiche Ergebnisse - Komplexe Pipelines, besonders in Schleifen - Unnötige Prozessaufrufe für einfache Operationen
Eine der effektivsten Optimierungsstrategien ist die Verwendung interner Shell-Befehle anstelle externer Programme:
#!/bin/bash
# Ineffizient: Externe Programme für String-Operationen
filename="example.txt"
basename=$(echo "$filename" | sed 's/\..*$//') # Erzeugt 2 Prozesse
# Effizient: Bash-interne String-Manipulation
basename=${filename%.*} # Kein zusätzlicher Prozess| Externer Befehl | Shell-interne Alternative | Beschreibung |
|---|---|---|
echo |
printf oder echo (Builtin) |
Ausgabe von Text |
basename |
${filename##*/} |
Dateiname ohne Pfad |
dirname |
${filename%/*} |
Verzeichnisname |
grep -q pattern file |
[[ -f $file ]] && [[ $(< $file) =~ $pattern ]] |
Mustersuche in Datei |
expr $a + $b |
$((a + b)) |
Arithmetische Operationen |
sed 's/find/replace/' |
${variable//find/replace} |
Textersetzung |
tr 'A-Z' 'a-z' |
${variable,,} (Bash 4+) |
Konvertierung in Kleinbuchstaben |
tr 'a-z' 'A-Z' |
${variable^^} (Bash 4+) |
Konvertierung in Großbuchstaben |
cut -d: -f1 |
${line%%:*} |
Erstes Feld mit Trennzeichen |
cut -d: -f2 |
${line#*:} (bei 2 Feldern) |
Zweites Feld mit Trennzeichen |
wc -l |
Zählervariable erhöhen | Zählen von Zeilen |
Häufig wiederholte Befehle sollten nach Möglichkeit außerhalb von Schleifen ausgeführt werden:
#!/bin/bash
# Ineffizient: Wiederholter Aufruf im Loop
for file in *.txt; do
lines=$(wc -l < "$file")
if [[ $lines -gt 100 ]]; then
current_date=$(date +"%Y-%m-%d") # Unnötige Wiederholung
echo "$file hat $lines Zeilen am $current_date"
fi
done
# Effizient: Aufruf vor der Schleife
current_date=$(date +"%Y-%m-%d") # Nur einmal aufgerufen
for file in *.txt; do
lines=$(wc -l < "$file")
if [[ $lines -gt 100 ]]; then
echo "$file hat $lines Zeilen am $current_date"
fi
doneBesonders kritisch ist die Prozesserzeugung innerhalb von Schleifen:
#!/bin/bash
# Ineffizient: Prozessaufruf in jeder Iteration
for i in {1..1000}; do
echo "$i" | grep -q "^[0-9]*$"
# Weitere Verarbeitung...
done
# Effizient: Bash-interne Funktionen nutzen
for i in {1..1000}; do
if [[ $i =~ ^[0-9]*$ ]]; then
# Weitere Verarbeitung...
fi
doneBei unvermeidbaren Prozessaufrufen in Schleifen sollte die Batch-Verarbeitung erwogen werden:
#!/bin/bash
# Ineffizient: Einzelverarbeitung jeder Datei
for file in *.log; do
gzip "$file"
done
# Effizient: Batch-Verarbeitung aller Dateien
gzip *.logSubshells verursachen wie externe Befehle einen Performance-Overhead:
#!/bin/bash
# Ineffizient: Unnötige Subshell
result=$(cat file.txt) # Erzeugt Subshell
# Effizient: Redirection ohne Subshell
result=$(<file.txt) # Keine SubshellBesonders bei der Verarbeitung von Pipelines können Subshells eingespart werden:
#!/bin/bash
# Ineffizient: Pipeline in Subshell
lines=$(grep "error" logfile.txt | wc -l) # 3 Prozesse: grep, wc und Subshell
# Effizienter: Process Substitution
while read -r line; do
(( count++ ))
done < <(grep "error" logfile.txt) # 1 Prozess: grep, keine Subshell für die Zählung
echo "Anzahl der Fehler: $count"Funktionsaufrufe in Bash erzeugen normalerweise keine neuen Prozesse, können aber durch bestimmte Konstrukte dazu führen:
#!/bin/bash
# Ineffizient: Implizite Subshell durch Pipe in Funktion
get_errors() {
grep "error" "$1" | sort # Erzeugt Subshell wegen Pipe
}
errors=$(get_errors "logfile.txt")
# Effizienter: Verwendung von Process Substitution
get_errors_efficient() {
sort < <(grep "error" "$1") # Vermeidet eine zusätzliche Subshell
}
errors=$(get_errors_efficient "logfile.txt")Pipelines sind ein mächtiges Feature, erzeugen aber für jeden Schritt einen neuen Prozess:
#!/bin/bash
# Ineffizient: Lange Pipeline mit vielen Prozessen
cat logfile.txt | grep "error" | sort | uniq -c | sort -nr | head -5
# Effizienter: Reduzierung der Pipeline-Elemente
grep "error" logfile.txt | sort | uniq -c | sort -nr | head -5 # Ein Prozess wenigerIn extremen Fällen kann eine Pipeline durch eine kombinierte Operation ersetzt werden:
#!/bin/bash
# Ineffizient: Viele kleine Operationen
cat names.txt | tr 'A-Z' 'a-z' | sort | uniq > unique_names.txt
# Effizienter: Verwendung von awk für kombinierte Operationen
awk '{print tolower($0)}' names.txt | sort -u > unique_names.txtDie Verwendung temporärer Dateien führt oft zu zusätzlichen Prozessaufrufen:
#!/bin/bash
# Ineffizient: Temporäre Dateien und zusätzliche Prozesse
grep "pattern" input.txt > temp.txt
sort temp.txt > sorted.txt
uniq sorted.txt > result.txt
rm temp.txt sorted.txt
# Effizient: Direkte Pipeline ohne temporäre Dateien
grep "pattern" input.txt | sort | uniq > result.txtDas Traversieren des Dateisystems kann bei ineffizienter Implementierung zu vielen Prozessaufrufen führen:
#!/bin/bash
# Ineffizient: Mehrfache find-Aufrufe
for ext in txt log csv; do
count=$(find /path -name "*.$ext" | wc -l)
echo "Anzahl $ext-Dateien: $count"
done
# Effizient: Einmaliger find-Aufruf
declare -A counts
while read -r file; do
ext="${file##*.}"
((counts[$ext]++))
done < <(find /path -type f)
for ext in "${!counts[@]}"; do
echo "Anzahl $ext-Dateien: ${counts[$ext]}"
doneDie Shell bietet viele eingebaute Befehle, die keine separaten Prozesse erfordern:
#!/bin/bash
# Übersicht wichtiger Bash-Builtins
builtin_commands=$(help | grep -o '^\w\+' | sort)
echo "Verfügbare Bash-Builtins: $builtin_commands"
# Beispiel: test-Builtin statt externes Programm
if [ -f "$file" ]; then # Externes [
echo "Datei existiert"
fi
# Effizienter: Bash-internes [[
if [[ -f "$file" ]]; then # Bash-Builtin
echo "Datei existiert"
fiWichtige Bash-Builtins für die Leistungsoptimierung sind: -
[[ ]] anstelle von [ ] oder test
- read zum Einlesen von Daten - printf
anstelle von echo (in einigen Fällen) -
mapfile/readarray zum Einlesen von Arrays
(Bash 4+) - cd, pwd für Verzeichnisoperationen
- time für Zeitmessungen - local,
declare für Variablendeklarationen
Arithmetische Operationen sollten mit Shell-internen Funktionen durchgeführt werden:
#!/bin/bash
# Ineffizient: Externe Programme für Berechnungen
sum=$(expr $a + $b)
product=$(echo "$a * $b" | bc)
# Effizient: Bash-interne Arithmetik
sum=$((a + b))
product=$((a * b))
# Für komplexere Berechnungen: bc in einer Transaktion
complex_calculation=$(bc <<EOF
scale=2
$a * $b / ($c + $d)
EOF
)#!/bin/bash
# log_parser_optimized.sh - Optimiertes Parsing von Log-Dateien
LOG_FILE="application.log"
SEARCH_TERM="ERROR"
OUTPUT_FILE="error_summary.txt"
# Ineffiziente Implementierung:
#
# for line in $(cat "$LOG_FILE"); do
# if echo "$line" | grep -q "$SEARCH_TERM"; then
# timestamp=$(echo "$line" | cut -d' ' -f1-2)
# error_code=$(echo "$line" | grep -o "ERR-[0-9]\+" || echo "UNKNOWN")
# echo "$timestamp: $error_code" >> "$OUTPUT_FILE"
# fi
# done
# Optimierte Implementierung:
# 1. Vermeidet `cat`
# 2. Verwendet Bash-Regex statt grep
# 3. Verwendet Bash-Parameter-Expansion statt cut
# 4. Reduziert die Anzahl der Prozessaufrufe in der Schleife
# Initialisierung des Ausgabefiles
> "$OUTPUT_FILE"
# Regex-Muster für Fehlercode vordefinieren
error_pattern="(ERR-[0-9]+)"
while IFS= read -r line; do
if [[ "$line" == *"$SEARCH_TERM"* ]]; then
# Zeitstempel extrahieren (die ersten beiden Felder)
timestamp="${line%% *}" # Erstes Feld
rest="${line#* }"
timestamp="$timestamp ${rest%% *}" # Zweites Feld hinzufügen
# Fehlercode extrahieren
if [[ "$line" =~ $error_pattern ]]; then
error_code="${BASH_REMATCH[1]}"
else
error_code="UNKNOWN"
fi
echo "$timestamp: $error_code" >> "$OUTPUT_FILE"
fi
done < "$LOG_FILE"
echo "Fehleranalyse abgeschlossen. Ergebnisse in $OUTPUT_FILE"#!/bin/bash
# filesystem_stats_optimized.sh - Optimierte Dateisystem-Statistiken
TARGET_DIR="${1:-.}" # Zielverzeichnis oder aktuelles Verzeichnis
if [[ ! -d "$TARGET_DIR" ]]; then
echo "Fehler: Verzeichnis '$TARGET_DIR' existiert nicht." >&2
exit 1
fi
echo "Analysiere Verzeichnis: $TARGET_DIR"
# Ineffiziente Implementierung:
#
# echo "Anzahl der Dateien pro Typ:"
# for ext in txt log conf bak; do
# count=$(find "$TARGET_DIR" -type f -name "*.$ext" | wc -l)
# echo ".$ext: $count"
# done
#
# total_size=$(du -sh "$TARGET_DIR" | cut -f1)
# echo "Gesamtgröße: $total_size"
#
# largest_file=$(find "$TARGET_DIR" -type f -exec du -h {} \; | sort -hr | head -1)
# echo "Größte Datei: $largest_file"
# Optimierte Implementierung:
# 1. Einmaliger find-Aufruf statt mehrerer
# 2. Prozessierung in Bash statt externen Programmen
# 3. Verwendung assoziativer Arrays für Zählung
# 4. Parallele Operationen für unabhängige Analysen
# Deklaration für Statistiken
declare -A file_counts
declare -A size_bytes
# Prozess 1: Dateianzahl und -größen analysieren
{
while IFS= read -r file; do
# Dateiendung extrahieren
if [[ "$file" == *.* ]]; then
ext="${file##*.}"
((file_counts[$ext]++))
# Dateigröße für die Top-10-Liste
size=$(stat -c "%s" "$file" 2>/dev/null || echo "0")
size_bytes["$file"]=$size
fi
done < <(find "$TARGET_DIR" -type f -not -path "*/\.*" 2>/dev/null)
# Ausgabe der Dateianzahl nach Typ
echo "=== Anzahl der Dateien pro Typ ==="
for ext in "${!file_counts[@]}"; do
printf "%-10s: %d\n" ".$ext" "${file_counts[$ext]}"
done
} > stats_types.txt &
# Prozess 2: Gesamtgröße berechnen (du ist schneller als manuelles Aufsummieren)
{
echo "=== Speicherverbrauch ==="
echo -n "Gesamtgröße: "
du -sh "$TARGET_DIR" 2>/dev/null | cut -f1
} > stats_size.txt &
# Prozess 3: Die 10 größten Dateien finden
{
echo "=== Die 10 größten Dateien ==="
find "$TARGET_DIR" -type f -not -path "*/\.*" -exec du -h {} \; 2>/dev/null | sort -hr | head -10
} > stats_largest.txt &
# Auf Abschluss aller Hintergrundprozesse warten
wait
# Ergebnisse zusammenführen und anzeigen
cat stats_types.txt stats_size.txt stats_largest.txt
rm stats_types.txt stats_size.txt stats_largest.txt
echo "Analyse abgeschlossen."Die Vermeidung unnötiger Prozesse und Subshells ist eine der effektivsten Strategien zur Performance-Optimierung von Shell-Skripten:
sed,
awk, cut etc.$(()) statt
expr oder bc$(<file) statt $(cat file)
verwenden<(cmd)) anstelle von Pipelines
in Variablenzuweisungencat)awk
oder perlDurch die konsequente Anwendung dieser Prinzipien können Sie die Ausführungszeit von Shell-Skripten erheblich reduzieren und ihre Effizienz steigern. Besonders bei Skripten, die häufig ausgeführt werden oder mit großen Datenmengen arbeiten, können die Performanceverbesserungen signifikant sein.
set -x oder
bash -x, um unnötige Prozessaufrufe zu identifizierentime-Funktion, um den Effekt Ihrer
Optimierungen zu messenhelp in Bash)Schleifen und bedingte Anweisungen sind grundlegende Bausteine in Shell-Skripten und haben erheblichen Einfluss auf die Performance. In diesem Abschnitt werden Techniken vorgestellt, um Schleifen und Bedingungen zu optimieren und deren Ausführung zu beschleunigen.
Schleifen in Shell-Skripten werden interpretiert und nicht kompiliert, was sie potenziell langsamer macht als in kompilierten Sprachen. Die Hauptfaktoren, die die Performance von Schleifen beeinflussen, sind:
Die Bash bietet verschiedene Schleifenkonstrukte, die unterschiedliche Performance-Eigenschaften aufweisen:
#!/bin/bash
# Vergleich verschiedener Schleifentypen
echo "Performance-Vergleich von Schleifentypen:"
# 1. for-Schleife mit Bereichsexpansion (schnellste Methode für numerische Bereiche)
time {
for i in {1..10000}; do
: # Null-Operation
done
}
# 2. for-Schleife mit seq (erzeugt separaten Prozess)
time {
for i in $(seq 1 10000); do
:
done
}
# 3. C-ähnliche for-Schleife (gut für numerische Schleifen)
time {
for ((i=1; i<=10000; i++)); do
:
done
}
# 4. while-Schleife mit arithmetischem Test
time {
i=1
while (( i <= 10000 )); do
((i++))
done
}
# 5. until-Schleife mit arithmetischem Test
time {
i=1
until (( i > 10000 )); do
((i++))
done
}Ergebnisse und Empfehlungen:
{1..10000}) ist
am schnellsten für feste numerische Bereiche, da sie vor der
Schleifenausführung expandiert wird.seq oder anderen externen Befehlen
sollte vermieden werden, da sie zusätzliche Prozesse erzeugen.#!/bin/bash
# Ineffizient: Verwendung von seq (erzeugt einen separaten Prozess)
for i in $(seq 1 1000); do
echo "$i"
done
# Effizienter: Bereichsexpansion (interne Shell-Expansion)
for i in {1..1000}; do
echo "$i"
done
# Ebenfalls effizient: C-ähnliche for-Schleife
for ((i=1; i<=1000; i++)); do
echo "$i"
done#!/bin/bash
# Ineffizient: Verwendung von ls in einer Schleife
for file in $(ls *.txt); do
# Problematisch bei Dateinamen mit Leerzeichen und Sonderzeichen
wc -l "$file"
done
# Effizienter: Direkte Globbing-Expansion
for file in *.txt; do
wc -l "$file"
done#!/bin/bash
# Ineffizient: Verwendung von cat
for line in $(cat file.txt); do
# Problematisch - teilt nach Whitespace, nicht nach Zeilen
echo "Zeile: $line"
done
# Besser: Verwendung von while-read
while IFS= read -r line; do
echo "Zeile: $line"
done < file.txt#!/bin/bash
# Ineffizient: Verwendung von cat und Pipe
cat file.txt | while read -r line; do
echo "Verarbeite: $line"
done
# Effizient: Direkte Umleitung
while IFS= read -r line; do
echo "Verarbeite: $line"
done < file.txt#!/bin/bash
# Ineffizient: Mehrere externe Befehle pro Zeile
while IFS= read -r line; do
name=$(echo "$line" | cut -d, -f1)
email=$(echo "$line" | cut -d, -f2)
echo "Name: $name, Email: $email"
done < users.csv
# Effizient: IFS für Feldtrennung verwenden
while IFS=, read -r name email rest; do
echo "Name: $name, Email: $email"
done < users.csv#!/bin/bash
# Effizienter: Zeilenweises Lesen mit IFS und Batch-Verarbeitung
{
# Array für Batch-Verarbeitung
declare -a batch
batch_size=1000
count=0
while IFS= read -r line; do
batch[count++]="$line"
# Batch verarbeiten, wenn voll
if ((count == batch_size)); then
# Batch-Verarbeitung hier
for item in "${batch[@]}"; do
# Verarbeitung jedes Elements
: # Platzhalter für tatsächliche Verarbeitung
done
# Batch zurücksetzen
batch=()
count=0
fi
done < largefile.txt
# Restlichen Batch verarbeiten
for item in "${batch[@]}"; do
# Verarbeitung jedes Elements
: # Platzhalter für tatsächliche Verarbeitung
done
}Eine wichtige Optimierungstechnik ist das frühzeitige Abbrechen einer Schleife, sobald das Ziel erreicht ist:
#!/bin/bash
# Ineffizient: Durchlaufen des gesamten Arrays
array=(1 2 3 4 5 ... 10000)
found=0
for item in "${array[@]}"; do
if [[ "$item" -eq 42 ]]; then
found=1
fi
# Weitere teure Operationen...
done
if [[ "$found" -eq 1 ]]; then
echo "Element gefunden"
fi
# Effizient: Frühes Abbrechen
for item in "${array[@]}"; do
if [[ "$item" -eq 42 ]]; then
echo "Element gefunden"
break # Schleife sofort verlassen
fi
# Weitere teure Operationen...
doneBedingte Anweisungen (if, case) können
ebenfalls optimiert werden, um die Performance zu verbessern.
#!/bin/bash
# Ineffizient: Häufigster Fall wird zuletzt geprüft
process_request() {
if [[ "$request_type" == "delete" ]]; then
# Selten (5% der Fälle)
delete_resource
elif [[ "$request_type" == "update" ]]; then
# Gelegentlich (15% der Fälle)
update_resource
elif [[ "$request_type" == "get" ]]; then
# Sehr häufig (80% der Fälle)
get_resource
fi
}
# Effizient: Häufigste Fälle zuerst prüfen
process_request_optimized() {
if [[ "$request_type" == "get" ]]; then
# Sehr häufig (80% der Fälle)
get_resource
elif [[ "$request_type" == "update" ]]; then
# Gelegentlich (15% der Fälle)
update_resource
elif [[ "$request_type" == "delete" ]]; then
# Selten (5% der Fälle)
delete_resource
fi
}case statt mehrerer if-Anweisungen#!/bin/bash
# Ineffizient: Mehrere if-Anweisungen
check_status() {
if [[ "$status" == "running" ]]; then
handle_running
elif [[ "$status" == "stopped" ]]; then
handle_stopped
elif [[ "$status" == "paused" ]]; then
handle_paused
elif [[ "$status" == "unknown" ]]; then
handle_unknown
fi
}
# Effizient: case-Anweisung
check_status_optimized() {
case "$status" in
running)
handle_running
;;
stopped)
handle_stopped
;;
paused)
handle_paused
;;
unknown|*)
handle_unknown
;;
esac
}Die case-Anweisung ist nicht nur lesbarer, sondern in
der Regel auch schneller, da sie eine effizientere interne
Implementierung nutzt.
Die Bash führt eine Kurzschlussauswertung (short-circuit evaluation) bei logischen Operatoren durch:
#!/bin/bash
# Ineffizient: Alle Bedingungen werden immer geprüft
if [[ -f "$file" ]]; then
if [[ -r "$file" ]]; then
if [[ $(stat -c %s "$file") -gt 0 ]]; then
process_file "$file"
fi
fi
fi
# Effizient: Kurzschlussauswertung
if [[ -f "$file" && -r "$file" && $(stat -c %s "$file") -gt 0 ]]; then
process_file "$file"
fi
# Noch effizienter: Teure Operationen zuletzt
if [[ -f "$file" && -r "$file" ]] && [[ $(stat -c %s "$file") -gt 0 ]]; then
process_file "$file"
fiBei der Kurzschlussauswertung werden Bedingungen von links nach
rechts ausgewertet und die Auswertung wird abgebrochen, sobald das
Ergebnis feststeht. Daher sollten: - Bei UND-Verknüpfungen
(&&) die wahrscheinlich falschen oder günstigen
Bedingungen zuerst geprüft werden - Bei ODER-Verknüpfungen
(||) die wahrscheinlich wahren oder günstigen Bedingungen
zuerst geprüft werden
#!/bin/bash
# Ineffizient: Teure Berechnung wird immer durchgeführt
if [[ $(expensive_calculation) -eq $expected_value ]]; then
# Nur relevant, wenn Bedingung wahr ist
fi
# Effizient: Berechnung in Variable speichern, wenn mehrfach verwendet
result=$(expensive_calculation)
if [[ $result -eq $expected_value ]]; then
# Verwende $result mehrfach
fi
# Alternative: Berechnung nur bei Bedarf durchführen
if [[ condition_that_is_often_false ]]; then
result=$(expensive_calculation)
if [[ $result -eq $expected_value ]]; then
# Weitere Verarbeitung
fi
fi#!/bin/bash
# Ineffizient: Schleife für einfache Textsuche
found=0
while IFS= read -r line; do
if [[ "$line" == *"ERROR"* ]]; then
found=1
break
fi
done < logfile.txt
# Effizient: grep für einfache Textsuche verwenden
if grep -q "ERROR" logfile.txt; then
found=1
fiFür komplexere Muster kann eine Kombination aus grep und
Bedingungen effizienter sein:
#!/bin/bash
# Komplexe Filterung effizient implementieren
if grep -q "ERROR" logfile.txt; then
# Nur wenn Fehler vorhanden sind, weitere Analysen durchführen
error_lines=$(grep "ERROR" logfile.txt)
# Weitere Verarbeitung der Fehlerzeilen
echo "$error_lines" | while IFS= read -r line; do
# Detaillierte Analyse jeder Fehlerzeile
error_code=$(echo "$line" | grep -o "ERR-[0-9]\+")
echo "Gefundener Fehlercode: $error_code"
done
fi#!/bin/bash
# Ineffizient: Bedingungsprüfungen in jeder Iteration
for file in *.log; do
if [[ -f "$file" && ! -L "$file" && -r "$file" ]]; then
if grep -q "ERROR" "$file"; then
# Verarbeitung...
fi
fi
done
# Effizienter: Vorabfilterung der Dateien
for file in *.log; do
[[ -f "$file" && ! -L "$file" && -r "$file" ]] || continue
# Nur Dateien verarbeiten, die die Bedingungen erfüllen
if grep -q "ERROR" "$file"; then
# Verarbeitung...
fi
done
# Alternative: find mit exec für parallele Verarbeitung
find . -name "*.log" -type f -readable -exec grep -l "ERROR" {} \; |
while IFS= read -r file; do
# Verarbeitung...
doneDie Verwendung von continue zur frühen Überspringung
nicht relevanter Iterationen kann die Performance erheblich
verbessern.
Für Bash 4+ bietet der mapfile (oder
readarray) Befehl eine effiziente Methode, Zeilen direkt in
ein Array einzulesen:
#!/bin/bash
# Ineffizient: Zeilenweise Verarbeitung
while IFS= read -r line; do
lines+=("$line")
done < file.txt
# Effizient: Direkt in Array einlesen (Bash 4+)
mapfile -t lines < file.txt
# Noch effizienter für große Dateien: Einlesen in Blöcken
mapfile -n 1000 -t lines < file.txt # Erste 1000 Zeilen einlesenOft kann die Iteration durch Verwendung von Transformationsbefehlen vermieden werden:
#!/bin/bash
# Ineffizient: Iteration zum Transformieren jeder Zeile
transformed_lines=()
while IFS= read -r line; do
transformed_lines+=("$(echo "$line" | tr '[:lower:]' '[:upper:]')")
done < file.txt
# Effizient: Direkte Transformation ohne Schleife
tr '[:lower:]' '[:upper:]' < file.txt > transformed.txtFür rechenintensive Operationen kann die Parallelisierung die Performance erheblich verbessern:
#!/bin/bash
# Sequenzielle Verarbeitung
for file in *.log; do
process_file "$file" # Zeitaufwändige Verarbeitung
done
# Parallele Verarbeitung mit Kontrolle der gleichzeitigen Prozesse
max_jobs=4 # Maximale Anzahl gleichzeitiger Prozesse
current_jobs=0
for file in *.log; do
# Warten, wenn maximale Anzahl an Jobs erreicht
while ((current_jobs >= max_jobs)); do
# Warten auf Beendigung eines Jobs
wait -n
((current_jobs--))
done
# Starten eines neuen Hintergrundprozesses
process_file "$file" &
((current_jobs++))
done
# Warten auf Abschluss aller verbleibenden Jobs
wait#!/bin/bash
# optimized_text_processor.sh - Effiziente Verarbeitung großer Textdateien
INPUT_FILE="$1"
OUTPUT_FILE="$2"
SEARCH_PATTERN="$3"
if [[ $# -lt 3 ]]; then
echo "Verwendung: $0 <eingabedatei> <ausgabedatei> <suchmuster>" >&2
exit 1
fi
if [[ ! -f "$INPUT_FILE" || ! -r "$INPUT_FILE" ]]; then
echo "Fehler: Eingabedatei nicht gefunden oder nicht lesbar." >&2
exit 1
fi
# Zähle die Zeilen, die dem Suchmuster entsprechen
matching_lines=$(grep -c "$SEARCH_PATTERN" "$INPUT_FILE")
# Frühzeitiger Abbruch, wenn keine Übereinstimmungen
if [[ $matching_lines -eq 0 ]]; then
echo "Keine Übereinstimmungen gefunden. Erstelle leere Ausgabedatei."
> "$OUTPUT_FILE"
exit 0
fi
echo "Gefundene Übereinstimmungen: $matching_lines"
# Effiziente Batch-Verarbeitung der Zeilen
{
# Kopfzeile in Ausgabedatei schreiben
echo "# Verarbeitete Zeilen mit Muster: $SEARCH_PATTERN" > "$OUTPUT_FILE"
echo "# Gesamtzahl der Übereinstimmungen: $matching_lines" >> "$OUTPUT_FILE"
echo "# Zeitstempel: $(date)" >> "$OUTPUT_FILE"
echo "# Format: Zeilennummer|Verarbeitete Zeile" >> "$OUTPUT_FILE"
echo "-------------------------------------------" >> "$OUTPUT_FILE"
# Optimierte Verarbeitung der übereinstimmenden Zeilen
grep -n "$SEARCH_PATTERN" "$INPUT_FILE" |
while IFS=: read -r line_num line_content; do
# Effiziente String-Manipulation mit Shell-internen Funktionen
# 1. Entferne führende/nachfolgende Leerzeichen
trimmed="${line_content#"${line_content%%[![:space:]]*}"}"
trimmed="${trimmed%"${trimmed##*[![:space:]]}"}"
# 2. Konvertiere zu Großbuchstaben (Bash 4+)
upper="${trimmed^^}"
# 3. Schreibe das Ergebnis in die Ausgabedatei
echo "$line_num|$upper" >> "$OUTPUT_FILE"
done
}
echo "Verarbeitung abgeschlossen. Ergebnisse in $OUTPUT_FILE"#!/bin/bash
# optimized_file_finder.sh - Effiziente Suche und Filterung von Dateien
TARGET_DIR="${1:-.}"
MIN_SIZE="${2:-0}" # Minimale Größe in Bytes
PATTERN="${3:-.*}" # Regex-Muster für Dateinamen
MAX_DEPTH="${4:-999}" # Maximale Suchtiefe
if [[ ! -d "$TARGET_DIR" ]]; then
echo "Fehler: Verzeichnis '$TARGET_DIR' existiert nicht." >&2
exit 1
fi
echo "Suche Dateien in: $TARGET_DIR"
echo "Minimale Größe: $MIN_SIZE Bytes"
echo "Namensmuster: $PATTERN"
echo "Maximale Suchtiefe: $MAX_DEPTH"
# Array für gefundene Dateien
declare -a found_files
# Optimierte Tiefenbegrenzung mit find
if [[ $MAX_DEPTH -eq 999 ]]; then
# Keine Tiefenbegrenzung angegeben, verwende Standard
depth_option=""
else
depth_option="-maxdepth $MAX_DEPTH"
fi
# Effiziente Verwendung von find mit vorgefilterten Ergebnissen
echo "Suche nach Dateien..."
start_time=$(date +%s.%N)
# Einmaliger find-Aufruf mit allen Filtern
mapfile -t found_files < <(find "$TARGET_DIR" $depth_option -type f -size +"$MIN_SIZE"c -regextype posix-extended -regex ".*$PATTERN.*" -not -path "*/\.*")
end_time=$(date +%s.%N)
elapsed=$(echo "$end_time - $start_time" | bc)
# Effiziente Verarbeitung der Ergebnisse
total_files=${#found_files[@]}
total_size=0
if [[ $total_files -gt 0 ]]; then
echo -e "\nGefundene Dateien ($total_files):"
# Effiziente Schleife durch die gefundenen Dateien
for ((i=0; i<$total_files; i++)); do
file="${found_files[$i]}"
# Größe direkt mit stat abrufen (effizienter als du)
size=$(stat -c %s "$file")
size_human=$(numfmt --to=iec-i --suffix=B --format="%.2f" "$size")
# Akkumulierte Größe berechnen
((total_size += size))
# Ausgabe mit Formatierung
printf "%4d: %s (%s)\n" $((i+1)) "$file" "$size_human"
# Bei zu vielen Dateien nach jeweils 20 eine Pause für Benutzerinteraktion
if ((i+1 >= 20 && (i+1) % 20 == 0 && i+1 < total_files)); then
read -p "-- Drücken Sie Enter für weitere Ergebnisse (${i+1}/$total_files) --" -s
echo
fi
done
# Gesamtgröße formatieren
total_size_human=$(numfmt --to=iec-i --suffix=B --format="%.2f" "$total_size")
echo -e "\nZusammenfassung:"
echo "Gefundene Dateien: $total_files"
echo "Gesamtgröße: $total_size_human"
echo "Suchzeit: $elapsed Sekunden"
else
echo "Keine Dateien gefunden, die den Kriterien entsprechen."
fiDie Optimierung von Schleifen und Bedingungen kann die Performance von Shell-Skripten erheblich verbessern:
{1..n}) für feste numerische
Bereichewhile read für zeilenweise Dateiverarbeitungseq in
Schleifendefinitionenbreak, wenn
möglichcontinuemapfile/readarray für
effizientes Einlesen in Arrayscase statt mehrerer
if-Anweisungen&& und
||grep für
TextmustersucheDurch die Anwendung dieser Optimierungstechniken können Shell-Skripte deutlich schneller ausgeführt werden, insbesondere wenn sie große Datenmengen verarbeiten oder viele Iterationen durchführen.
Caching und Memoization sind leistungsstarke Techniken zur Vermeidung wiederholter, rechenintensiver Operationen in Shell-Skripten. Durch die Speicherung von Ergebnissen für spätere Wiederverwendung können erhebliche Performance-Verbesserungen erzielt werden. In diesem Abschnitt werden Methoden und Strategien für effektives Caching in Shell-Skripten vorgestellt.
Caching in Shell-Skripten basiert auf einem einfachen Prinzip: Speichere das Ergebnis einer teuren Operation und verwende es wieder, anstatt die Operation erneut auszuführen.
Caching bietet besonders große Vorteile bei:
Die einfachste Form des Cachings ist die Speicherung von Ergebnissen in Variablen:
#!/bin/bash
# Ineffizient: Wiederholter Aufruf desselben Befehls
for user in $(cat users.txt); do
home_dir=$(grep "^$user:" /etc/passwd | cut -d: -f6)
echo "Heimatverzeichnis von $user ist $home_dir"
done
# Effizient: Caching der Passwortdaten
passwd_data=$(cat /etc/passwd)
for user in $(cat users.txt); do
home_dir=$(echo "$passwd_data" | grep "^$user:" | cut -d: -f6)
echo "Heimatverzeichnis von $user ist $home_dir"
doneMemoization ist eine spezifische Form des Cachings, bei der die Ergebnisse von Funktionsaufrufen gespeichert werden:
#!/bin/bash
# Deklarieren eines assoziativen Arrays für den Cache
declare -A factorial_cache
# Rekursive Fakultätsfunktion mit Memoization
factorial() {
local n=$1
# Basisfall
if [[ $n -le 1 ]]; then
echo 1
return
fi
# Prüfen, ob das Ergebnis bereits im Cache ist
if [[ -n "${factorial_cache[$n]}" ]]; then
echo "${factorial_cache[$n]}"
return
fi
# Berechnung durchführen
local prev=$(factorial $(($n - 1)))
local result=$(($n * $prev))
# Ergebnis im Cache speichern
factorial_cache[$n]=$result
echo $result
}
# Beispielverwendung
time echo "Fakultät von 20: $(factorial 20)"
time echo "Fakultät von 20 (cached): $(factorial 20)"In diesem Beispiel wird das Ergebnis jeder Fakultätsberechnung in einem assoziativen Array gespeichert, wodurch wiederholte Berechnungen vermieden werden.
Für persistentes Caching zwischen verschiedenen Skriptaufrufen kann das Dateisystem verwendet werden:
#!/bin/bash
# Funktion zum Speichern von Daten im Cache
cache_store() {
local key=$1
local value=$2
local cache_dir=${CACHE_DIR:-"$HOME/.cache/myscript"}
# Cache-Verzeichnis erstellen, falls nicht vorhanden
mkdir -p "$cache_dir"
# Schlüssel in einen sicheren Dateinamen umwandeln
local safe_key=$(echo "$key" | md5sum | cut -d' ' -f1)
# Wert in die Cache-Datei schreiben
echo "$value" > "$cache_dir/$safe_key"
# Metadaten speichern (Original-Schlüssel und Zeitstempel)
echo "key: $key" > "$cache_dir/$safe_key.meta"
echo "timestamp: $(date +%s)" >> "$cache_dir/$safe_key.meta"
}
# Funktion zum Abrufen von Daten aus dem Cache
cache_retrieve() {
local key=$1
local max_age=${2:-86400} # Standardmäßig 24 Stunden (in Sekunden)
local cache_dir=${CACHE_DIR:-"$HOME/.cache/myscript"}
# Schlüssel in einen sicheren Dateinamen umwandeln
local safe_key=$(echo "$key" | md5sum | cut -d' ' -f1)
local cache_file="$cache_dir/$safe_key"
local meta_file="$cache_dir/$safe_key.meta"
# Prüfen, ob Cache-Eintrag existiert
if [[ ! -f "$cache_file" || ! -f "$meta_file" ]]; then
return 1
fi
# Zeitstempel aus Metadaten lesen
local timestamp=$(grep "^timestamp: " "$meta_file" | cut -d' ' -f2)
local current_time=$(date +%s)
local age=$((current_time - timestamp))
# Prüfen, ob der Cache-Eintrag noch gültig ist
if [[ $age -gt $max_age ]]; then
return 1
fi
# Cache-Eintrag zurückgeben
cat "$cache_file"
return 0
}
# Funktion zum Löschen eines Cache-Eintrags
cache_invalidate() {
local key=$1
local cache_dir=${CACHE_DIR:-"$HOME/.cache/myscript"}
# Schlüssel in einen sicheren Dateinamen umwandeln
local safe_key=$(echo "$key" | md5sum | cut -d' ' -f1)
# Cache-Dateien löschen
rm -f "$cache_dir/$safe_key" "$cache_dir/$safe_key.meta"
}
# Funktion mit Caching
get_weather() {
local city=$1
local cache_key="weather_$city"
# Versuchen, Daten aus dem Cache zu holen (max. 1 Stunde alt)
if weather_data=$(cache_retrieve "$cache_key" 3600); then
echo "$weather_data [cached]"
return
fi
# Wenn nicht im Cache, Daten abrufen (simuliert)
echo "Rufe Wetterdaten für $city ab..." >&2
sleep 2 # Simuliere eine langsame API-Anfrage
weather_data="Sonnig, 25°C in $city"
# Daten im Cache speichern
cache_store "$cache_key" "$weather_data"
echo "$weather_data [fresh]"
}
# Beispielverwendung
get_weather "Berlin" # Erste Anfrage: nicht im Cache
get_weather "Berlin" # Zweite Anfrage: aus dem Cache
get_weather "München" # Neue Stadt: nicht im CacheDiese Implementierung bietet: - Persistentes Caching zwischen Skriptaufrufen - Schlüsselbasiertes Abrufen von Daten - Zeitbasierte Cache-Invalidierung - Speicherung von Metadaten
Ein wichtiger Aspekt des Cachings ist die Entscheidung, wann Cache-Einträge ungültig werden:
#!/bin/bash
# Funktion zum Laden einer Konfigurationsdatei mit Cache-Validierung
load_config() {
local config_file="$1"
local cache_key="config_${config_file//\//_}"
local config_data
# Zeitstempel der Konfigurationsdatei
local file_timestamp=$(stat -c %Y "$config_file" 2>/dev/null || echo "0")
# Prüfen, ob ein gültiger Cache-Eintrag existiert
if [[ -f "$CACHE_DIR/$cache_key" ]]; then
local cache_timestamp=$(stat -c %Y "$CACHE_DIR/$cache_key" 2>/dev/null || echo "0")
# Cache verwenden, wenn die Datei nicht geändert wurde
if [[ $file_timestamp -le $cache_timestamp ]]; then
config_data=$(<"$CACHE_DIR/$cache_key")
echo "$config_data"
return
fi
fi
# Cache ist ungültig oder nicht vorhanden, Datei neu laden
if [[ -f "$config_file" ]]; then
config_data=$(<"$config_file")
# Cache aktualisieren
mkdir -p "$CACHE_DIR"
echo "$config_data" > "$CACHE_DIR/$cache_key"
echo "$config_data"
else
echo "Fehler: Konfigurationsdatei nicht gefunden" >&2
return 1
fi
}Diese Funktion validiert den Cache basierend auf dem Änderungszeitpunkt der Originaldatei.
Die richtige Granularität ist entscheidend für effizientes Caching:
#!/bin/bash
# Zu feingranular: Caching auf Zeilenebene
process_file_fine_grained() {
local file="$1"
local result=""
while IFS= read -r line; do
# Caching zu feingranular - Overhead > Nutzen
line_hash=$(echo "$line" | md5sum | cut -d' ' -f1)
if [[ -f "$CACHE_DIR/$line_hash" ]]; then
processed_line=$(<"$CACHE_DIR/$line_hash")
else
processed_line=$(process_line "$line")
echo "$processed_line" > "$CACHE_DIR/$line_hash"
fi
result+="$processed_line"$'\n'
done < "$file"
echo "$result"
}
# Gute Granularität: Caching auf Dateiebene
process_file_good_granularity() {
local file="$1"
local file_hash=$(md5sum "$file" | cut -d' ' -f1)
if [[ -f "$CACHE_DIR/$file_hash" ]]; then
cat "$CACHE_DIR/$file_hash"
else
# Verarbeiten und Ergebnis cachen
process_file_content "$file" > "$CACHE_DIR/$file_hash"
cat "$CACHE_DIR/$file_hash"
fi
}Der Cache-Schlüssel sollte eindeutig die zu cachende Operation identifizieren:
#!/bin/bash
# Ineffizient: Ungenauer Cache-Schlüssel
cache_data() {
local query="$1"
local cache_key="query" # Zu generisch!
# Rest der Implementierung...
}
# Effizienter: Präziser Cache-Schlüssel
cache_data_improved() {
local query="$1"
local cache_key="query_$(echo "$query" | md5sum | cut -d' ' -f1)"
# Rest der Implementierung...
}
# Noch besser: Berücksichtigung aller Einflussfaktoren
cache_data_optimal() {
local query="$1"
local api_version="$2"
local format="$3"
# Alle relevanten Parameter im Schlüssel berücksichtigen
local cache_key="query_${api_version}_${format}_$(echo "$query" | md5sum | cut -d' ' -f1)"
# Rest der Implementierung...
}Bei großen Datensätzen sollte ein Gleichgewicht zwischen Cache-Größe und Performance gefunden werden:
#!/bin/bash
# Funktion zur Verwaltung einer begrenzten Cache-Größe
maintain_cache_size() {
local max_size=${1:-100} # Maximale Anzahl von Cache-Einträgen
local cache_dir=${CACHE_DIR:-"$HOME/.cache/myscript"}
# Anzahl der Cache-Einträge zählen
local cache_count=$(find "$cache_dir" -type f -not -name "*.meta" | wc -l)
# Cache bereinigen, wenn er zu groß wird
if [[ $cache_count -gt $max_size ]]; then
echo "Cache-Bereinigung: Lösche alte Einträge..." >&2
# Lösche die ältesten Einträge basierend auf dem Zeitstempel
find "$cache_dir" -type f -name "*.meta" -exec stat -c "%Y %n" {} \; |
sort -n |
head -n $(($cache_count - $max_size)) |
while read timestamp meta_file; do
# Extrahiere Basis-Dateinamen ohne .meta-Suffix
base_file="${meta_file%.meta}"
rm -f "$base_file" "$meta_file"
done
fi
}
# Verwenden der Cache-Größenverwaltung nach dem Hinzufügen neuer Einträge
cache_store() {
# ... [Implementierung wie zuvor] ...
# Cache-Größe überprüfen und gegebenenfalls bereinigen
maintain_cache_size 100
}Eine häufige Anwendung von Caching ist das Zwischenspeichern der Ausgabe teurer Shell-Befehle:
#!/bin/bash
# Funktion zum Cachen von Befehlsausgaben
cached_command() {
local cmd="$1"
local cache_key="cmd_$(echo "$cmd" | md5sum | cut -d' ' -f1)"
local max_age=${2:-3600} # Standard: 1 Stunde
# Versuchen, aus dem Cache zu laden
if output=$(cache_retrieve "$cache_key" "$max_age"); then
echo "$output"
return
fi
# Befehl ausführen und Ausgabe cachen
output=$(eval "$cmd")
cache_store "$cache_key" "$output"
echo "$output"
}
# Beispielverwendung
# Teure Befehlsausführung mit 5-minütigem Cache
system_info=$(cached_command "lshw -short" 300)
echo "$system_info"#!/bin/bash
# api_client.sh - API-Client mit Caching
# Konfiguration
CACHE_DIR="$HOME/.cache/api_client"
API_KEY="your_api_key_here"
API_ENDPOINT="https://api.example.com/data"
# Cache-Verzeichnis erstellen
mkdir -p "$CACHE_DIR"
# Funktion zum Abrufen von API-Daten mit Caching
get_api_data() {
local query="$1"
local max_age=${2:-3600} # Standard: 1 Stunde Cache-Gültigkeit
# Cache-Schlüssel generieren
local cache_key="api_$(echo "${query}_${API_ENDPOINT}" | md5sum | cut -d' ' -f1)"
local cache_file="$CACHE_DIR/$cache_key"
local meta_file="$CACHE_DIR/$cache_key.meta"
# Prüfen, ob ein gültiger Cache-Eintrag existiert
if [[ -f "$cache_file" && -f "$meta_file" ]]; then
local timestamp=$(grep "^timestamp: " "$meta_file" | cut -d' ' -f2)
local current_time=$(date +%s)
local age=$((current_time - timestamp))
if [[ $age -le $max_age ]]; then
echo "Verwende gecachte Daten (Alter: $age Sekunden)" >&2
cat "$cache_file"
return 0
else
echo "Cache abgelaufen (Alter: $age Sekunden, Max: $max_age Sekunden)" >&2
fi
else
echo "Kein Cache-Eintrag gefunden" >&2
fi
# API-Anfrage durchführen
echo "Rufe API-Daten ab für Query: $query" >&2
# Simulierte API-Anfrage (in echten Anwendungen: curl oder wget)
sleep 2 # Simuliere Netzwerklatenz
# In realen Szenarien würde hier ein echter API-Aufruf stehen, z.B.:
# local response=$(curl -s -H "Authorization: Bearer $API_KEY" "$API_ENDPOINT?q=$query")
# Simulierte Antwort für das Beispiel
local response="{ \"data\": \"Ergebnis für $query\", \"timestamp\": \"$(date)\" }"
# Antwort im Cache speichern
echo "$response" > "$cache_file"
# Metadaten speichern
echo "query: $query" > "$meta_file"
echo "timestamp: $(date +%s)" >> "$meta_file"
echo "endpoint: $API_ENDPOINT" >> "$meta_file"
# Cache-Größe verwalten
maintain_cache_size 50
# Antwort zurückgeben
echo "$response"
}
# Funktion zur Verwaltung der Cache-Größe
maintain_cache_size() {
local max_entries=${1:-100}
local current_entries=$(find "$CACHE_DIR" -name "*.meta" | wc -l)
if [[ $current_entries -gt $max_entries ]]; then
echo "Cache-Bereinigung: $current_entries Einträge gefunden, Limit: $max_entries" >&2
# Lösche die ältesten Einträge
find "$CACHE_DIR" -name "*.meta" -type f -printf "%T@ %p\n" |
sort -n |
head -n $((current_entries - max_entries)) |
while read timestamp meta_file; do
base_file="${meta_file%.meta}"
echo "Lösche alten Cache-Eintrag: ${base_file##*/}" >&2
rm -f "$base_file" "$meta_file"
done
fi
}
# Funktion zum manuellen Invalidieren des Caches
invalidate_cache() {
local query="$1"
if [[ -z "$query" ]]; then
echo "Lösche gesamten Cache..." >&2
rm -rf "${CACHE_DIR:?}/"*
else
echo "Lösche Cache für Query: $query" >&2
local cache_key="api_$(echo "${query}_${API_ENDPOINT}" | md5sum | cut -d' ' -f1)"
rm -f "$CACHE_DIR/$cache_key" "$CACHE_DIR/$cache_key.meta"
fi
}
# Hauptfunktion
main() {
if [[ $1 == "--clear-cache" ]]; then
invalidate_cache "${2:-}"
exit 0
fi
local query="$1"
if [[ -z "$query" ]]; then
echo "Verwendung: $0 <suchbegriff> [cache_max_alter_in_sekunden]" >&2
echo " $0 --clear-cache [suchbegriff]" >&2
exit 1
fi
local max_age="${2:-3600}" # Standardwert: 1 Stunde
# API-Daten abrufen mit Caching
get_api_data "$query" "$max_age"
}
# Skript ausführen
main "$@"#!/bin/bash
# file_analyzer.sh - Skript zur Analyse von Dateiinhalten mit Memoization
# Konfiguration
MEMO_DIR="$HOME/.cache/file_analyzer"
mkdir -p "$MEMO_DIR"
# Deklaration des assoziativen Arrays für In-Memory-Cache
declare -A mem_cache
# Funktion zur Analyse von Dateien mit mehrstufigem Caching
analyze_file() {
local file="$1"
local analysis_type="$2" # z.B. "wordcount", "lines", "complexity"
if [[ ! -f "$file" ]]; then
echo "Fehler: Datei nicht gefunden: $file" >&2
return 1
fi
# Cache-Schlüssel basierend auf Dateiinhalt, nicht nur Name
local file_hash=$(md5sum "$file" | cut -d' ' -f1)
local cache_key="${file_hash}_${analysis_type}"
# 1. Stufe: In-Memory-Cache prüfen (schnellste Option)
if [[ -n "${mem_cache[$cache_key]}" ]]; then
echo "${mem_cache[$cache_key]}"
echo "Quelle: In-Memory-Cache" >&2
return 0
fi
# 2. Stufe: Persistenter Dateisystem-Cache
local cache_file="$MEMO_DIR/$cache_key"
if [[ -f "$cache_file" ]]; then
# Prüfen, ob die Datei seit dem Caching geändert wurde
local cache_timestamp=$(stat -c %Y "$cache_file")
local file_timestamp=$(stat -c %Y "$file")
if [[ $file_timestamp -le $cache_timestamp ]]; then
# Cache ist noch gültig
local result=$(<"$cache_file")
# In den In-Memory-Cache übernehmen
mem_cache[$cache_key]="$result"
echo "$result"
echo "Quelle: Dateisystem-Cache" >&2
return 0
else
echo "Cache ungültig: Datei wurde seit dem Caching geändert" >&2
fi
fi
# 3. Stufe: Analyse durchführen
local result
echo "Führe Analyse durch: $analysis_type für $file" >&2
case "$analysis_type" in
"wordcount")
# Wortanzahl zählen
result=$(wc -w < "$file")
;;
"lines")
# Zeilenanzahl zählen
result=$(wc -l < "$file")
;;
"complexity")
# Textkomplexität simulieren (in einer realen Anwendung: komplexere Analyse)
# Zähle Wörter mit mehr als 6 Buchstaben
result=$(grep -o '\b[A-Za-z]\{7,\}\b' "$file" | wc -l)
;;
"unique_words")
# Anzahl eindeutiger Wörter zählen
result=$(tr -s '[:space:]' '\n' < "$file" | sort | uniq | wc -l)
;;
*)
echo "Fehler: Unbekannter Analysetyp: $analysis_type" >&2
return 1
;;
esac
# Ergebnis in beide Cache-Ebenen speichern
echo "$result" > "$cache_file"
mem_cache[$cache_key]="$result"
echo "$result"
echo "Quelle: Frische Analyse" >&2
return 0
}
# Funktion zum Löschen des Caches
clear_cache() {
local type="$1"
case "$type" in
"memory")
# Nur In-Memory-Cache löschen
mem_cache=()
echo "In-Memory-Cache gelöscht" >&2
;;
"disk")
# Nur Dateisystem-Cache löschen
rm -f "$MEMO_DIR"/*
echo "Dateisystem-Cache gelöscht" >&2
;;
"all"|*)
# Beide Cache-Typen löschen
mem_cache=()
rm -f "$MEMO_DIR"/*
echo "Vollständiger Cache gelöscht" >&2
;;
esac
}
# Analysiere mehrere Dateien
batch_analyze() {
local analysis_type="$1"
shift
for file in "$@"; do
echo "===== Analyse von $file ====="
local result=$(analyze_file "$file" "$analysis_type")
echo "$file: $result"
done
}
# Hauptfunktion
main() {
if [[ "$1" == "--clear-cache" ]]; then
clear_cache "${2:-all}"
exit 0
fi
if [[ "$1" == "--batch" ]]; then
local analysis_type="$2"
shift 2
batch_analyze "$analysis_type" "$@"
exit 0
fi
if [[ $# -lt 2 ]]; then
echo "Verwendung: $0 <datei> <analysetyp>" >&2
echo " $0 --clear-cache [memory|disk|all]" >&2
echo " $0 --batch <analysetyp> <datei1> <datei2> ..." >&2
echo "Analysetypen: wordcount, lines, complexity, unique_words" >&2
exit 1
fi
local file="$1"
local analysis_type="$2"
analyze_file "$file" "$analysis_type"
}
# Skript ausführen
main "$@"Caching und Memoization sind leistungsstarke Techniken zur Performance-Optimierung in Shell-Skripten:
Durch die Implementierung dieser Caching-Techniken können Shell-Skripte erheblich beschleunigt werden, insbesondere wenn sie rechenintensive Operationen durchführen, auf externe Ressourcen zugreifen oder große Datenmengen verarbeiten. Der Schlüssel zum erfolgreichen Caching liegt in der richtigen Balance zwischen Cache-Komplexität, Speicherverbrauch und der Sicherstellung der Datenaktualität.
In Shell-Skripten gibt es zahlreiche Konstrukte und Muster, die besonders ressourcenintensiv sein können. Diese beeinträchtigen nicht nur die Performance, sondern verbrauchen auch unnötig Systemressourcen. In diesem Abschnitt werden alternative Ansätze vorgestellt, die effizienter und ressourcenschonender sind.
Bevor wir Alternativen betrachten, ist es wichtig zu verstehen, welche Shell-Konstrukte besonders ressourcenintensiv sind:
while read-Schleifen mit PipelinesKomplexe Pipelines erzeugen mehrere Prozesse, was Performance-Kosten verursacht:
#!/bin/bash
# Ineffizient: Komplexe Pipeline mit vielen Komponenten
cat logfile.txt | grep "ERROR" | cut -d':' -f2- | sort | uniq -c | sort -nr > error_summary.txt#!/bin/bash
# Effizienter: Ein einzelner awk-Prozess
awk '/ERROR/ {
sub(/^.*ERROR:/, "");
counts[$0]++;
}
END {
for (error in counts)
print counts[error], error;
}' logfile.txt | sort -nr > error_summary.txt#!/bin/bash
# Effizienter: Reduzierung der Pipeline-Komponenten
grep "ERROR" logfile.txt | awk -F':' '{print $2}' | sort | uniq -c | sort -nr > error_summary.txtDie Erzeugung von Prozessen in Schleifen ist besonders teuer:
#!/bin/bash
# Ineffizient: Prozesserzeugung in jeder Schleifeniteration
for file in *.txt; do
wordcount=$(wc -w < "$file")
echo "$file: $wordcount Wörter"
done#!/bin/bash
# Effizienter: Batchverarbeitung aller Dateien auf einmal
wc -w *.txt | while read count file; do
if [[ "$file" != "total" ]]; then
echo "$file: $count Wörter"
fi
done#!/bin/bash
# Effizienter: Interne Shell-Funktionalität verwenden
count_words() {
local file="$1"
local count=0
local word
# Datei in ein Array einlesen
mapfile -t words < <(cat "$file" | tr '[:space:]' '\n' | grep -v '^$')
echo "${#words[@]}"
}
for file in *.txt; do
wordcount=$(count_words "$file")
echo "$file: $wordcount Wörter"
doneTextverarbeitung mit sed, grep,
awk usw. in Schleifen kann ineffizient sein:
#!/bin/bash
# Ineffizient: Wiederholte externe Prozesse für Textmanipulation
for line in $(cat file.txt); do
# Extrahiere die ersten 3 Zeichen
prefix=$(echo "$line" | cut -c1-3)
# Konvertiere in Großbuchstaben
upper_prefix=$(echo "$prefix" | tr '[:lower:]' '[:upper:]')
echo "$upper_prefix: $line"
done#!/bin/bash
# Effizienter: Bash-interne Stringmanipulation
while IFS= read -r line; do
# Extrahiere die ersten 3 Zeichen
prefix="${line:0:3}"
# Konvertiere in Großbuchstaben (Bash 4+)
upper_prefix="${prefix^^}"
echo "$upper_prefix: $line"
done < file.txt#!/bin/bash
# Effizienter: Einmaliger Aufruf eines Tools wie awk
awk '{
prefix = substr($0, 1, 3);
gsub(/[a-z]/, toupper, prefix);
print prefix ": " $0;
}' file.txtJede Subshell ($(...) oder Backticks) erzeugt einen
neuen Prozess:
#!/bin/bash
# Ineffizient: Übermäßiger Einsatz von Subshells
for file in *.log; do
timestamp=$(date +%Y%m%d-%H%M%S)
size=$(du -h "$file" | cut -f1)
owner=$(stat -c "%U" "$file")
echo "[$timestamp] $file ($size) owned by $owner"
done#!/bin/bash
# Effizienter: Reduzierung von Subshell-Aufrufen
timestamp=$(date +%Y%m%d-%H%M%S) # Nur einmal aufrufen
for file in *.log; do
read size _ < <(du -h "$file")
owner=$(stat -c "%U" "$file")
echo "[$timestamp] $file ($size) owned by $owner"
done#!/bin/bash
# Effizienter: Verarbeitung in Stapeln
declare -a files=( *.log )
# Nur einen Zeitstempel für alle Dateien verwenden
timestamp=$(date +%Y%m%d-%H%M%S)
# Informationen in einem Durchgang sammeln
owner_info=$(stat -c "%n %U" "${files[@]}")
size_info=$(du -h "${files[@]}")
# Verarbeiten und Ausgeben
while IFS= read -r file_info; do
file=$(echo "$file_info" | awk '{print $2}')
size=$(echo "$size_info" | grep "$file" | awk '{print $1}')
owner=$(echo "$owner_info" | grep "$file" | awk '{print $2}')
echo "[$timestamp] $file ($size) owned by $owner"
done < <(ls -l "${files[@]}" | tail -n+2)Ineffiziente Dateiverarbeitung kann die Performance stark beeinträchtigen:
#!/bin/bash
# Ineffizient: Wiederholtes Öffnen und Schließen von Dateien
for i in {1..100}; do
echo "Zeile $i" >> output.txt
done#!/bin/bash
# Effizienter: Einmaliges Öffnen mit Dateideskriptor
exec 3> output.txt
for i in {1..100}; do
echo "Zeile $i" >&3
done
exec 3>&- # Deskriptor schließen#!/bin/bash
# Effizienter: Einmaliges Schreiben des kompletten Inhalts
{
for i in {1..100}; do
echo "Zeile $i"
done
} > output.txtwhile read-Schleifen mit
PipelinesEine häufige ineffiziente Struktur ist die
while read-Schleife als Teil einer Pipeline:
#!/bin/bash
# Ineffizient: while-read mit Pipe
cat large_file.txt | while read -r line; do
# Verarbeitung...
echo "Processed: $line"
doneDiese Struktur erzeugt eine Subshell für die
while-Schleife, wodurch Variablen außerhalb der Schleife
nicht zugänglich sind.
#!/bin/bash
# Effizienter: Direkte Umleitung
while read -r line; do
# Verarbeitung...
echo "Processed: $line"
done < large_file.txt#!/bin/bash
# Effizient: Process Substitution für komplexere Szenarien
while read -r line; do
# Verarbeitung...
echo "Processed: $line"
count=$((count + 1)) # Zählvariable bleibt erhalten
done < <(grep "pattern" large_file.txt)
echo "Processed $count lines" # Variable ist zugänglichDie Zahlenverarbeitung in Bash kann ineffizient sein, besonders bei komplexen Berechnungen:
#!/bin/bash
# Ineffizient: Externe Prozesse für einfache Berechnungen
sum=0
for i in {1..1000}; do
square=$(echo "$i * $i" | bc)
sum=$(echo "$sum + $square" | bc)
done
echo "Sum of squares: $sum"#!/bin/bash
# Effizienter: Bash-interne Arithmetik
sum=0
for ((i=1; i<=1000; i++)); do
((square = i * i))
((sum += square))
done
echo "Sum of squares: $sum"#!/bin/bash
# Effizienter: Einmaliger Aufruf von awk für komplexe Berechnungen
sum=$(awk 'BEGIN {
sum = 0;
for (i = 1; i <= 1000; i++) {
sum += i * i;
}
print sum;
}')
echo "Sum of squares: $sum"Manchmal ist das beste Mittel zur Performance-Verbesserung die Wahl eines geeigneteren Tools:
awk statt mehrerer spezialisierter Tools#!/bin/bash
# Ineffizient: Mehrere spezialisierte Tools
grep "pattern" log.txt | cut -d':' -f2 | sort | uniq -c
# Effizienter: Ein einzelner awk-Prozess
awk '/pattern/ {
count[substr($0, index($0, ":")+1)]++;
}
END {
for (item in count)
print count[item], item;
}' log.txt | sort -nrfind mit -exec statt Schleife#!/bin/bash
# Ineffizient: Schleife über find-Ergebnisse
for file in $(find /path -type f -name "*.log"); do
grep "ERROR" "$file"
done
# Effizienter: find mit -exec
find /path -type f -name "*.log" -exec grep -l "ERROR" {} \;
# Noch effizienter: find mit -exec und +
find /path -type f -name "*.log" -exec grep -l "ERROR" {} \+jq statt Textmanipulation#!/bin/bash
# Ineffizient: JSON mit textbasierten Tools verarbeiten
grep '"name":' data.json | sed 's/.*"name": "\(.*\)".*/\1/'
# Effizienter: jq für JSON-Verarbeitung
jq -r '.name' data.jsonxmlstarlet statt komplexer Text-Tools#!/bin/bash
# Ineffizient: XML mit grep/sed verarbeiten
grep "<title>" book.xml | sed 's/<title>\(.*\)<\/title>/\1/'
# Effizienter: xmlstarlet für XML-Verarbeitung
xmlstarlet sel -t -v "//title" book.xmlFür bestimmte rechenintensive Aufgaben können andere Sprachen effizienter sein:
#!/bin/bash
# Ineffizient: Komplexe Datenverarbeitung in Bash
# [Komplexes Bash-Skript mit vielen Schleifen und Berechnungen]
# Effizienter: Python-Skript aufrufen
python3 - <<EOF
import sys
# Hier komplexe Berechnungen durchführen
data = []
for i in range(1, 1001):
data.append(i ** 2)
# Ergebnisse ausgeben
print(f"Summe: {sum(data)}")
print(f"Durchschnitt: {sum(data)/len(data)}")
print(f"Maximum: {max(data)}")
EOF#!/bin/bash
# Ineffizient: Komplexe Textverarbeitung in Bash
# [Komplexes Bash-Skript mit vielen sed/grep/cut]
# Effizienter: Awk-Skript
awk '
BEGIN {
FS = ",";
}
{
if ($1 ~ /^#/ || NF < 3) next; # Kommentare und ungültige Zeilen überspringen
# Daten verarbeiten und aggregieren
sum[$2] += $3;
count[$2]++;
}
END {
print "Kategorie,Summe,Durchschnitt";
for (cat in sum) {
printf "%s,%d,%.2f\n", cat, sum[cat], sum[cat]/count[cat];
}
}' data.csv > summary.csvFür extrem rechenintensive Aufgaben kann ein kompiliertes C-Programm die beste Lösung sein:
#!/bin/bash
# Beispiel: C-Programm compilieren und ausführen
cat > processor.c <<EOF
#include <stdio.h>
#include <stdlib.h>
int main(int argc, char **argv) {
if (argc < 2) {
fprintf(stderr, "Verwendung: %s <datei>\n", argv[0]);
return 1;
}
FILE *file = fopen(argv[1], "r");
if (!file) {
perror("Fehler beim Öffnen der Datei");
return 1;
}
// Hier hochperformante Verarbeitung
fclose(file);
return 0;
}
EOF
# Kompilieren und ausführen
gcc -O3 -o processor processor.c
./processor large_data.txtOft ist ein Hybrid-Ansatz am effizientesten, bei dem Shell-Skripting für Koordination und spezialisierte Tools für Datenverarbeitung verwendet werden:
#!/bin/bash
# Beispiel eines Hybrid-Ansatzes
# 1. Dateierfassung mit Shell (gute Dateisystemintegration)
log_files=$(find /var/log -name "*.log" -mtime -1)
# 2. Datenextraktion mit grep und awk (effiziente Textverarbeitung)
echo "Extrahiere relevante Daten..."
for log in $log_files; do
grep "ERROR" "$log" >> error_lines.tmp
done
# 3. Komplexe Datenanalyse mit Python (leistungsfähige Datenverarbeitung)
echo "Analysiere Fehler..."
python3 - <<EOF
import re
from collections import Counter
# Fehler parsen und zählen
pattern = re.compile(r'ERROR: (.*?) in')
errors = []
with open('error_lines.tmp', 'r') as f:
for line in f:
match = pattern.search(line)
if match:
errors.append(match.group(1))
# Häufigste Fehler zählen
counts = Counter(errors)
print("\nTop 5 Fehler:")
for error, count in counts.most_common(5):
print(f"{count:4d}: {error}")
EOF
# 4. Aufräumen mit Shell
rm error_lines.tmp#!/bin/bash
# optimized_log_analyzer.sh - Effiziente Analyse von Log-Dateien
# Konfiguration
LOG_DIR="/var/log"
LOG_PATTERN="*.log"
OUTPUT_FILE="log_summary.txt"
TEMP_DIR=$(mktemp -d)
trap 'rm -rf "$TEMP_DIR"' EXIT
echo "Analysiere Log-Dateien in $LOG_DIR..."
# Ineffizient wäre:
#
# find "$LOG_DIR" -name "$LOG_PATTERN" | while read -r log_file; do
# grep "ERROR" "$log_file" >> errors.txt
# grep "WARNING" "$log_file" >> warnings.txt
# grep "INFO" "$log_file" >> info.txt
# done
#
# cat errors.txt | sort | uniq -c | sort -nr > error_summary.txt
# ...
# Effiziente Implementierung:
# 1. Eine einzige find-Operation mit paralleler Verarbeitung
find "$LOG_DIR" -name "$LOG_PATTERN" -type f -print0 | xargs -0 -P "$(nproc)" grep -l "ERROR\|WARNING\|INFO" > "$TEMP_DIR/relevant_logs.txt"
# 2. Awk für effiziente einmalige Durchsuchung jeder Datei
while IFS= read -r log_file; do
echo "Verarbeite: $log_file"
awk '
/ERROR/ { errors[substr($0, index($0, "ERROR") + 6)]++ }
/WARNING/ { warnings[substr($0, index($0, "WARNING") + 8)]++ }
/INFO/ { info_count++ }
END {
# Fehler in temporäre Datei schreiben
for (msg in errors)
printf "%d\t%s\n", errors[msg], msg > "'$TEMP_DIR'/errors.tsv";
# Warnungen in temporäre Datei schreiben
for (msg in warnings)
printf "%d\t%s\n", warnings[msg], msg > "'$TEMP_DIR'/warnings.tsv";
# Anzahl der Info-Meldungen schreiben
printf "%d\n", info_count > "'$TEMP_DIR'/info_count.txt";
}' "$log_file"
done < "$TEMP_DIR/relevant_logs.txt"
# 3. Zusammenfassung generieren (effiziente Sortierung und Aggregation)
{
echo "=== LOG-ANALYSE ZUSAMMENFASSUNG ==="
echo "Generiert am: $(date)"
echo ""
# Info-Zählung
total_info=$(awk '{sum += $1} END {print sum}' "$TEMP_DIR/info_count.txt")
echo "Gesamt INFO-Meldungen: $total_info"
echo ""
# Top-Fehler
echo "=== TOP 10 FEHLER ==="
if [[ -f "$TEMP_DIR/errors.tsv" ]]; then
sort -nr "$TEMP_DIR/errors.tsv" | head -10 |
while IFS=$'\t' read -r count message; do
printf "%4d: %s\n" "$count" "$message"
done
else
echo "Keine Fehler gefunden."
fi
echo ""
# Top-Warnungen
echo "=== TOP 10 WARNUNGEN ==="
if [[ -f "$TEMP_DIR/warnings.tsv" ]]; then
sort -nr "$TEMP_DIR/warnings.tsv" | head -10 |
while IFS=$'\t' read -r count message; do
printf "%4d: %s\n" "$count" "$message"
done
else
echo "Keine Warnungen gefunden."
fi
} > "$OUTPUT_FILE"
echo "Analyse abgeschlossen. Ergebnisse wurden in $OUTPUT_FILE gespeichert."#!/bin/bash
# optimized_disk_usage.sh - Effiziente Ermittlung der Dateisystemstatistik
# Konfiguration
TARGET_DIR="${1:-$PWD}"
TEMP_DIR=$(mktemp -d)
OUTPUT_FILE="disk_usage_report.txt"
MIN_SIZE=${2:-1048576} # Standard: 1 MB (1024*1024 Bytes)
trap 'rm -rf "$TEMP_DIR"' EXIT
if [[ ! -d "$TARGET_DIR" ]]; then
echo "Fehler: Verzeichnis '$TARGET_DIR' existiert nicht." >&2
exit 1
fi
echo "Analysiere Verzeichnis: $TARGET_DIR"
echo "Minimale Dateigröße für die Analyse: $(numfmt --to=iec-i --suffix=B --format="%.2f" "$MIN_SIZE")"
# Ineffizient wäre:
#
# find "$TARGET_DIR" -type f -size +"$MIN_SIZE"c | while read -r file; do
# size=$(du -b "$file" | cut -f1)
# extension="${file##*.}"
# echo "$size $extension" >> "$TEMP_DIR/sizes.txt"
# done
#
# Verarbeitung der Ergebnisse...
# Effiziente Implementierung:
# 1. Parallele Erfassung aller großen Dateien mit einer einzigen find-Operation
echo "Erfasse Dateien..."
find "$TARGET_DIR" -type f -size +"$MIN_SIZE"c -printf "%s\t%p\n" > "$TEMP_DIR/all_files.tsv"
# 2. Extraktion der Dateiendungen mit awk in einem einzigen Durchlauf
echo "Analysiere Dateitypen..."
awk -F'\t' '{
# Dateiname extrahieren
file = $2;
# Größe in Bytes
size = $1;
# Dateiendung ermitteln (oder "ohne" falls keine vorhanden)
if (match(file, /\.([^\/\.]+)$/)) {
ext = tolower(substr(file, RSTART+1, RLENGTH-1));
} else {
ext = "ohne";
}
# Statistiken sammeln
total_bytes += size;
type_count[ext]++;
type_bytes[ext] += size;
}
END {
# Gesamtstatistik schreiben
printf "%.0f\n", total_bytes > "'$TEMP_DIR'/total_bytes.txt";
# Statistik nach Typ schreiben
for (ext in type_count) {
printf "%s\t%d\t%.0f\n", ext, type_count[ext], type_bytes[ext] > "'$TEMP_DIR'/type_stats.tsv";
}
}' "$TEMP_DIR/all_files.tsv"
# 3. Generierung des Berichts
{
echo "=== DATEISYSTEM-NUTZUNGSSTATISTIK ==="
echo "Verzeichnis: $TARGET_DIR"
echo "Generiert am: $(date)"
echo "Minimale Dateigröße: $(numfmt --to=iec-i --suffix=B --format="%.2f" "$MIN_SIZE")"
echo ""
# Gesamtstatistik
total_bytes=$(cat "$TEMP_DIR/total_bytes.txt")
total_files=$(wc -l < "$TEMP_DIR/all_files.tsv")
echo "Gesamt: $total_files Dateien, $(numfmt --to=iec-i --suffix=B --format="%.2f" "$total_bytes")"
echo ""
# Top 10 größte Dateien
echo "=== TOP 10 GRÖSSTE DATEIEN ==="
sort -nr "$TEMP_DIR/all_files.tsv" | head -10 |
while IFS=$'\t' read -r size file; do
# Formatierte Größe
human_size=$(numfmt --to=iec-i --suffix=B --format="%.2f" "$size")
printf "%-10s: %s\n" "$human_size" "$file"
done
echo ""
# Statistik nach Dateityp
echo "=== STATISTIK NACH DATEITYP ==="
sort -k3,3nr "$TEMP_DIR/type_stats.tsv" |
while IFS=$'\t' read -r ext count bytes; do
# Formatierte Größe
human_bytes=$(numfmt --to=iec-i --suffix=B --format="%.2f" "$bytes")
percent=$(echo "scale=2; $bytes * 100 / $total_bytes" | bc)
printf "%-10s: %5d Dateien, %10s (%5.2f%%)\n" ".$ext" "$count" "$human_bytes" "$percent"
done
} > "$OUTPUT_FILE"
echo "Analyse abgeschlossen. Ergebnisse wurden in $OUTPUT_FILE gespeichert."Die Vermeidung ressourcenintensiver Shell-Konstrukte kann die Performance von Skripten erheblich verbessern:
awk
oder perl<(...))Durch die Implementierung dieser Alternativen können Shell-Skripte erheblich beschleunigt und ihr Ressourcenverbrauch minimiert werden. Die Entscheidung für die richtige Alternative sollte auf Basis der spezifischen Anforderungen, der Datenmenge und der Häufigkeit der Skriptausführung getroffen werden.
In modernen IT-Umgebungen spielen DevOps-Pipelines eine zentrale Rolle bei der kontinuierlichen Integration und Bereitstellung von Software. Shell-Skripte können als flexible Bausteine in diesen Pipelines dienen und tragen wesentlich zur Automatisierung von Entwicklungs-, Test- und Deployment-Prozessen bei.
DevOps-Pipelines bestehen typischerweise aus mehreren Phasen wie Code-Checkout, Build, Test, Paketierung und Deployment. Shell-Skripte können in jeder dieser Phasen eingesetzt werden, um spezifische Aufgaben zu automatisieren:
#!/bin/bash
# build-and-test.sh - Ein einfaches Beispiel für ein CI-Pipeline-Skript
set -e # Beendet das Skript bei Fehlern
echo "Phase 1: Abhängigkeiten installieren"
npm install
echo "Phase 2: Linting durchführen"
npm run lint
echo "Phase 3: Tests ausführen"
npm test
echo "Phase 4: Build erstellen"
npm run build
echo "Alle Phasen erfolgreich abgeschlossen!"Der Befehl set -e ist besonders nützlich in
Pipeline-Skripten, da er sicherstellt, dass die Ausführung bei einem
Fehler sofort stoppt, was die Früherkennung von Problemen
ermöglicht.
In Jenkins können Shell-Skripte direkt in der Pipeline-Definition verwendet werden:
pipeline {
agent any
stages {
stage('Build') {
steps {
sh './scripts/build.sh'
}
}
stage('Test') {
steps {
sh './scripts/run-tests.sh'
}
}
stage('Deploy') {
steps {
sh './scripts/deploy.sh ${ENVIRONMENT}'
}
}
}
}Bei GitLab CI/CD werden Shell-Skripte in der
.gitlab-ci.yml-Datei definiert:
stages:
- build
- test
- deploy
build_job:
stage: build
script:
- ./scripts/build.sh
test_job:
stage: test
script:
- ./scripts/run-tests.sh
deploy_job:
stage: deploy
script:
- ./scripts/deploy.sh $CI_ENVIRONMENT_NAME
only:
- mainGitHub Actions verwendet ebenfalls YAML-Dateien zur Definition von Workflows:
name: CI/CD Pipeline
on:
push:
branches: [ main ]
pull_request:
branches: [ main ]
jobs:
build-and-test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Build und Test
run: |
chmod +x ./scripts/build.sh
./scripts/build.sh
./scripts/run-tests.shUm Shell-Skripte flexibel in verschiedenen Umgebungen einsetzen zu können, ist eine sorgfältige Parametrisierung wichtig:
#!/bin/bash
# deploy.sh - Deployment-Skript mit Umgebungsparametern
# Parameter überprüfen
if [ $# -lt 1 ]; then
echo "Verwendung: $0 <umgebung> [version]"
exit 1
fi
ENVIRONMENT=$1
VERSION=${2:-latest} # Standardwert 'latest', falls nicht angegeben
# Umgebungsspezifische Konfiguration laden
CONFIG_FILE="./config/${ENVIRONMENT}.conf"
if [ ! -f "$CONFIG_FILE" ]; then
echo "Fehler: Konfigurationsdatei für Umgebung '$ENVIRONMENT' nicht gefunden"
exit 1
fi
source "$CONFIG_FILE"
echo "Starte Deployment für Umgebung: $ENVIRONMENT, Version: $VERSION"
echo "Verwende Server: $SERVER_HOST, Port: $SERVER_PORT"
# Hier folgt die eigentliche Deployment-Logik
# ...In DevOps-Pipelines ist der sichere Umgang mit Zugangsdaten und anderen sensiblen Informationen essenziell. Moderne CI/CD-Systeme bieten dafür spezielle Mechanismen:
#!/bin/bash
# secure-deploy.sh - Sicheres Deployment mit Umgebungsvariablen für Secrets
# Überprüfen, ob die benötigten Zugangsdaten als Umgebungsvariablen verfügbar sind
if [ -z "$DB_PASSWORD" ] || [ -z "$API_KEY" ]; then
echo "Fehler: Benötigte Zugangsdaten fehlen"
exit 1
fi
# Verwendung der Secrets in der Anwendungskonfiguration
sed -i "s/DB_PASSWORD_PLACEHOLDER/$DB_PASSWORD/g" ./config/application.properties
sed -i "s/API_KEY_PLACEHOLDER/$API_KEY/g" ./config/application.properties
echo "Konfiguration aktualisiert, starte Deployment..."Anstatt Secrets direkt im Skript zu speichern, werden sie als Umgebungsvariablen über das CI/CD-System bereitgestellt:
Für komplexe Pipelines kann die Parallelisierung von Aufgaben die Ausführungszeit erheblich reduzieren:
#!/bin/bash
# parallel-tests.sh - Parallele Ausführung von Tests
# Parallele Ausführung von Frontend- und Backend-Tests
echo "Starte Tests parallel..."
(cd frontend && npm test) &
FRONTEND_PID=$!
(cd backend && ./gradlew test) &
BACKEND_PID=$!
# Warten auf Abschluss beider Prozesse
wait $FRONTEND_PID
FRONTEND_STATUS=$?
wait $BACKEND_PID
BACKEND_STATUS=$?
# Überprüfen der Ergebnisse
if [ $FRONTEND_STATUS -ne 0 ] || [ $BACKEND_STATUS -ne 0 ]; then
echo "Tests fehlgeschlagen!"
exit 1
else
echo "Alle Tests erfolgreich!"
fiDas Caching von Abhängigkeiten und Build-Artefakten kann die Ausführungszeit von Pipelines signifikant verkürzen:
#!/bin/bash
# cached-build.sh - Build mit Dependency-Caching
CACHE_DIR="./.cache/dependencies"
CHECKSUM_FILE="./package-lock.json"
CHECKSUM=$(md5sum $CHECKSUM_FILE | cut -d ' ' -f 1)
CACHE_VALID=0
# Überprüfen, ob ein gültiger Cache existiert
if [ -d "$CACHE_DIR" ] && [ -f "$CACHE_DIR/checksum" ]; then
CACHED_CHECKSUM=$(cat "$CACHE_DIR/checksum")
if [ "$CHECKSUM" == "$CACHED_CHECKSUM" ]; then
CACHE_VALID=1
echo "Dependency-Cache ist gültig, überspringe Installation..."
fi
fi
# Dependencies installieren oder aus dem Cache wiederherstellen
if [ $CACHE_VALID -eq 0 ]; then
echo "Cache ungültig oder nicht vorhanden, installiere Dependencies..."
rm -rf "$CACHE_DIR"
mkdir -p "$CACHE_DIR"
npm ci
# Node_modules in den Cache kopieren
cp -r ./node_modules "$CACHE_DIR/"
echo "$CHECKSUM" > "$CACHE_DIR/checksum"
else
# Dependencies aus dem Cache wiederherstellen
cp -r "$CACHE_DIR/node_modules" ./
fi
# Build ausführen
npm run buildIn DevOps-Pipelines ist robuste Fehlerbehandlung entscheidend. Shell-Skripte sollten Fehler erkennen, protokollieren und angemessen darauf reagieren:
#!/bin/bash
# monitored-deploy.sh - Deployment mit Monitoring und Fehlerbehandlung
# Logging-Funktion
log() {
echo "[$(date '+%Y-%m-%d %H:%M:%S')] $1"
}
# Fehlerbehandlungsfunktion
handle_error() {
local exit_code=$1
local error_message=${2:-"Ein unbekannter Fehler ist aufgetreten"}
log "FEHLER: $error_message (Exit-Code: $exit_code)"
# Benachrichtigung senden (z.B. per Slack oder E-Mail)
if [ -n "$SLACK_WEBHOOK_URL" ]; then
curl -s -X POST -H 'Content-type: application/json' \
--data "{\"text\":\"Deployment fehlgeschlagen: $error_message\"}" \
"$SLACK_WEBHOOK_URL"
fi
# Rollback durchführen, falls möglich
if [ -d "./backup" ]; then
log "Führe Rollback durch..."
# Rollback-Logik hier
fi
exit $exit_code
}
# Trap für Fehlerbehandlung einrichten
trap 'handle_error $?' ERR
# Hauptfunktionen
backup() {
log "Erstelle Backup der aktuellen Anwendung..."
mkdir -p ./backup
cp -r ./current/* ./backup/
}
deploy() {
log "Starte Deployment..."
# Deployment-Logik hier
# ...
log "Deployment abgeschlossen"
}
verify() {
log "Überprüfe Anwendungsstatus..."
# Healthcheck-Logik hier
# ...
if [ $? -ne 0 ]; then
handle_error 1 "Anwendung reagiert nicht nach Deployment"
fi
log "Anwendung läuft korrekt"
}
# Hauptscript
backup
deploy
verifyIdempotenz: Skripte sollten bei mehrfacher Ausführung das gleiche Ergebnis liefern und keine unerwünschten Nebeneffekte verursachen.
Atomarität: Entweder wird die gesamte Operation erfolgreich durchgeführt, oder sie schlägt vollständig fehl, ohne das System in einem inkonsistenten Zustand zu hinterlassen.
Selbstdokumentation: Skripte sollten gut dokumentiert sein, mit klaren Kommentaren und aussagekräftigen Variablennamen.
Versionierung: Pipeline-Skripte sollten wie anderer Code versioniert werden, idealerweise im selben Repository wie die Anwendung.
Testbarkeit: Skripte sollten in isolierten Umgebungen testbar sein, bevor sie in Produktionspipelines eingesetzt werden.
Shell-Skripte können auch zur Automatisierung von Infrastrukturaufgaben in DevOps-Pipelines verwendet werden:
#!/bin/bash
# provision-environment.sh - Infrastruktur mit Terraform bereitstellen
set -e
# Umgebung aus Parameter
ENVIRONMENT=${1:-dev}
# Terraform-Konfiguration initialisieren
cd "./terraform/$ENVIRONMENT"
terraform init
# Plan erstellen und anwenden
terraform plan -out=tfplan
terraform apply -auto-approve tfplan
# Ausgabewerte extrahieren
SERVER_IP=$(terraform output -raw server_ip)
DB_ENDPOINT=$(terraform output -raw database_endpoint)
# Konfiguration für Anwendung erstellen
cat > ../../config/environment.conf <<EOF
# Automatisch generierte Konfiguration für $ENVIRONMENT
SERVER_HOST=$SERVER_IP
DB_CONNECTION_STRING=jdbc:postgresql://$DB_ENDPOINT/appdb
EOF
echo "Umgebung $ENVIRONMENT wurde erfolgreich bereitgestellt"Die Stärke der Shell-Programmierung liegt nicht nur in ihrer eigenen Leistungsfähigkeit, sondern auch in ihrer Fähigkeit, nahtlos mit anderen Programmiersprachen und Tools zu interagieren. Diese Interoperabilität macht Shell-Skripte zu einem idealen “Klebstoff”, der verschiedene Komponenten in komplexen Systemen verbindet und koordiniert.
Python ist aufgrund seiner umfangreichen Bibliotheken und Datenverarbeitungsfähigkeiten eine häufige Ergänzung zu Shell-Skripten:
#!/bin/bash
# python-integration.sh - Kombination von Bash und Python
echo "Starte Datenanalyse..."
# Python-Skript mit Parametern aufrufen
python3 ./analytics/process_data.py --input-file="$1" --output-format=json
# Prüfen, ob das Python-Skript erfolgreich war
if [ $? -ne 0 ]; then
echo "Fehler bei der Datenanalyse!" >&2
exit 1
fi
echo "Datenanalyse abgeschlossen, verarbeite Ergebnisse..."
# Mit den Ergebnissen weiterarbeiten
jq '.summary' ./analytics/results.json > ./report.txtDas Python-Skript könnte so aussehen:
#!/usr/bin/env python3
# process_data.py - Datenverarbeitungslogik
import argparse
import pandas as pd
import json
def main():
parser = argparse.ArgumentParser(description='Datenanalyse-Tool')
parser.add_argument('--input-file', required=True, help='Pfad zur Eingabedatei')
parser.add_argument('--output-format', default='json', help='Ausgabeformat')
args = parser.parse_args()
# Daten laden und verarbeiten
data = pd.read_csv(args.input_file)
result = data.describe().to_dict()
# Ergebnisse speichern
with open('./analytics/results.json', 'w') as f:
json.dump({
'summary': result,
'metadata': {'source': args.input_file, 'format': args.output_format}
}, f, indent=2)
if __name__ == '__main__':
main()Für webbasierte Aufgaben oder JavaScript-Bibliotheken kann Node.js eingebunden werden:
#!/bin/bash
# node-integration.sh - Integration von Node.js in Shell-Skripte
# JSON-Datei für Node.js vorbereiten
cat > ./temp/config.json <<EOF
{
"apiEndpoint": "$API_ENDPOINT",
"apiKey": "$API_KEY",
"parameters": {
"limit": $MAX_ITEMS,
"format": "$OUTPUT_FORMAT"
}
}
EOF
# Node.js-Skript ausführen
echo "Starte API-Abfrage mit Node.js..."
node ./scripts/api-fetch.js ./temp/config.json
# Ergebnisse weiterverarbeiten
if [ -f "./temp/api-results.csv" ]; then
echo "Daten erfolgreich abgerufen, starte Verarbeitung..."
awk -F, '{sum+=$3} END {print "Gesamtsumme:", sum}' ./temp/api-results.csv
else
echo "Fehler: Keine Ergebnisdaten gefunden!" >&2
exit 1
fiDas entsprechende Node.js-Skript:
// api-fetch.js - API-Daten mit Node.js abrufen
const fs = require('fs');
const axios = require('axios');
const { Parser } = require('json2csv');
async function fetchData() {
// Konfiguration aus Befehlszeilenargument laden
const configFile = process.argv[2];
const config = JSON.parse(fs.readFileSync(configFile, 'utf8'));
try {
// API-Anfrage senden
const response = await axios.get(config.apiEndpoint, {
headers: { 'Authorization': `Bearer ${config.apiKey}` },
params: config.parameters
});
// Daten in CSV umwandeln
const parser = new Parser();
const csv = parser.parse(response.data.items);
// Ergebnisse speichern
fs.writeFileSync('./temp/api-results.csv', csv);
console.log('Daten erfolgreich abgerufen und gespeichert');
} catch (error) {
console.error('Fehler bei der API-Anfrage:', error.message);
process.exit(1);
}
}
fetchData();Die klassische UNIX-Philosophie der Komposition kleiner, spezialisierter Tools wird durch Pipes realisiert:
#!/bin/bash
# log-analysis.sh - Verknüpfung verschiedener Tools über Pipes
# Apache-Logs analysieren und Top-10-IP-Adressen extrahieren
echo "Analysiere Apache-Logs..."
cat /var/log/apache2/access.log | \
grep -v "127.0.0.1" | \
awk '{print $1}' | \
sort | \
uniq -c | \
sort -nr | \
head -10 > ./reports/top_ips.txt
# Python zur geografischen Zuordnung verwenden
echo "Bestimme geografische Herkunft der IP-Adressen..."
python3 ./scripts/geolocate.py ./reports/top_ips.txt
# Ergebnisse mit R visualisieren
echo "Erstelle Visualisierung..."
Rscript ./scripts/visualize.R ./reports/geo_data.csv ./reports/traffic_map.pdf
echo "Analyse abgeschlossen. Ergebnisse in ./reports/"Andere Sprachen können Shell-Befehle ausführen, um betriebssystemnahe Funktionen zu nutzen:
# Python-Skript mit eingebetteten Shell-Befehlen
import subprocess
import sys
def run_shell_command(command):
"""Shell-Befehl ausführen und Ausgabe zurückgeben"""
try:
result = subprocess.run(command, shell=True, check=True,
stdout=subprocess.PIPE, stderr=subprocess.PIPE,
text=True)
return result.stdout
except subprocess.CalledProcessError as e:
print(f"Fehler beim Ausführen des Befehls: {e}", file=sys.stderr)
print(f"Fehlerausgabe: {e.stderr}", file=sys.stderr)
sys.exit(1)
# Festplattennutzung ermitteln
disk_usage = run_shell_command("df -h / | tail -1 | awk '{print $5}'")
print(f"Aktuelle Festplattennutzung: {disk_usage.strip()}")
# Laufende Prozesse zählen
process_count = run_shell_command("ps aux | wc -l")
print(f"Anzahl laufender Prozesse: {int(process_count.strip()) - 1}")
# Netzwerkverbindungen analysieren
connections = run_shell_command("netstat -tuln | grep LISTEN | wc -l")
print(f"Offene Netzwerkports: {connections.strip()}")Shell-Skripte können wiederverwendbare Funktionen in separate Dateien auslagern:
# common-functions.sh - Gemeinsam genutzte Funktionen für mehrere Skripte
# Logging-Funktion
log() {
local level=$1
local message=$2
echo "[$(date '+%Y-%m-%d %H:%M:%S')] [$level] $message" | \
tee -a ./logs/application.log
}
# Konfiguration laden
load_config() {
local config_file=$1
if [ ! -f "$config_file" ]; then
log "ERROR" "Konfigurationsdatei nicht gefunden: $config_file"
return 1
fi
# Konfigurationsvariablen in die aktuelle Umgebung exportieren
while IFS='=' read -r key value; do
# Kommentare und leere Zeilen überspringen
if [[ ! $key =~ ^#.*$ && -n $key ]]; then
export "$key=$value"
log "DEBUG" "Konfiguration geladen: $key=$value"
fi
done < "$config_file"
return 0
}
# Ergebnisprüfung
check_result() {
local exit_code=$1
local operation=$2
if [ $exit_code -ne 0 ]; then
log "ERROR" "Operation fehlgeschlagen: $operation (Exit-Code: $exit_code)"
return 1
else
log "INFO" "Operation erfolgreich: $operation"
return 0
fi
}Verwendung in einem Haupt-Skript:
#!/bin/bash
# main-script.sh - Verwendet gemeinsam genutzte Funktionen
# Bibliothek einbinden
source ./lib/common-functions.sh
log "INFO" "Skriptausführung gestartet"
# Konfiguration laden
if ! load_config "./config/app.conf"; then
log "FATAL" "Konnte Konfiguration nicht laden"
exit 1
fi
# Externe Anwendung ausführen
log "INFO" "Starte Datenbankbackup"
pg_dump -U "$DB_USER" -h "$DB_HOST" "$DB_NAME" > ./backups/db_$(date +%Y%m%d).sql
check_result $? "Datenbankbackup" || exit 1
log "INFO" "Skriptausführung erfolgreich beendet"Shell-Skripte können mit curl oder wget auf
Webservices zugreifen:
#!/bin/bash
# weather-forecast.sh - Wetterdaten von einer API abrufen
API_KEY="your_api_key_here"
LOCATION=$1
if [ -z "$LOCATION" ]; then
echo "Verwendung: $0 <ort>" >&2
exit 1
fi
# API-Anfrage senden
echo "Rufe Wetterdaten für $LOCATION ab..."
response=$(curl -s "https://api.weatherapi.com/v1/forecast.json?key=$API_KEY&q=$LOCATION&days=3")
# Fehlerprüfung
if echo "$response" | jq -e '.error' > /dev/null; then
error_msg=$(echo "$response" | jq -r '.error.message')
echo "API-Fehler: $error_msg" >&2
exit 1
fi
# Daten extrahieren und formatieren
city=$(echo "$response" | jq -r '.location.name')
country=$(echo "$response" | jq -r '.location.country')
current_temp=$(echo "$response" | jq -r '.current.temp_c')
condition=$(echo "$response" | jq -r '.current.condition.text')
echo "Aktuelles Wetter in $city, $country:"
echo "Temperatur: ${current_temp}°C"
echo "Bedingungen: $condition"
# Vorhersage für die nächsten Tage
echo -e "\nVorhersage für die nächsten Tage:"
echo "$response" | jq -r '.forecast.forecastday[] | "Datum: \(.date) | Max: \(.day.maxtemp_c)°C | Min: \(.day.mintemp_c)°C | \(.day.condition.text)"'
# Daten für andere Anwendungen in JSON speichern
echo "$response" > ./data/weather_${LOCATION// /_}.json
echo "Vollständige Daten wurden in ./data/weather_${LOCATION// /_}.json gespeichert"Shell-Skripte können auch Benachrichtigungen an Slack, Discord oder andere Messaging-Dienste senden:
#!/bin/bash
# notify-slack.sh - Benachrichtigungen an Slack senden
# Konfiguration
WEBHOOK_URL="https://hooks.slack.com/services/TXXXXXXXX/BXXXXXXXX/XXXXXXXXXXXXXXXXXXXXXXXX"
CHANNEL="#monitoring"
USERNAME="Monitoring-Bot"
# Parameter prüfen
if [ $# -lt 2 ]; then
echo "Verwendung: $0 <status> <nachricht> [details]" >&2
exit 1
fi
STATUS=$1
MESSAGE=$2
DETAILS=${3:-"Keine weiteren Details"}
# Icon basierend auf Status festlegen
case $STATUS in
"success")
ICON=":white_check_mark:"
COLOR="#36a64f"
;;
"warning")
ICON=":warning:"
COLOR="#f2c744"
;;
"error")
ICON=":x:"
COLOR="#d00000"
;;
*)
ICON=":information_source:"
COLOR="#0066cc"
;;
esac
# JSON für Slack-Nachricht erstellen
payload=$(cat <<EOF
{
"channel": "$CHANNEL",
"username": "$USERNAME",
"icon_emoji": "$ICON",
"attachments": [
{
"color": "$COLOR",
"title": "$MESSAGE",
"text": "$DETAILS",
"footer": "Server: $(hostname) | Zeit: $(date '+%Y-%m-%d %H:%M:%S')"
}
]
}
EOF
)
# Nachricht an Slack senden
curl -s -X POST -H 'Content-type: application/json' --data "$payload" "$WEBHOOK_URL" > /dev/null
# Ausgabe im lokalen Log
echo "[$(date '+%Y-%m-%d %H:%M:%S')] Slack-Benachrichtigung gesendet: $STATUS - $MESSAGE"Shell-Skripte können Datenbankabfragen ausführen und die Ergebnisse verarbeiten:
#!/bin/bash
# db-report.sh - Datenbank abfragen und Bericht erstellen
# Datenbankkonfiguration
DB_USER="dbuser"
DB_PASS="dbpassword"
DB_NAME="appdb"
DB_HOST="localhost"
# Temporäre Dateien
TEMP_SQL="/tmp/query_$$.sql"
TEMP_CSV="/tmp/results_$$.csv"
# SQL-Abfrage erstellen
cat > "$TEMP_SQL" <<EOF
SELECT
department,
COUNT(*) as employee_count,
ROUND(AVG(salary), 2) as avg_salary,
MIN(hire_date) as earliest_hire,
MAX(hire_date) as latest_hire
FROM employees
GROUP BY department
ORDER BY employee_count DESC;
EOF
# Abfrage ausführen und Ergebnisse in CSV-Datei speichern
echo "Führe Datenbankabfrage aus..."
psql -h "$DB_HOST" -U "$DB_USER" -d "$DB_NAME" -A -F"," -f "$TEMP_SQL" > "$TEMP_CSV"
if [ $? -ne 0 ]; then
echo "Fehler bei der Datenbankabfrage!" >&2
rm -f "$TEMP_SQL" "$TEMP_CSV"
exit 1
fi
# CSV-Header und Daten trennen
header=$(head -1 "$TEMP_CSV")
data=$(tail -n +2 "$TEMP_CSV")
# Markdown-Bericht erstellen
report_file="./reports/department_report_$(date +%Y%m%d).md"
cat > "$report_file" <<EOF
# Abteilungsbericht vom $(date +%d.%m.%Y)
## Übersicht
Dieser Bericht zeigt eine Zusammenfassung der Mitarbeiterdaten nach Abteilungen.
## Daten
| $(echo "$header" | sed 's/,/ | /g') |
| $(echo "$header" | sed 's/[^,]*/---/g; s/,/ | /g') |
EOF
# Daten zum Bericht hinzufügen
echo "$data" | while IFS= read -r line; do
echo "| $(echo "$line" | sed 's/,/ | /g') |" >> "$report_file"
done
# Abschluss des Berichts
cat >> "$report_file" <<EOF
## Zusammenfassung
Der Bericht wurde automatisch generiert am $(date +%d.%m.%Y) um $(date +%H:%M) Uhr.
EOF
# Temporäre Dateien entfernen
rm -f "$TEMP_SQL" "$TEMP_CSV"
echo "Bericht wurde erstellt: $report_file"
# Optional: Bericht in PDF konvertieren
if command -v pandoc > /dev/null && command -v wkhtmltopdf > /dev/null; then
echo "Konvertiere in PDF..."
pdf_file="${report_file%.md}.pdf"
pandoc "$report_file" -o "$pdf_file"
echo "PDF erstellt: $pdf_file"
fiShell-Skripte können Docker-Container verwalten und mit ihnen interagieren:
#!/bin/bash
# docker-manager.sh - Docker-Container verwalten
# Befehl prüfen
if [ $# -lt 1 ]; then
echo "Verwendung: $0 <befehl> [parameter]" >&2
echo "Befehle: start, stop, status, logs, backup" >&2
exit 1
fi
COMMAND=$1
shift
# Konfiguration
APP_NAME="myapp"
CONTAINER_NAME="${APP_NAME}-container"
IMAGE_NAME="myorg/${APP_NAME}:latest"
DATA_VOLUME="${APP_NAME}-data"
BACKUP_DIR="./backups"
# Funktionen
start_container() {
echo "Starte Container $CONTAINER_NAME..."
# Prüfen, ob Container bereits existiert
if docker ps -a | grep -q "$CONTAINER_NAME"; then
echo "Container existiert bereits, starte ihn..."
docker start "$CONTAINER_NAME"
else
echo "Erstelle und starte neuen Container..."
docker run -d \
--name "$CONTAINER_NAME" \
-v "$DATA_VOLUME:/app/data" \
-p 8080:80 \
-e "APP_ENV=production" \
"$IMAGE_NAME"
fi
# Warten und Status prüfen
sleep 2
if docker ps | grep -q "$CONTAINER_NAME"; then
echo "Container erfolgreich gestartet."
else
echo "Fehler beim Starten des Containers!" >&2
docker logs "$CONTAINER_NAME"
return 1
fi
}
stop_container() {
echo "Stoppe Container $CONTAINER_NAME..."
docker stop "$CONTAINER_NAME"
}
show_status() {
echo "Status von $APP_NAME:"
if docker ps | grep -q "$CONTAINER_NAME"; then
echo "Container läuft."
# Laufzeit anzeigen
uptime=$(docker ps --format "{{.RunningFor}}" --filter "name=$CONTAINER_NAME")
echo "Laufzeit: $uptime"
# Port-Weiterleitungen anzeigen
echo "Port-Weiterleitungen:"
docker port "$CONTAINER_NAME"
# Ressourcennutzung anzeigen
echo -e "\nRessourcennutzung:"
docker stats "$CONTAINER_NAME" --no-stream --format "CPU: {{.CPUPerc}}, RAM: {{.MemUsage}}"
else
if docker ps -a | grep -q "$CONTAINER_NAME"; then
echo "Container ist gestoppt."
else
echo "Container existiert nicht."
fi
fi
}
show_logs() {
local lines=${1:-100}
echo "Zeige letzte $lines Logzeilen von $CONTAINER_NAME:"
docker logs --tail "$lines" "$CONTAINER_NAME"
}
backup_data() {
local backup_file="$BACKUP_DIR/${APP_NAME}_data_$(date +%Y%m%d_%H%M%S).tar.gz"
echo "Erstelle Backup von $DATA_VOLUME in $backup_file..."
# Backup-Verzeichnis erstellen, falls es nicht existiert
mkdir -p "$BACKUP_DIR"
# Temporären Container erstellen, um auf das Volume zuzugreifen
docker run --rm \
-v "$DATA_VOLUME:/data" \
-v "$(pwd)/$BACKUP_DIR:/backup" \
alpine \
tar -czf "/backup/$(basename "$backup_file")" -C /data .
if [ $? -eq 0 ]; then
echo "Backup erfolgreich erstellt: $backup_file"
echo "Backup-Größe: $(du -h "$backup_file" | cut -f1)"
else
echo "Fehler beim Erstellen des Backups!" >&2
return 1
fi
}
# Befehle ausführen
case $COMMAND in
"start")
start_container
;;
"stop")
stop_container
;;
"status")
show_status
;;
"logs")
show_logs "$1"
;;
"backup")
backup_data
;;
*)
echo "Unbekannter Befehl: $COMMAND" >&2
exit 1
;;
esacShell-Skripte können als “Glue Code” zwischen verschiedenen Konfigurations-Management-Tools dienen:
#!/bin/bash
# multi-tool-deployment.sh - Orchestrierung verschiedener Konfigurations-Tools
set -e # Bei Fehlern sofort beenden
# Logging
LOG_FILE="./logs/deployment_$(date +%Y%m%d_%H%M%S).log"
mkdir -p ./logs
exec > >(tee -a "$LOG_FILE") 2>&1
echo "=== Deployment-Prozess gestartet ==="
echo "Zeitstempel: $(date)"
echo "Server: $(hostname)"
echo "Benutzer: $(whoami)"
echo "=============================="
# Ansible für Infrastrukturkonfiguration
echo "Phase 1: Infrastrukturkonfiguration mit Ansible"
cd ./ansible
ansible-playbook -i inventory/production.yml site.yml
# Terraform für Cloud-Ressourcen
echo "Phase 2: Cloud-Ressourcen mit Terraform"
cd ../terraform
terraform init
terraform apply -auto-approve
# Chef für Anwendungskonfiguration
echo "Phase 3: Anwendungskonfiguration mit Chef"
cd ../chef
knife ssh 'role:application-server' 'sudo chef-client' --ssh-user deploy
# Eigenes Deployment-Skript für die Anwendung
echo "Phase 4: Anwendungsdeployment"
cd ../app
./deploy.sh --env=production --version="$APP_VERSION"
# Smoke-Tests
echo "Phase 5: Smoke-Tests"
cd ../tests
./run-smoke-tests.sh --env=production
echo "=============================="
echo "Deployment-Prozess abgeschlossen"
echo "Zeitstempel: $(date)"
echo "Status: ERFOLGREICH"Shell-Skripte sind unverzichtbare Werkzeuge für die Automatisierung von Build- und Deployment-Prozessen. Sie ermöglichen es, komplexe Abläufe zu standardisieren, menschliche Fehler zu reduzieren und die Effizienz in Software-Entwicklungszyklen erheblich zu steigern.
Kontinuierliche Integration ist eine Entwicklungspraxis, bei der Entwickler ihren Code regelmäßig in ein gemeinsames Repository integrieren. Shell-Skripte spielen dabei eine zentrale Rolle:
#!/bin/bash
# ci-build.sh - Grundlegendes CI-Build-Skript
set -e # Beendet das Skript bei Fehlern
# Konfiguration
PROJECT_DIR=$(pwd)
BUILD_DIR="$PROJECT_DIR/build"
ARTIFACT_DIR="$PROJECT_DIR/artifacts"
# Build-Umgebung vorbereiten
echo "Bereite Build-Umgebung vor..."
rm -rf "$BUILD_DIR" "$ARTIFACT_DIR"
mkdir -p "$BUILD_DIR" "$ARTIFACT_DIR"
# Abhängigkeiten installieren
echo "Installiere Abhängigkeiten..."
if [ -f "package.json" ]; then
npm ci # Nutze 'ci' statt 'install' für reproduzierbare Builds
elif [ -f "pom.xml" ]; then
mvn dependency:go-offline
elif [ -f "requirements.txt" ]; then
python -m pip install -r requirements.txt
else
echo "Warnung: Kein bekanntes Abhängigkeitsformat erkannt"
fi
# Code-Qualitätsprüfungen
echo "Führe Qualitätsprüfungen durch..."
if [ -f "package.json" ]; then
npm run lint
npm run test
elif [ -f "pom.xml" ]; then
mvn test
elif [ -f "requirements.txt" ]; then
python -m pytest
fi
# Build ausführen
echo "Erstelle Build..."
if [ -f "package.json" ]; then
npm run build
elif [ -f "pom.xml" ]; then
mvn package -DskipTests
fi
# Artefakte sammeln
echo "Sammle Build-Artefakte..."
if [ -d "dist" ]; then
cp -r dist/* "$ARTIFACT_DIR/"
elif [ -d "target" ]; then
find target -name "*.jar" -exec cp {} "$ARTIFACT_DIR/" \;
fi
# Build-Informationen speichern
cat > "$ARTIFACT_DIR/build-info.txt" <<EOF
Build erstellt: $(date)
Commit: $(git rev-parse HEAD)
Branch: $(git rev-parse --abbrev-ref HEAD)
Erstellt von: $(whoami)@$(hostname)
EOF
echo "Build erfolgreich abgeschlossen"Ein zentrales Prinzip bei automatisierten Builds ist ihre Reproduzierbarkeit. Shell-Skripte sollten so gestaltet sein, dass sie bei gleichen Eingaben immer die gleichen Ausgaben erzeugen:
#!/bin/bash
# reproducible-build.sh - Erzeugt reproduzierbare Builds
set -euo pipefail # Strikte Fehlerbehandlung
# Build-Zeitstempel festlegen (für Reproduzierbarkeit)
export SOURCE_DATE_EPOCH=$(git log -1 --pretty=%ct)
# Deterministischer Modus für verschiedene Build-Tools
if [ -f "pom.xml" ]; then
# Maven: Reproduzierbare Builds
mvn clean package \
-Dmaven.buildNumber.skip=true \
-Dmaven.build.timestamp.format="yyyy-MM-dd'T'HH:mm:ssX" \
-Dproject.build.outputTimestamp="$(date -u -d "@$SOURCE_DATE_EPOCH" "+%Y-%m-%dT%H:%M:%SZ")"
elif [ -f "build.gradle" ]; then
# Gradle: Reproduzierbare Builds
GRADLE_OPTS="$GRADLE_OPTS -Dorg.gradle.caching=true -Dorg.gradle.configureondemand=true"
./gradlew clean assemble -PreproducibleBuild
elif [ -f "package.json" ]; then
# Node.js/npm
export NODE_ENV=production
npm ci # Verwendet package-lock.json für exakte Versionen
npm run build
fi
# Build-Artefakte überprüfen
echo "Prüfe Build-Artefakte auf Reproduzierbarkeit..."
find ./build -type f -name "*.jar" -exec sha256sum {} \; | sort > build_checksums.txt
echo "Build-Checksummen wurden in build_checksums.txt gespeichert"Ein einfaches Deployment-Skript kann wie folgt aussehen:
#!/bin/bash
# deploy.sh - Grundlegendes Deployment-Skript
set -e
# Konfiguration
APP_NAME="myapp"
ENV=$1 # z.B. "dev", "staging", "prod"
VERSION=$2
# Parameter prüfen
if [ -z "$ENV" ] || [ -z "$VERSION" ]; then
echo "Verwendung: $0 <umgebung> <version>" >&2
exit 1
fi
# Umgebungsspezifische Konfiguration laden
CONFIG_FILE="./deploy/config/${ENV}.conf"
if [ ! -f "$CONFIG_FILE" ]; then
echo "Fehler: Konfigurationsdatei für Umgebung '$ENV' nicht gefunden" >&2
exit 1
fi
source "$CONFIG_FILE"
echo "Starte Deployment von $APP_NAME Version $VERSION auf $ENV-Umgebung"
echo "Zielserver: $SERVER_HOST"
# Artefakt herunterladen
echo "Lade Artefakt herunter..."
ARTIFACT_URL="${ARTIFACT_REPO_URL}/${APP_NAME}/${VERSION}/${APP_NAME}-${VERSION}.tar.gz"
curl -s -o "/tmp/${APP_NAME}-${VERSION}.tar.gz" "$ARTIFACT_URL"
# Auf den Zielserver kopieren
echo "Kopiere Artefakt auf Zielserver..."
scp "/tmp/${APP_NAME}-${VERSION}.tar.gz" "${SERVER_USER}@${SERVER_HOST}:/tmp/"
# Remotebefehle auf dem Zielserver ausführen
echo "Führe Deployment auf Zielserver aus..."
ssh "${SERVER_USER}@${SERVER_HOST}" bash -s << EOF
set -e
echo "Bereite Anwendungsverzeichnis vor..."
mkdir -p ${DEPLOY_DIR}/${APP_NAME}
echo "Entpacke Artefakt..."
tar -xzf /tmp/${APP_NAME}-${VERSION}.tar.gz -C ${DEPLOY_DIR}/${APP_NAME}
echo "Aktualisiere Symbolischen Link..."
ln -sfn ${DEPLOY_DIR}/${APP_NAME} ${DEPLOY_DIR}/current
echo "Starte Anwendung neu..."
${RESTART_COMMAND}
echo "Prüfe Anwendungsstatus..."
${HEALTH_CHECK_COMMAND}
echo "Räume temporäre Dateien auf..."
rm /tmp/${APP_NAME}-${VERSION}.tar.gz
EOF
echo "Deployment erfolgreich abgeschlossen!"Eine fortschrittlichere Deployment-Strategie ist das Blue-Green Deployment, bei dem zwei identische Produktionsumgebungen (Blue und Green) genutzt werden, um Ausfallzeiten zu minimieren:
#!/bin/bash
# blue-green-deploy.sh - Blue-Green Deployment-Implementierung
set -e
# Konfiguration
APP_NAME="myapp"
VERSION=$1
DEPLOY_USER="deploy"
DEPLOY_SERVERS=("app-server-1" "app-server-2")
CONFIG_PATH="/etc/myapp"
DEPLOY_PATH="/opt/myapp"
HEALTH_CHECK_URL="http://localhost:8080/health"
LOAD_BALANCER_CLI="/usr/local/bin/lb-control"
# Parameter prüfen
if [ -z "$VERSION" ]; then
echo "Verwendung: $0 <version>" >&2
exit 1
fi
# Hilfsfunktionen
log() {
echo "[$(date '+%Y-%m-%d %H:%M:%S')] $1"
}
perform_health_check() {
local server=$1
local max_retries=10
local retry_interval=5
local attempt=1
log "Führe Health-Checks für $server durch..."
while [ $attempt -le $max_retries ]; do
log " Versuch $attempt von $max_retries..."
if ssh "$DEPLOY_USER@$server" "curl -s -f $HEALTH_CHECK_URL > /dev/null"; then
log " Health-Check erfolgreich!"
return 0
else
log " Health-Check fehlgeschlagen, warte $retry_interval Sekunden..."
sleep $retry_interval
attempt=$((attempt + 1))
fi
done
log "FEHLER: Health-Check für $server nach $max_retries Versuchen fehlgeschlagen"
return 1
}
# Aktuelle aktive Umgebung ermitteln
determine_current_environment() {
if $LOAD_BALANCER_CLI status | grep -q "active: blue"; then
echo "blue"
else
echo "green"
fi
}
# Deployment durchführen
deploy_to_environment() {
local environment=$1
log "Starte Deployment in $environment-Umgebung..."
# Server für die Umgebung ermitteln
local servers=()
for server in "${DEPLOY_SERVERS[@]}"; do
if $LOAD_BALANCER_CLI get-env "$server" | grep -q "$environment"; then
servers+=("$server")
fi
done
# Artefakt auf jeden Server kopieren und installieren
for server in "${servers[@]}"; do
log "Deploye auf Server: $server"
# Artefakt kopieren
log " Kopiere Artefakt..."
scp "./artifacts/${APP_NAME}-${VERSION}.tar.gz" "$DEPLOY_USER@$server:/tmp/"
# Deployment ausführen
log " Führe Deployment aus..."
ssh "$DEPLOY_USER@$server" << EOF
set -e
# Entpacke das Artefakt
mkdir -p ${DEPLOY_PATH}/${VERSION}
tar -xzf /tmp/${APP_NAME}-${VERSION}.tar.gz -C ${DEPLOY_PATH}/${VERSION}
# Aktualisiere Konfiguration
cp ${CONFIG_PATH}/${environment}/* ${DEPLOY_PATH}/${VERSION}/config/
# Aktualisiere Symlink
ln -sfn ${DEPLOY_PATH}/${VERSION} ${DEPLOY_PATH}/${environment}
# Starte Anwendung
systemctl restart ${APP_NAME}-${environment}
# Räume auf
rm /tmp/${APP_NAME}-${VERSION}.tar.gz
EOF
# Health-Check
if ! perform_health_check "$server"; then
log "FEHLER: Deployment auf $server fehlgeschlagen"
return 1
fi
done
log "Deployment in $environment-Umgebung erfolgreich abgeschlossen"
return 0
}
# Hauptablauf
log "Starte Blue-Green Deployment für $APP_NAME Version $VERSION"
# Bestimme aktuelle und inaktive Umgebung
CURRENT_ENV=$(determine_current_environment)
if [ "$CURRENT_ENV" == "blue" ]; then
TARGET_ENV="green"
else
TARGET_ENV="blue"
fi
log "Aktuelle Umgebung ist $CURRENT_ENV, Zielumgebung ist $TARGET_ENV"
# Deployment in Zielumgebung
if deploy_to_environment "$TARGET_ENV"; then
# Umschalten des Load Balancers
log "Schalte Load Balancer auf $TARGET_ENV-Umgebung..."
$LOAD_BALANCER_CLI switch-to "$TARGET_ENV"
log "Prüfe Gesamtsystem-Health..."
sleep 10 # Kurze Pause für Stabilisierung
if curl -s -f "https://app.example.com/health" > /dev/null; then
log "Systemüberprüfung erfolgreich"
log "Blue-Green Deployment erfolgreich abgeschlossen!"
else
log "FEHLER: Systemüberprüfung fehlgeschlagen, Rollback wird durchgeführt..."
$LOAD_BALANCER_CLI switch-to "$CURRENT_ENV"
log "Rollback auf $CURRENT_ENV abgeschlossen"
exit 1
fi
else
log "Deployment in $TARGET_ENV-Umgebung fehlgeschlagen, keine Änderungen am Produktivverkehr"
exit 1
fiDie Erstellung und Veröffentlichung von Container-Images lässt sich ebenfalls mit Shell-Skripten automatisieren:
#!/bin/bash
# container-build-push.sh - Container-Image erstellen und in Registry pushen
set -euo pipefail
# Konfiguration
IMAGE_NAME="myorganization/myapp"
IMAGE_TAG=${1:-latest} # Verwende "latest" als Standard, wenn kein Tag angegeben
REGISTRY="docker.example.com"
BUILD_ARGS=""
# Optionale Build-Argumente basierend auf der Umgebung
if [ -n "${CI_ENVIRONMENT:-}" ]; then
BUILD_ARGS="--build-arg ENV=${CI_ENVIRONMENT}"
fi
# Zusätzliche Tags für Version und Build-Nummer
ADDITIONAL_TAGS=()
if [ -n "${VERSION:-}" ]; then
ADDITIONAL_TAGS+=("$VERSION")
fi
if [ -n "${CI_BUILD_NUMBER:-}" ]; then
ADDITIONAL_TAGS+=("build-${CI_BUILD_NUMBER}")
fi
echo "Baue Container-Image: ${IMAGE_NAME}:${IMAGE_TAG}"
# Image bauen
docker build \
--no-cache \
$BUILD_ARGS \
-t "${IMAGE_NAME}:${IMAGE_TAG}" \
.
# Zusätzliche Tags hinzufügen
for tag in "${ADDITIONAL_TAGS[@]}"; do
echo "Füge Tag hinzu: ${IMAGE_NAME}:${tag}"
docker tag "${IMAGE_NAME}:${IMAGE_TAG}" "${IMAGE_NAME}:${tag}"
done
# In die Registry einloggen (falls Anmeldedaten vorhanden sind)
if [ -n "${REGISTRY_USER:-}" ] && [ -n "${REGISTRY_PASSWORD:-}" ]; then
echo "Melde bei Registry an: ${REGISTRY}"
echo "${REGISTRY_PASSWORD}" | docker login "${REGISTRY}" -u "${REGISTRY_USER}" --password-stdin
fi
# Images in die Registry pushen
echo "Pushe Images in Registry..."
docker push "${IMAGE_NAME}:${IMAGE_TAG}"
for tag in "${ADDITIONAL_TAGS[@]}"; do
docker push "${IMAGE_NAME}:${tag}"
done
# Lokale Images aufräumen
echo "Räume lokale Images auf..."
docker image prune -f
echo "Container-Build und Push abgeschlossen"Shell-Skripte können auch für das Deployment in Kubernetes-Clustern verwendet werden:
#!/bin/bash
# k8s-deploy.sh - Deployment in Kubernetes-Cluster
set -e
# Konfiguration
APP_NAME="myapp"
NAMESPACE=$1
VERSION=$2
KUBE_CONFIG_PATH=${3:-"$HOME/.kube/config"}
DEPLOYMENT_TEMPLATE="./k8s/deployment.yaml.tmpl"
SERVICE_TEMPLATE="./k8s/service.yaml.tmpl"
TEMP_DIR="/tmp/k8s-deploy-${APP_NAME}"
# Parameter prüfen
if [ -z "$NAMESPACE" ] || [ -z "$VERSION" ]; then
echo "Verwendung: $0 <namespace> <version> [kube-config-pfad]" >&2
exit 1
fi
# Temporäres Verzeichnis für generierte Manifeste
mkdir -p "$TEMP_DIR"
# Umgebungsspezifische Konfiguration laden
CONFIG_FILE="./k8s/config/${NAMESPACE}.env"
if [ -f "$CONFIG_FILE" ]; then
echo "Lade Konfiguration aus $CONFIG_FILE"
source "$CONFIG_FILE"
else
echo "Warnung: Keine spezifische Konfiguration für $NAMESPACE gefunden"
fi
# Kubernetes-Kontext setzen
export KUBECONFIG="$KUBE_CONFIG_PATH"
kubectl config use-context "$KUBE_CONTEXT"
# Namespace erstellen, falls nicht vorhanden
if ! kubectl get namespace "$NAMESPACE" > /dev/null 2>&1; then
echo "Namespace $NAMESPACE existiert nicht, wird erstellt..."
kubectl create namespace "$NAMESPACE"
else
echo "Namespace $NAMESPACE existiert bereits"
fi
# Deployment-Manifest generieren
echo "Generiere Deployment-Manifest..."
envsubst < "$DEPLOYMENT_TEMPLATE" > "${TEMP_DIR}/deployment.yaml"
# Service-Manifest generieren
echo "Generiere Service-Manifest..."
envsubst < "$SERVICE_TEMPLATE" > "${TEMP_DIR}/service.yaml"
# Manifeste anwenden
echo "Wende Kubernetes-Manifeste an..."
kubectl apply -f "${TEMP_DIR}/deployment.yaml" -n "$NAMESPACE"
kubectl apply -f "${TEMP_DIR}/service.yaml" -n "$NAMESPACE"
# Auf Deployment-Abschluss warten
echo "Warte auf Abschluss des Deployments..."
kubectl rollout status deployment/${APP_NAME} -n "$NAMESPACE" --timeout=300s
# Status überprüfen
echo "Überprüfe Deployment-Status..."
PODS=$(kubectl get pods -n "$NAMESPACE" -l app=${APP_NAME} -o jsonpath='{.items[*].metadata.name}')
for pod in $PODS; do
echo "Überprüfe Pod $pod..."
STATUS=$(kubectl get pod $pod -n "$NAMESPACE" -o jsonpath='{.status.phase}')
if [ "$STATUS" != "Running" ]; then
echo "FEHLER: Pod $pod ist nicht im Status 'Running' (aktuell: $STATUS)" >&2
kubectl describe pod $pod -n "$NAMESPACE"
exit 1
fi
done
# Service-Endpunkt abrufen
if [ "$EXPOSE_SERVICE" == "true" ]; then
echo "Rufe Service-Endpunkt ab..."
if [ "$SERVICE_TYPE" == "LoadBalancer" ]; then
# Warten auf externe IP
echo "Warte auf externe IP für Service..."
for i in {1..30}; do
EXTERNAL_IP=$(kubectl get service ${APP_NAME} -n "$NAMESPACE" -o jsonpath='{.status.loadBalancer.ingress[0].ip}')
if [ -n "$EXTERNAL_IP" ]; then
break
fi
echo "Warte auf externe IP... ($i/30)"
sleep 10
done
if [ -n "$EXTERNAL_IP" ]; then
echo "Service ist verfügbar unter: $EXTERNAL_IP:$SERVICE_PORT"
else
echo "Warnung: Konnte keine externe IP abrufen, Service möglicherweise noch nicht vollständig bereitgestellt"
fi
elif [ "$SERVICE_TYPE" == "ClusterIP" ]; then
CLUSTER_IP=$(kubectl get service ${APP_NAME} -n "$NAMESPACE" -o jsonpath='{.spec.clusterIP}')
echo "Service ist intern verfügbar unter: $CLUSTER_IP:$SERVICE_PORT"
fi
fi
# Aufräumen
rm -rf "$TEMP_DIR"
echo "Deployment von $APP_NAME Version $VERSION in Namespace $NAMESPACE erfolgreich abgeschlossen"Besonders bei nativen Anwendungen sind Cross-Plattform-Builds eine Herausforderung, die mit Shell-Skripten gelöst werden kann:
#!/bin/bash
# cross-platform-build.sh - Erstellt Builds für mehrere Plattformen
set -euo pipefail
# Konfiguration
APP_NAME="myapp"
VERSION=${1:-$(git describe --tags --always)}
OUTPUT_DIR="./dist"
# Unterstützte Plattformen
PLATFORMS=(
"linux:amd64"
"linux:arm64"
"darwin:amd64"
"darwin:arm64"
"windows:amd64"
)
# Vorbereitung
mkdir -p "$OUTPUT_DIR"
echo "Starte Cross-Platform-Build für $APP_NAME Version $VERSION"
# Build für jede Plattform
for platform in "${PLATFORMS[@]}"; do
# Plattform und Architektur aufteilen
IFS=':' read -r os arch <<< "$platform"
echo "Baue für ${os}_${arch}..."
# Ausgabedateiname setzen
if [ "$os" == "windows" ]; then
output_file="${OUTPUT_DIR}/${APP_NAME}_${VERSION}_${os}_${arch}.exe"
else
output_file="${OUTPUT_DIR}/${APP_NAME}_${VERSION}_${os}_${arch}"
fi
# Umgebungsvariablen für den Go-Compiler setzen
export GOOS="$os"
export GOARCH="$arch"
# Build durchführen
go build -ldflags="-s -w -X main.Version=${VERSION}" -o "$output_file" ./cmd/main.go
# Für nicht-Windows-Plattformen ausführbare Rechte setzen
if [ "$os" != "windows" ]; then
chmod +x "$output_file"
fi
# Prüfsumme erstellen
if command -v sha256sum > /dev/null; then
sha256sum "$output_file" >> "${OUTPUT_DIR}/checksums.txt"
elif command -v shasum > /dev/null; then
shasum -a 256 "$output_file" >> "${OUTPUT_DIR}/checksums.txt"
fi
echo "Build für ${os}_${arch} abgeschlossen"
done
# Ausgabe komprimieren
echo "Erstelle Archivdateien..."
cd "$OUTPUT_DIR"
for file in "${APP_NAME}_${VERSION}"*; do
if [[ "$file" == *.exe ]]; then
# Windows: ZIP-Archiv erstellen
zip "${file%.exe}.zip" "$file"
rm "$file"
else
# Unix: TAR.GZ-Archiv erstellen
tar -czf "${file}.tar.gz" "$file"
rm "$file"
fi
done
cd - > /dev/null
echo "Cross-Platform-Build abgeschlossen. Ausgabedateien in $OUTPUT_DIR"Für effizientere Builds kann ein inkrementeller Ansatz verwendet werden:
#!/bin/bash
# incremental-build.sh - Inkrementelle Build-Strategie für große Projekte
set -e
# Konfiguration
PROJECT_ROOT=$(pwd)
BUILD_CACHE_DIR="${PROJECT_ROOT}/.build-cache"
LAST_BUILD_FILE="${BUILD_CACHE_DIR}/last-build-hash"
SOURCE_DIRS=("src" "include" "lib")
BUILD_CMD="make -j$(nproc)"
CLEAN_CMD="make clean"
# Hilfsfunktionen
calculate_source_hash() {
find "${SOURCE_DIRS[@]}" -type f -name "*.cpp" -o -name "*.h" -o -name "*.c" | \
sort | xargs cat | md5sum | cut -d' ' -f1
}
# Build-Cache-Verzeichnis erstellen
mkdir -p "$BUILD_CACHE_DIR"
# Aktuelle Quellcode-Prüfsumme berechnen
echo "Berechne Quellcode-Prüfsumme..."
CURRENT_HASH=$(calculate_source_hash)
echo "Aktuelle Quellcode-Prüfsumme: $CURRENT_HASH"
# Prüfen, ob sich der Quellcode seit dem letzten Build geändert hat
if [ -f "$LAST_BUILD_FILE" ]; then
LAST_HASH=$(cat "$LAST_BUILD_FILE")
echo "Letzte Build-Prüfsumme: $LAST_HASH"
if [ "$CURRENT_HASH" == "$LAST_HASH" ]; then
echo "Keine Änderungen am Quellcode seit dem letzten Build. Überspringe Build."
exit 0
fi
else
echo "Keine vorherige Build-Prüfsumme gefunden"
fi
# Buildstatus-Datei erstellen, um Buildabbrüche zu erkennen
echo "running" > "${BUILD_CACHE_DIR}/build-status"
# CMake-Cache prüfen und aktualisieren
if [ -d "build" ]; then
if [ -f "CMakeLists.txt" ] && [ -f "build/CMakeCache.txt" ]; then
echo "Vorhandener CMake-Cache gefunden, führe inkrementellen Build durch..."
cd build
else
echo "Führe Clean-Build durch..."
$CLEAN_CMD
fi
else
echo "Kein Build-Verzeichnis gefunden, erstelle eines..."
mkdir -p build
cd build
cmake ..
fi
# Build ausführen
echo "Starte Build-Prozess..."
if $BUILD_CMD; then
# Build erfolgreich
echo "$CURRENT_HASH" > "$LAST_BUILD_FILE"
echo "success" > "${BUILD_CACHE_DIR}/build-status"
echo "Build erfolgreich abgeschlossen"
else
# Build fehlgeschlagen
echo "failed" > "${BUILD_CACHE_DIR}/build-status"
echo "Build fehlgeschlagen" >&2
exit 1
fiContainer-Technologien wie Docker und Kubernetes haben die Art und Weise, wie wir Anwendungen entwickeln, bereitstellen und betreiben, grundlegend verändert. Shell-Skripte spielen in diesen Umgebungen eine wichtige Rolle und müssen an die Besonderheiten containerisierter Infrastrukturen angepasst werden.
In Docker werden Shell-Skripte häufig als Entrypoints verwendet, um Container zu initialisieren und zu starten:
#!/bin/bash
# docker-entrypoint.sh - Typischer Entrypoint für einen Docker-Container
set -e
# Umgebungsvariablen mit Standardwerten
: "${DB_HOST:=localhost}"
: "${DB_PORT:=5432}"
: "${APP_PORT:=8080}"
: "${LOG_LEVEL:=info}"
# Konfigurationsdateien aus Templates generieren
echo "Generiere Konfiguration mit folgenden Parametern:"
echo "Database: $DB_HOST:$DB_PORT"
echo "App Port: $APP_PORT"
echo "Log Level: $LOG_LEVEL"
# Template-Ersetzung für Konfigurationsdateien
if [ -d "/app/templates" ]; then
for template in /app/templates/*.tmpl; do
output_file="${template%.tmpl}"
echo "Generiere $(basename "$output_file") aus Template..."
envsubst < "$template" > "$output_file"
done
fi
# Datenbankverbindung testen
echo "Teste Datenbankverbindung..."
max_retries=30
counter=0
until pg_isready -h "$DB_HOST" -p "$DB_PORT" -U "$DB_USER"; do
if [ $counter -eq $max_retries ]; then
echo "Konnte keine Verbindung zur Datenbank herstellen!" >&2
exit 1
fi
echo "Warte auf Datenbank... ($((counter + 1))/$max_retries)"
counter=$((counter + 1))
sleep 1
done
# Schema-Migration, falls erforderlich
if [ "$RUN_MIGRATIONS" = "true" ]; then
echo "Führe Datenbankmigrationen aus..."
/app/bin/migrate -database "postgres://${DB_USER}:${DB_PASSWORD}@${DB_HOST}:${DB_PORT}/${DB_NAME}?sslmode=disable" -path /app/migrations up
fi
# Signal-Handling für sauberes Herunterfahren
pid=0
# SIGTERM-Handler
term_handler() {
if [ $pid -ne 0 ]; then
echo "Beende Anwendungsprozess..."
kill -SIGTERM "$pid"
wait "$pid"
fi
exit 143 # 128 + 15 -- SIGTERM
}
# SIGTERM-Handler registrieren
trap 'term_handler' SIGTERM
# Hauptanwendung starten
echo "Starte Anwendung..."
if [ "$1" = "app" ]; then
# Anwendung im Vordergrund starten
exec /app/bin/server --port "$APP_PORT" --log-level "$LOG_LEVEL"
elif [ "$1" = "worker" ]; then
# Hintergrundarbeiten ausführen
exec /app/bin/worker --concurrency "$WORKER_CONCURRENCY"
elif [ "$1" = "cron" ]; then
# Scheduled Tasks ausführen
exec /app/bin/scheduler
else
# Benutzerdefinierter Befehl
exec "$@"
fi
# Sollte nur erreicht werden, wenn exec fehlschlägt
exit 1Die Verwendung von exec ist wichtig, damit das
Shell-Skript durch den ausgeführten Prozess ersetzt wird, wodurch der
Container den Prozess-ID 1 erhält und Signale korrekt verarbeitet werden
können.
Ein häufiges Problem in Containern ist die korrekte Verarbeitung von Signalen, insbesondere SIGTERM zum sauberen Herunterfahren:
#!/bin/bash
# signal-handling.sh - Beispiel für Signalverarbeitung in Containern
set -e
# Anwendungskonfiguration
APP_BIN="/app/server"
APP_ARGS="--config=/app/config.yml"
APP_PID=""
# Hilfsfunktion für Logging
log() {
echo "[$(date '+%Y-%m-%d %H:%M:%S')] $1"
}
# Funktion zum sauberen Herunterfahren
shutdown() {
log "Container wird heruntergefahren..."
if [ -n "$APP_PID" ]; then
log "Sende SIGTERM an Anwendungsprozess (PID: $APP_PID)..."
kill -TERM "$APP_PID" 2>/dev/null || true
# Warte maximal 30 Sekunden auf das Beenden des Prozesses
log "Warte auf Beendigung des Prozesses..."
for i in {1..30}; do
if ! kill -0 "$APP_PID" 2>/dev/null; then
log "Prozess wurde beendet"
break
fi
sleep 1
done
# Falls der Prozess immer noch läuft, sende SIGKILL
if kill -0 "$APP_PID" 2>/dev/null; then
log "Prozess reagiert nicht, sende SIGKILL..."
kill -KILL "$APP_PID" 2>/dev/null || true
fi
fi
log "Aufräumaktionen werden durchgeführt..."
# Hier können zusätzliche Aufräumaktionen erfolgen
log "Container-Shutdown abgeschlossen"
exit 0
}
# Signal-Handler einrichten
trap shutdown SIGTERM SIGINT
# Anwendung starten
log "Starte Anwendung: $APP_BIN $APP_ARGS"
$APP_BIN $APP_ARGS &
APP_PID=$!
log "Anwendung gestartet mit PID: $APP_PID"
# Warte auf Beendigung der Anwendung
wait $APP_PID
EXIT_CODE=$?
log "Anwendung beendet mit Exit-Code: $EXIT_CODE"
exit $EXIT_CODEShell-Skripte können auch verwendet werden, um Docker-Images effizient zu erstellen und zu verwalten:
#!/bin/bash
# build-images.sh - Skript zum Erstellen und Taggen von Docker-Images
set -euo pipefail
# Konfiguration
IMAGE_PREFIX="mycompany"
REGISTRY="registry.example.com"
GIT_SHA=$(git rev-parse --short HEAD)
BUILD_DATE=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
# Hilfsfunktionen
log() {
echo "[$(date '+%Y-%m-%d %H:%M:%S')] $1"
}
build_image() {
local service=$1
local version=$2
local dockerfile="./services/$service/Dockerfile"
local context="./services/$service"
if [ ! -f "$dockerfile" ]; then
log "Fehler: Dockerfile für Service '$service' nicht gefunden"
return 1
fi
local image_name="$IMAGE_PREFIX/$service"
local image_tag="$REGISTRY/$image_name:$version"
local latest_tag="$REGISTRY/$image_name:latest"
log "Baue Image für $service (Version: $version)..."
# Build-Arguments für alle Images
docker build \
--build-arg VERSION="$version" \
--build-arg BUILD_DATE="$BUILD_DATE" \
--build-arg GIT_SHA="$GIT_SHA" \
--tag "$image_tag" \
--tag "$latest_tag" \
-f "$dockerfile" \
"$context"
log "Image erfolgreich gebaut: $image_tag"
return 0
}
push_image() {
local service=$1
local version=$2
local image_name="$IMAGE_PREFIX/$service"
local image_tag="$REGISTRY/$image_name:$version"
local latest_tag="$REGISTRY/$image_name:latest"
log "Pushe Image für $service (Version: $version)..."
docker push "$image_tag"
docker push "$latest_tag"
log "Image erfolgreich in Registry gepusht: $image_tag"
return 0
}
# Hauptskript
VERSION=${1:-"$GIT_SHA"}
SERVICES=${2:-$(ls -d ./services/* | xargs -n1 basename)}
log "Starte Build-Prozess für Version: $VERSION"
log "Zu bauende Services: $SERVICES"
# Docker-Login, falls Anmeldedaten vorhanden sind
if [ -n "${REGISTRY_USER:-}" ] && [ -n "${REGISTRY_PASSWORD:-}" ]; then
log "Anmeldung bei Registry: $REGISTRY"
echo "$REGISTRY_PASSWORD" | docker login "$REGISTRY" -u "$REGISTRY_USER" --password-stdin
fi
# Images bauen und pushen
for service in $SERVICES; do
if build_image "$service" "$VERSION"; then
push_image "$service" "$VERSION"
else
log "Fehler beim Bauen des Images für $service"
exit 1
fi
done
log "Build-Prozess erfolgreich abgeschlossen"Bei Multi-Stage Builds können Shell-Skripte verwendet werden, um komplexe Build-Prozesse zu orchestrieren:
#!/bin/bash
# compile-assets.sh - Asset-Kompilierung im Build-Container
set -e
# Konfiguration
NODE_ENV=${NODE_ENV:-"production"}
BUILD_DIR="/app/build"
SRC_DIR="/app/src"
CACHE_DIR="/app/cache"
# Hilfsfunktionen
log() {
echo "[$(date '+%Y-%m-%d %H:%M:%S')] $1"
}
# Vorbereitung
log "Starte Asset-Kompilierung (Umgebung: $NODE_ENV)..."
mkdir -p "$BUILD_DIR" "$CACHE_DIR"
# Abhängigkeiten installieren
if [ "$NODE_ENV" = "production" ]; then
log "Installiere Produktionsabhängigkeiten..."
npm ci --production --cache "$CACHE_DIR" --prefer-offline
else
log "Installiere alle Abhängigkeiten..."
npm ci --cache "$CACHE_DIR" --prefer-offline
fi
# Assets kompilieren
log "Kompiliere Assets..."
npm run build
# Optimierungen für Produktionsbuild
if [ "$NODE_ENV" = "production" ]; then
log "Optimiere Assets für Produktion..."
# CSS minifizieren
log "Minifiziere CSS..."
for css_file in "$BUILD_DIR"/css/*.css; do
if [ -f "$css_file" ]; then
npx csso "$css_file" -o "$css_file"
fi
done
# JavaScript minifizieren
log "Minifiziere JavaScript..."
for js_file in "$BUILD_DIR"/js/*.js; do
if [ -f "$js_file" ]; then
npx terser "$js_file" -c -m -o "$js_file"
fi
done
# Bilder optimieren
log "Optimiere Bilder..."
if command -v optipng &> /dev/null; then
find "$BUILD_DIR" -type f -name "*.png" -exec optipng -quiet -strip all {} \;
fi
if command -v jpegoptim &> /dev/null; then
find "$BUILD_DIR" -type f \( -name "*.jpg" -o -name "*.jpeg" \) -exec jpegoptim --strip-all --all-progressive --max=85 {} \;
fi
fi
# Manifestdatei generieren
log "Generiere Asset-Manifest..."
find "$BUILD_DIR" -type f -name "*.js" -o -name "*.css" | while read -r file; do
file_rel=${file#$BUILD_DIR/}
hash=$(md5sum "$file" | cut -d ' ' -f 1)
echo "${file_rel},${hash}" >> "$BUILD_DIR/manifest.txt"
done
log "Asset-Kompilierung abgeschlossen"Shell-Skripte können Docker Compose-Befehle orchestrieren und erweitern:
#!/bin/bash
# compose-env.sh - Erweitertes Docker Compose-Management
set -eo pipefail
# Konfiguration
COMPOSE_FILE="docker-compose.yml"
ENV_FILE=".env"
ENVIRONMENT=${1:-"dev"}
COMMAND=${2:-"up"}
SERVICES=${@:3}
# Umgebungsspezifische Compose-Dateien
ENV_COMPOSE_FILE="docker-compose.${ENVIRONMENT}.yml"
# Hilfsfunktionen
usage() {
echo "Verwendung: $0 <umgebung> <befehl> [services...]"
echo
echo "Umgebungen: dev, test, staging, prod"
echo "Befehle: up, down, restart, logs, ps, exec, build"
exit 1
}
log() {
echo "[$(date '+%Y-%m-%d %H:%M:%S')] $1"
}
generate_env_file() {
local env=$1
local template_file=".env.template"
local env_specific_file=".env.${env}"
log "Generiere .env-Datei für Umgebung: $env"
# Standardwerte aus Template übernehmen
if [ -f "$template_file" ]; then
cat "$template_file" > "$ENV_FILE"
else
echo "# Automatisch generierte .env-Datei" > "$ENV_FILE"
echo "ENVIRONMENT=$env" >> "$ENV_FILE"
fi
# Umgebungsspezifische Werte hinzufügen
if [ -f "$env_specific_file" ]; then
log "Überschreibe mit umgebungsspezifischen Werten aus: $env_specific_file"
while IFS= read -r line; do
if [[ "$line" =~ ^[A-Za-z0-9_]+=.* ]]; then
key=${line%%=*}
# Entferne bereits vorhandene Schlüssel
sed -i "/^${key}=/d" "$ENV_FILE"
# Füge neue Zeile hinzu
echo "$line" >> "$ENV_FILE"
fi
done < "$env_specific_file"
fi
log ".env-Datei erfolgreich generiert"
}
get_compose_files() {
local compose_files="-f $COMPOSE_FILE"
if [ -f "$ENV_COMPOSE_FILE" ]; then
compose_files="$compose_files -f $ENV_COMPOSE_FILE"
fi
echo "$compose_files"
}
# Parameter prüfen
if [[ ! "$ENVIRONMENT" =~ ^(dev|test|staging|prod)$ ]]; then
echo "Fehler: Unbekannte Umgebung '$ENVIRONMENT'"
usage
fi
# .env-Datei für die Umgebung generieren
generate_env_file "$ENVIRONMENT"
# Docker Compose-Dateien bestimmen
COMPOSE_FILES=$(get_compose_files)
# Befehl ausführen
case "$COMMAND" in
"up")
log "Starte Container für Umgebung: $ENVIRONMENT"
if [ -z "$SERVICES" ]; then
docker-compose $COMPOSE_FILES up -d
else
docker-compose $COMPOSE_FILES up -d $SERVICES
fi
;;
"down")
log "Stoppe Container für Umgebung: $ENVIRONMENT"
docker-compose $COMPOSE_FILES down
;;
"restart")
log "Starte Container neu für Umgebung: $ENVIRONMENT"
if [ -z "$SERVICES" ]; then
docker-compose $COMPOSE_FILES restart
else
docker-compose $COMPOSE_FILES restart $SERVICES
fi
;;
"logs")
log "Zeige Logs für Umgebung: $ENVIRONMENT"
if [ -z "$SERVICES" ]; then
docker-compose $COMPOSE_FILES logs -f
else
docker-compose $COMPOSE_FILES logs -f $SERVICES
fi
;;
"ps")
log "Zeige Container für Umgebung: $ENVIRONMENT"
docker-compose $COMPOSE_FILES ps
;;
"exec")
if [ -z "$SERVICES" ]; then
echo "Fehler: Service und Befehl müssen angegeben werden"
exit 1
fi
SERVICE=$3
EXEC_COMMAND=${@:4}
log "Führe Befehl aus in Service $SERVICE: $EXEC_COMMAND"
docker-compose $COMPOSE_FILES exec $SERVICE $EXEC_COMMAND
;;
"build")
log "Baue Images für Umgebung: $ENVIRONMENT"
if [ -z "$SERVICES" ]; then
docker-compose $COMPOSE_FILES build
else
docker-compose $COMPOSE_FILES build $SERVICES
fi
;;
*)
echo "Fehler: Unbekannter Befehl '$COMMAND'"
usage
;;
esac
log "Befehl erfolgreich ausgeführt"Shell-Skripte können auch die Verwaltung von Kubernetes-Ressourcen automatisieren:
#!/bin/bash
# k8s-deploy.sh - Deployment in Kubernetes automatisieren
set -eo pipefail
# Konfiguration
APP_NAME=${1:-"myapp"}
NAMESPACE=${2:-"default"}
VERSION=${3:-"latest"}
HELM_VALUES_DIR="./k8s/helm-values"
# Hilfsfunktionen
log() {
echo "[$(date '+%Y-%m-%d %H:%M:%S')] $1"
}
usage() {
echo "Verwendung: $0 <app-name> <namespace> <version>"
echo
echo "Beispiel: $0 myapp production v1.2.3"
exit 1
}
check_prerequisites() {
for cmd in kubectl helm jq; do
if ! command -v $cmd &> /dev/null; then
log "Fehler: $cmd ist nicht installiert"
exit 1
fi
done
# Prüfen, ob kubectl konfiguriert ist
if ! kubectl cluster-info &> /dev/null; then
log "Fehler: kubectl ist nicht konfiguriert oder Cluster ist nicht erreichbar"
exit 1
fi
}
set_kube_context() {
local namespace=$1
# Kontext basierend auf der Umgebung setzen
case "$namespace" in
"production")
CONTEXT="prod-cluster"
;;
"staging")
CONTEXT="staging-cluster"
;;
*)
CONTEXT="dev-cluster"
;;
esac
log "Setze kubectl-Kontext auf: $CONTEXT"
kubectl config use-context "$CONTEXT"
# Namespace erstellen, falls nicht vorhanden
if ! kubectl get namespace "$namespace" &> /dev/null; then
log "Namespace $namespace existiert nicht, wird erstellt..."
kubectl create namespace "$namespace"
fi
}
deploy_with_helm() {
local app=$1
local namespace=$2
local version=$3
local values_file="$HELM_VALUES_DIR/${namespace}.yaml"
if [ ! -f "$values_file" ]; then
log "Warnung: Keine spezifischen Helm-Werte für $namespace gefunden"
values_file="$HELM_VALUES_DIR/default.yaml"
fi
log "Deploye $app Version $version in Namespace $namespace mit Helm..."
# Helm Repository aktualisieren
log "Aktualisiere Helm Repositories..."
helm repo update
# Prüfen, ob Release existiert
if helm status -n "$namespace" "$app" &> /dev/null; then
# Upgrade
log "Führe Helm-Upgrade durch..."
helm upgrade "$app" "./charts/$app" \
--namespace "$namespace" \
--values "$values_file" \
--set image.tag="$version" \
--set fullnameOverride="$app" \
--atomic \
--timeout 5m
else
# Installation
log "Führe Helm-Installation durch..."
helm install "$app" "./charts/$app" \
--namespace "$namespace" \
--values "$values_file" \
--set image.tag="$version" \
--set fullnameOverride="$app" \
--atomic \
--timeout 5m
fi
log "Helm-Deployment abgeschlossen"
}
verify_deployment() {
local app=$1
local namespace=$2
log "Überprüfe Deployment-Status..."
# Warte, bis alle Pods bereit sind
log "Warte auf Rollout-Abschluss..."
kubectl rollout status deployment/"$app" -n "$namespace" --timeout=5m
# Prüfe, ob Pods laufen
local running_pods=$(kubectl get pods -n "$namespace" -l app="$app" -o jsonpath='{.items[?(@.status.phase=="Running")].metadata.name}' | wc -w)
local total_pods=$(kubectl get pods -n "$namespace" -l app="$app" -o jsonpath='{.items[*].metadata.name}' | wc -w)
if [ "$running_pods" -lt "$total_pods" ]; then
log "Fehler: Nicht alle Pods sind im Status 'Running' ($running_pods von $total_pods)"
kubectl get pods -n "$namespace" -l app="$app"
exit 1
fi
log "Alle Pods sind bereit"
# Prüfe Service-Endpunkte
if kubectl get service -n "$namespace" "$app" &> /dev/null; then
local service_type=$(kubectl get service -n "$namespace" "$app" -o jsonpath='{.spec.type}')
log "Service vom Typ $service_type ist verfügbar"
if [ "$service_type" = "LoadBalancer" ]; then
local external_ip=""
for i in {1..30}; do
external_ip=$(kubectl get service -n "$namespace" "$app" -o jsonpath='{.status.loadBalancer.ingress[0].ip}')
if [ -n "$external_ip" ]; then
log "Service ist extern verfügbar unter: $external_ip"
break
fi
log "Warte auf externe IP... ($i/30)"
sleep 10
done
if [ -z "$external_ip" ]; then
log "Warnung: Keine externe IP für LoadBalancer-Service erhalten"
fi
fi
fi
log "Deployment verifiziert und bereit"
}
# Hauptskript
if [ -z "$APP_NAME" ] || [ -z "$NAMESPACE" ]; then
usage
fi
log "Starte Deployment für $APP_NAME in Namespace $NAMESPACE (Version: $VERSION)"
check_prerequisites
set_kube_context "$NAMESPACE"
deploy_with_helm "$APP_NAME" "$NAMESPACE" "$VERSION"
verify_deployment "$APP_NAME" "$NAMESPACE"
log "Deployment erfolgreich abgeschlossen"Bei der Verwendung von Shell-Skripten in Containern sind einige Sicherheitsaspekte besonders wichtig:
#!/bin/bash
# secure-container-init.sh - Sichere Container-Initialisierung
set -euo pipefail
# Konfiguration
APP_USER="app"
APP_DIR="/app"
CONFIG_DIR="/config"
SECRETS_DIR="/run/secrets"
DATA_DIR="/data"
# Hilfsfunktionen
log() {
echo "[$(date '+%Y-%m-%d %H:%M:%S')] $1"
}
secure_container() {
log "Führe Sicherheitsmaßnahmen für Container durch..."
# Entferne setuid/setgid-Bits von Binärdateien
log "Entferne setuid/setgid-Bits..."
find / -xdev -perm /6000 -type f -exec chmod a-s {} \; || true
# Prüfe auf unnötige Software
for pkg in gcc g++ make python perl nc nmap telnet ftp; do
if command -v $pkg &> /dev/null; then
log "Warnung: Potentiell unsichere Software gefunden: $pkg"
fi
done
# Prüfe Dateiberechtigungen für kritische Dateien
log "Prüfe Dateiberechtigungen..."
for dir in "$CONFIG_DIR" "$SECRETS_DIR"; do
if [ -d "$dir" ]; then
chmod -R go-rwx "$dir"
log "Dateiberechtigungen für $dir gesichert"
fi
done
# Sichere Umgebungsvariablen
if [ -f "$SECRETS_DIR/env" ]; then
log "Lade sichere Umgebungsvariablen..."
set -a
source "$SECRETS_DIR/env"
set +a
# Lösche die Datei nach dem Laden
rm -f "$SECRETS_DIR/env"
fi
log "Sicherheitsmaßnahmen abgeschlossen"
}
setup_permissions() {
log "Konfiguriere Berechtigungen..."
# Erstelle Anwendungsverzeichnisse, falls sie nicht existieren
for dir in "$CONFIG_DIR" "$DATA_DIR"; do
if [ ! -d "$dir" ]; then
mkdir -p "$dir"
log "Verzeichnis erstellt: $dir"
fi
done
# Setze Berechtigungen für Anwendungsverzeichnisse
chown -R "$APP_USER:$APP_USER" "$APP_DIR" "$CONFIG_DIR" "$DATA_DIR"
chmod -R 755 "$APP_DIR"
chmod -R 750 "$CONFIG_DIR"
chmod -R 770 "$DATA_DIR"
log "Berechtigungen konfiguriert"
}
validate_config() {
log "Validiere Konfiguration..."
# Prüfe, ob erforderliche Konfigurationsdateien existieren
for file in "app.conf" "logging.conf"; do
if [ ! -f "$CONFIG_DIR/$file" ]; then
log "Fehler: Erforderliche Konfigurationsdatei nicht gefunden: $file"
return 1
fi
done
# Prüfe, ob Konfigurationsdateien gültig sind
if [ -f "$APP_DIR/bin/config-validator" ] && [ -x "$APP_DIR/bin/config-validator" ]; then
log "Führe Konfigurationsvalidierung aus..."
if ! "$APP_DIR/bin/config-validator" --config "$CONFIG_DIR/app.conf"; then
log "Fehler: Konfigurationsvalidierung fehlgeschlagen"
return 1
fi
fi
log "Konfiguration erfolgreich validiert"
return 0
}
# Hauptskript
log "Starte sichere Container-Initialisierung..."
secure_container
setup_permissions
if ! validate_config; then
log "Fehler: Initialisierung fehlgeschlagen aufgrund ungültiger Konfiguration"
exit 1
fi
log "Container-Initialisierung abgeschlossen, starte Anwendung..."
# Anwendung als nicht-root-Benutzer ausführen
exec gosu "$APP_USER" "$APP_DIR/bin/server" --config "$CONFIG_DIR/app.conf"# health-check.sh
perform_health_check() {
log "Führe Health-Check durch..."
# TCP-Port-Check
if ! check_tcp_port "$APP_PORT" "$TIMEOUT"; then
log "Fehler: Application-Port $APP_PORT ist nicht erreichbar"
return 1
fi
# HTTP-Endpoint-Check
if ! check_http_endpoint "$APP_PORT" "$HEALTH_ENDPOINT" "$TIMEOUT"; then
log "Fehler: Health-Endpoint nicht erreichbar"
return 1
fi
# Prozess-Check
if ! check_process "server"; then
log "Fehler: Server-Prozess nicht gefunden"
return 1
fi
# Dateisystem-Check
if ! check_filesystem "/data" 100; then
log "Warnung: Weniger als 100MB freier Speicherplatz in /data"
# Dies ist eine Warnung, kein Fehler
fi
log "Health-Check erfolgreich"
return 0
}
update_status() {
local status=$1
echo "$status" > "$STATUS_FILE"
log "Status aktualisiert: $status"
}
# Hauptlogik
log "Starte Health-Check-Prozess..."
# Initialen Status setzen
update_status "starting"
# Kurze Pause, um der Anwendung Zeit zum Starten zu geben
sleep 5
# Endlosschleife für kontinuierliche Checks
while true; do
if perform_health_check; then
FAILURE_COUNT=0
update_status "healthy"
else
FAILURE_COUNT=$((FAILURE_COUNT + 1))
log "Fehler #$FAILURE_COUNT von maximal $MAX_FAILURES"
if [ "$FAILURE_COUNT" -ge "$MAX_FAILURES" ]; then
update_status "unhealthy"
log "Anwendung als ungesund markiert nach $MAX_FAILURES aufeinanderfolgenden Fehlern"
fi
fi
sleep "$CHECK_INTERVAL"
doneDieses Skript kann als separater Prozess im Container ausgeführt
werden und meldet kontinuierlich den Gesundheitszustand der Anwendung.
In Kubernetes kann es mit livenessProbe und
readinessProbe verwendet werden:
livenessProbe:
exec:
command:
- cat
- /tmp/health_status
initialDelaySeconds: 15
periodSeconds: 5
failureThreshold: 3Da Container häufig ephemer sind, ist die Verwaltung von Logs ein wichtiger Aspekt:
#!/bin/bash
# log-manager.sh - Log-Verwaltung in Containern
set -eo pipefail
# Konfiguration
LOG_DIR="/var/log/app"
MAX_LOG_SIZE=${MAX_LOG_SIZE:-"100M"}
MAX_LOG_FILES=${MAX_LOG_FILES:-5}
LOG_STDOUT=${LOG_STDOUT:-true}
REMOTE_SYSLOG=${REMOTE_SYSLOG:-""}
LOG_LEVEL=${LOG_LEVEL:-"info"}
# Hilfsfunktionen
log() {
echo "[$(date '+%Y-%m-%d %H:%M:%S')] $1"
}
setup_logrotate() {
log "Konfiguriere Log-Rotation..."
cat > /etc/logrotate.d/app << EOF
$LOG_DIR/*.log {
size $MAX_LOG_SIZE
rotate $MAX_LOG_FILES
missingok
notifempty
compress
delaycompress
copytruncate
dateext
dateformat -%Y%m%d-%H%M%S
create 644 root root
}
EOF
log "Log-Rotation konfiguriert"
}
start_log_forwarding() {
if [ -n "$REMOTE_SYSLOG" ]; then
log "Starte Log-Forwarding zu $REMOTE_SYSLOG..."
# Prüfe, ob Syslog-Ziel erreichbar ist
host=$(echo "$REMOTE_SYSLOG" | cut -d: -f1)
port=$(echo "$REMOTE_SYSLOG" | cut -d: -f2)
if nc -z -w 2 "$host" "$port"; then
log "Syslog-Server erreichbar"
# Starte Fluentd, Logstash oder ein ähnliches Tool für die Log-Weiterleitung
if command -v fluentd &> /dev/null; then
log "Starte Fluentd für Log-Forwarding..."
cat > /etc/fluent/fluent.conf << EOF
<source>
@type tail
path $LOG_DIR/*.log
tag app
<parse>
@type json
</parse>
</source>
<match app>
@type syslog
host $host
port $port
facility user
severity $LOG_LEVEL
</match>
EOF
fluentd -c /etc/fluent/fluent.conf &
else
log "Warnung: Kein Log-Forwarding-Tool gefunden"
fi
else
log "Warnung: Syslog-Server nicht erreichbar"
fi
fi
}
# Hauptfunktion
main() {
log "Starte Log-Manager..."
# Erstelle Log-Verzeichnis, falls nicht vorhanden
mkdir -p "$LOG_DIR"
# Log-Rotation einrichten
setup_logrotate
# Log-Forwarding starten
start_log_forwarding
# Standardlogs in Dateien umleiten, wenn LOG_STDOUT=false
if [ "$LOG_STDOUT" != "true" ]; then
log "Leite Standardlogs in Dateien um..."
exec > >(tee -a "$LOG_DIR/stdout.log")
exec 2> >(tee -a "$LOG_DIR/stderr.log" >&2)
fi
log "Log-Manager erfolgreich gestartet"
}
mainShell-Skripte können helfen, Ressourcennutzung zu überwachen und zu steuern:
#!/bin/bash
# resource-monitor.sh - Container-Ressourcenüberwachung
set -eo pipefail
# Konfiguration
CHECK_INTERVAL=${CHECK_INTERVAL:-30}
MEMORY_THRESHOLD=${MEMORY_THRESHOLD:-80} # Prozent
CPU_THRESHOLD=${CPU_THRESHOLD:-90} # Prozent
DISK_THRESHOLD=${DISK_THRESHOLD:-85} # Prozent
# Hilfsfunktionen
log() {
echo "[$(date '+%Y-%m-%d %H:%M:%S')] $1"
}
get_memory_usage() {
local cgroup_memory="/sys/fs/cgroup/memory"
if [ -f "$cgroup_memory/memory.usage_in_bytes" ] && [ -f "$cgroup_memory/memory.limit_in_bytes" ]; then
local usage=$(cat "$cgroup_memory/memory.usage_in_bytes")
local limit=$(cat "$cgroup_memory/memory.limit_in_bytes")
# Wenn das Limit sehr hoch ist (z.B. Systemlimit), verwende den verfügbaren Speicher
if [ "$limit" -gt 10000000000000 ]; then
limit=$(free -b | awk '/Mem:/ {print $2}')
fi
echo $((usage * 100 / limit))
else
# Fallback, wenn Cgroups nicht verfügbar sind
free | awk '/Mem:/ {print int($3 * 100 / $2)}'
fi
}
get_cpu_usage() {
local cgroup_cpu="/sys/fs/cgroup/cpu,cpuacct"
if [ -f "$cgroup_cpu/cpuacct.usage" ]; then
# Snapshot der CPU-Nutzung
local start_time=$(date +%s%N)
local start_usage=$(cat "$cgroup_cpu/cpuacct.usage")
# Kurze Pause für Messung
sleep 1
local end_time=$(date +%s%N)
local end_usage=$(cat "$cgroup_cpu/cpuacct.usage")
# Berechnung der CPU-Nutzung in Prozent
local elapsed_time=$((end_time - start_time))
local elapsed_usage=$((end_usage - start_usage))
local cpu_count=$(grep -c '^processor' /proc/cpuinfo)
echo $((elapsed_usage * 100 / (elapsed_time * cpu_count)))
else
# Fallback auf top
top -bn1 | grep "Cpu(s)" | awk '{print int($2)}'
fi
}
get_disk_usage() {
df -h / | awk 'NR==2 {print int($5)}'
}
check_resources() {
log "Prüfe Ressourcennutzung..."
# Speichernutzung prüfen
local memory_usage=$(get_memory_usage)
log "Speichernutzung: ${memory_usage}%"
if [ "$memory_usage" -ge "$MEMORY_THRESHOLD" ]; then
log "WARNUNG: Hohe Speichernutzung erkannt!"
# Garbage Collection forcieren (Beispiel für Java-Anwendungen)
if pgrep -f java &> /dev/null; then
log "Versuche Java Garbage Collection zu forcieren..."
jcmd $(pgrep -f java) GC.run
fi
fi
# CPU-Nutzung prüfen
local cpu_usage=$(get_cpu_usage)
log "CPU-Nutzung: ${cpu_usage}%"
if [ "$cpu_usage" -ge "$CPU_THRESHOLD" ]; then
log "WARNUNG: Hohe CPU-Nutzung erkannt!"
# CPUs pausieren, die nicht kritisch sind (passen Sie dies an Ihre Anwendung an)
if [ -f "/proc/loadavg" ]; then
local load=$(cat /proc/loadavg | cut -d ' ' -f 1)
log "Aktuelle Load: $load"
fi
fi
# Festplattennutzung prüfen
local disk_usage=$(get_disk_usage)
log "Festplattennutzung: ${disk_usage}%"
if [ "$disk_usage" -ge "$DISK_THRESHOLD" ]; then
log "WARNUNG: Hohe Festplattennutzung erkannt!"
# Aufräumen von temporären Dateien
log "Räume temporäre Dateien auf..."
find /tmp -type f -atime +1 -delete
find /var/log -type f -name "*.gz" -delete
fi
}
# Einmalige Initialisierung
log "Starte Ressourcenmonitor..."
# Ermittle verfügbare Container-Limits
if [ -f "/sys/fs/cgroup/memory/memory.limit_in_bytes" ]; then
memory_limit=$(cat /sys/fs/cgroup/memory/memory.limit_in_bytes)
log "Container Memory-Limit: $((memory_limit / 1024 / 1024)) MB"
fi
if [ -f "/sys/fs/cgroup/cpu/cpu.cfs_quota_us" ] && [ -f "/sys/fs/cgroup/cpu/cpu.cfs_period_us" ]; then
cpu_quota=$(cat /sys/fs/cgroup/cpu/cpu.cfs_quota_us)
cpu_period=$(cat /sys/fs/cgroup/cpu/cpu.cfs_period_us)
if [ "$cpu_quota" -gt 0 ]; then
cpu_limit=$(bc <<< "scale=2; $cpu_quota / $cpu_period")
log "Container CPU-Limit: ${cpu_limit} Kerne"
fi
fi
# Kontinuierliche Überwachung
while true; do
check_resources
sleep "$CHECK_INTERVAL"
doneIn vielen Containern ist es wichtig, mehrere Prozesse zu überwachen und zu verwalten:
#!/bin/bash
# simple-init.sh - Einfaches Init-System für Container
set -eo pipefail
# Konfiguration
PROCESSES_FILE="/app/processes.conf"
SHUTDOWN_TIMEOUT=${SHUTDOWN_TIMEOUT:-10}
PID_DIR="/tmp/pids"
# Prozess-Informationen
declare -A PROCESS_COMMANDS
declare -A PROCESS_PIDS
declare -A PROCESS_RESTART
RUNNING=true
# Hilfsfunktionen
log() {
echo "[$(date '+%Y-%m-%d %H:%M:%S')] $1"
}
load_processes() {
log "Lade Prozessdefinitionen aus $PROCESSES_FILE..."
if [ ! -f "$PROCESSES_FILE" ]; then
log "Fehler: Prozessdefinitionsdatei nicht gefunden"
exit 1
fi
# Verzeichnis für PIDs erstellen
mkdir -p "$PID_DIR"
# Prozessdefinitionen laden
while IFS=: read -r name command restart; do
# Kommentare und leere Zeilen überspringen
[[ "$name" =~ ^#.*$ || -z "$name" ]] && continue
PROCESS_COMMANDS["$name"]="$command"
PROCESS_RESTART["$name"]="${restart:-true}"
log "Prozess definiert: $name (Neustart: ${restart:-true})"
done < "$PROCESSES_FILE"
}
start_process() {
local name=$1
local command=${PROCESS_COMMANDS[$name]}
if [ -z "$command" ]; then
log "Fehler: Unbekannter Prozess: $name"
return 1
fi
log "Starte Prozess: $name"
# Prozess im Hintergrund starten
eval "$command" &
# PID speichern
local pid=$!
PROCESS_PIDS["$name"]=$pid
echo $pid > "$PID_DIR/$name.pid"
log "Prozess $name gestartet mit PID $pid"
return 0
}
stop_process() {
local name=$1
local pid=${PROCESS_PIDS[$name]}
if [ -z "$pid" ]; then
log "Warnung: Prozess $name ist nicht aktiv"
return 0
fi
log "Stoppe Prozess $name (PID $pid)..."
# Sende SIGTERM
kill -TERM $pid 2>/dev/null || true
# Warte auf Beendigung
local timeout=$SHUTDOWN_TIMEOUT
while kill -0 $pid 2>/dev/null && [ $timeout -gt 0 ]; do
sleep 1
timeout=$((timeout - 1))
done
# Falls immer noch aktiv, SIGKILL senden
if kill -0 $pid 2>/dev/null; then
log "Prozess reagiert nicht, sende SIGKILL..."
kill -KILL $pid 2>/dev/null || true
fi
# PID-Datei entfernen
rm -f "$PID_DIR/$name.pid"
unset PROCESS_PIDS["$name"]
log "Prozess $name gestoppt"
return 0
}
start_all_processes() {
log "Starte alle Prozesse..."
for name in "${!PROCESS_COMMANDS[@]}"; do
start_process "$name"
done
log "Alle Prozesse gestartet"
}
stop_all_processes() {
log "Stoppe alle Prozesse..."
for name in "${!PROCESS_PIDS[@]}"; do
stop_process "$name"
done
log "Alle Prozesse gestoppt"
}
monitor_processes() {
while [ "$RUNNING" = "true" ]; do
for name in "${!PROCESS_PIDS[@]}"; do
local pid=${PROCESS_PIDS[$name]}
# Prüfe, ob Prozess noch läuft
if ! kill -0 $pid 2>/dev/null; then
log "Prozess $name (PID $pid) ist nicht mehr aktiv"
# PID-Datei entfernen
rm -f "$PID_DIR/$name.pid"
unset PROCESS_PIDS["$name"]
# Neustart, falls konfiguriert
if [ "${PROCESS_RESTART[$name]}" = "true" ]; then
log "Starte Prozess $name neu..."
start_process "$name"
else
log "Prozess $name wird nicht neu gestartet (Neustart deaktiviert)"
fi
fi
done
sleep 5
done
}
handle_signals() {
log "Signal $1 empfangen, beende Container..."
RUNNING=false
stop_all_processes
exit 0
}
# Hauptskript
log "Starte Container-Init-System..."
# Signal-Handler einrichten
trap 'handle_signals SIGTERM' SIGTERM
trap 'handle_signals SIGINT' SIGINT
# Prozessdefinitionen laden
load_processes
# Alle Prozesse starten
start_all_processes
# Prozesse überwachen
log "Überwache Prozesse..."
monitor_processesBeispiel für eine processes.conf-Datei:
# Format: Name:Befehl:Neustart(true|false)
app:/app/bin/server --config /app/config.yml:true
worker:/app/bin/worker:true
cron:/app/bin/cron:false
Bei der Migration bestehender Shell-Skripte in Container-Umgebungen sind einige Anpassungen notwendig:
#!/bin/bash
# container-ready-script.sh - Anpassungen für Container-Umgebungen
set -eo pipefail
# Konfiguration
# Statt absoluter Pfade relative Pfade oder Umgebungsvariablen verwenden
BASE_DIR=${BASE_DIR:-"/app"}
CONFIG_DIR=${CONFIG_DIR:-"$BASE_DIR/config"}
DATA_DIR=${DATA_DIR:-"/data"}
# Signalhandling für sauberes Herunterfahren
cleanup() {
echo "Aufräumen und Beenden..."
# Temporäre Dateien entfernen, Verbindungen schließen, etc.
exit 0
}
trap cleanup SIGTERM SIGINT
# Umgebungsvariablen statt Konfigurationsdateien
DB_HOST=${DB_HOST:-"localhost"}
DB_PORT=${DB_PORT:-5432}
DB_NAME=${DB_NAME:-"myapp"}
DB_USER=${DB_USER:-"user"}
DB_PASSWORD=${DB_PASSWORD:-"password"}
# Logging auf STDOUT/STDERR umleiten
log() {
echo "[$(date '+%Y-%m-%d %H:%M:%S')] $1"
}
error() {
echo "[$(date '+%Y-%m-%d %H:%M:%S')] FEHLER: $1" >&2
}
# Gespeicherte PID-Dateien vermeiden
run_background() {
"$@" &
bg_pid=$!
log "Prozess im Hintergrund gestartet: PID $bg_pid"
# Warte auf Beendigung
wait $bg_pid || {
error "Hintergrundprozess fehlgeschlagen: $?"
}
}
# Langlebige lokale Dateien vermeiden
use_temp_file() {
local temp_file=$(mktemp)
log "Temporäre Datei erstellt: $temp_file"
# Operation auf temporärer Datei ausführen
"$@" > "$temp_file"
# Ergebnis verarbeiten
cat "$temp_file"
# Aufräumen
rm -f "$temp_file"
}
# Endlosschleifen statt Cron-Jobs
run_scheduled_task() {
local interval=$1
shift
log "Starte geplante Aufgabe mit Intervall $interval Sekunden"
while true; do
log "Führe geplante Aufgabe aus..."
"$@"
log "Aufgabe abgeschlossen, warte $interval Sekunden..."
sleep $interval
done
}
# Hauptfunktion
main() {
log "Skript gestartet in Container-Umgebung"
# Verzeichnisse erstellen, falls sie nicht existieren
mkdir -p "$DATA_DIR"
# Beispiel für eine Hintergrundaufgabe
if [ "$RUN_WORKER" = "true" ]; then
log "Starte Worker-Prozess im Hintergrund"
run_background "$BASE_DIR/bin/worker" --config "$CONFIG_DIR/worker.yml" &
fi
# Beispiel für eine geplante Aufgabe
if [ "$RUN_SCHEDULER" = "true" ]; then
log "Starte Scheduler"
run_scheduled_task 300 "$BASE_DIR/bin/cleanup" --data-dir "$DATA_DIR" &
fi
# Hauptanwendung im Vordergrund ausführen
log "Starte Hauptanwendung..."
"$BASE_DIR/bin/server" --host "0.0.0.0" --port "$APP_PORT"
}
# Skript starten
mainFür komplexere Container-Umgebungen können Shell-Skripte Docker Compose automatisieren:
#!/bin/bash
# docker-compose-orchestrator.sh - Erweiterte Docker Compose-Orchestrierung
set -eo pipefail
# Konfiguration
COMPOSE_FILES_DIR="./docker"
ENVIRONMENTS=("dev" "test" "staging" "prod")
SERVICES_CONFIG="./services.conf"
ENV_FILE=".env"
# Parameter auswerten
ENVIRONMENT=${1:-"dev"}
ACTION=${2:-"up"}
SERVICES=${@:3} # Alle weiteren Parameter als Services interpretieren
# Hilfsfunktionen
log() {
echo "[$(date '+%Y-%m-%d %H:%M:%S')] $1"
}
usage() {
echo "Verwendung: $0 <umgebung> <aktion> [services...]"
echo ""
echo "Umgebungen: ${ENVIRONMENTS[*]}"
echo "Aktionen: up, down, restart, logs, ps, exec, build, pull, push"
exit 1
}
validate_env() {
local env=$1
if [[ ! " ${ENVIRONMENTS[@]} " =~ " ${env} " ]]; then
log "Fehler: Ungültige Umgebung: $env"
usage
fi
}
get_compose_files() {
local env=$1
local compose_files="-f $COMPOSE_FILES_DIR/docker-compose.yml"
# Umgebungsspezifische Compose-Datei hinzufügen, falls vorhanden
if [ -f "$COMPOSE_FILES_DIR/docker-compose.$env.yml" ]; then
compose_files="$compose_files -f $COMPOSE_FILES_DIR/docker-compose.$env.yml"
fi
# Override-Datei hinzufügen, falls vorhanden
if [ -f "$COMPOSE_FILES_DIR/docker-compose.override.yml" ]; then
compose_files="$compose_files -f $COMPOSE_FILES_DIR/docker-compose.override.yml"
fi
echo "$compose_files"
}
generate_env_file() {
local env=$1
local env_template="$COMPOSE_FILES_DIR/.env.template"
local env_specific="$COMPOSE_FILES_DIR/.env.$env"
log "Generiere .env-Datei für Umgebung: $env"
# Standardwerte aus Template-Datei übernehmen
if [ -f "$env_template" ]; then
cp "$env_template" "$ENV_FILE"
log "Standardwerte aus $env_template kopiert"
else
echo "# Automatisch generierte .env-Datei für $env" > "$ENV_FILE"
echo "ENVIRONMENT=$env" >> "$ENV_FILE"
log "Neue .env-Datei erstellt"
fi
# Umgebungsspezifische Werte hinzufügen/überschreiben
if [ -f "$env_specific" ]; then
log "Füge umgebungsspezifische Werte aus $env_specific hinzu"
while IFS='=' read -r key value; do
# Kommentare und leere Zeilen überspringen
[[ "$key" =~ ^#.*$ || -z "$key" ]] && continue
# Entferne Schlüssel, falls bereits vorhanden
sed -i "/^$key=/d" "$ENV_FILE"
# Füge neue Zeile hinzu
echo "$key=$value" >> "$ENV_FILE"
done < "$env_specific"
else
log "Keine umgebungsspezifische .env-Datei gefunden"
fi
}
load_services() {
if [ -f "$SERVICES_CONFIG" ]; then
log "Lade Service-Definitionen aus $SERVICES_CONFIG"
readarray -t ALL_SERVICES < <(grep -v "^#" "$SERVICES_CONFIG" | awk -F: '{print $1}')
log "Verfügbare Services: ${ALL_SERVICES[*]}"
else
log "Keine Service-Konfigurationsdatei gefunden"
ALL_SERVICES=()
fi
}
validate_services() {
if [ ${#ALL_SERVICES[@]} -eq 0 ]; then
# Wenn keine Services definiert sind, alle zulassen
return 0
fi
for service in "$@"; do
if [[ ! " ${ALL_SERVICES[@]} " =~ " ${service} " ]]; then
log "Warnung: Unbekannter Service: $service"
fi
done
}
execute_compose() {
local env=$1
local action=$2
shift 2
local services=("$@")
# Compose-Dateien bestimmen
local compose_files=$(get_compose_files "$env")
# Umgebungsvariablen für Docker Compose
export COMPOSE_PROJECT_NAME="${env}_project"
# Befehl ausführen
log "Führe Docker Compose aus: $action (Umgebung: $env)"
case "$action" in
"up")
docker-compose $compose_files up -d "${services[@]}"
;;
"down")
docker-compose $compose_files down "${services[@]}"
;;
"restart")
docker-compose $compose_files restart "${services[@]}"
;;
"logs")
docker-compose $compose_files logs -f "${services[@]}"
;;
"ps")
docker-compose $compose_files ps "${services[@]}"
;;
"exec")
if [ ${#services[@]} -lt 2 ]; then
log "Fehler: Service und Befehl müssen angegeben werden"
usage
fi
local service=${services[0]}
local command=("${services[@]:1}")
docker-compose $compose_files exec "$service" "${command[@]}"
;;
"build")
docker-compose $compose_files build "${services[@]}"
;;
"pull")
docker-compose $compose_files pull "${services[@]}"
;;
"push")
docker-compose $compose_files push "${services[@]}"
;;
*)
log "Fehler: Unbekannte Aktion: $action"
usage
;;
esac
}
# Hauptskript
validate_env "$ENVIRONMENT"
load_services
validate_services $SERVICES
generate_env_file "$ENVIRONMENT"
execute_compose "$ENVIRONMENT" "$ACTION" $SERVICES
log "Operation erfolgreich abgeschlossen"