Mehrzeilige Eingaben in Bash einlesen: `read` vs. `readarray`
July 29, 2022
Das zeilenweise Einlesen von der Standardeingabe (stdin) ist eine häufige Aufgabe in Bash-Skripten, insbesondere in Wettbewerbsprogrammierungs-Plattformen wie HackerRank. Ein klassisches Problem dabei ist, dass eine while read-Schleife die letzte Zeile der Eingabe ignoriert, wenn diese nicht mit einem Zeilenumbruch (\n) endet.
Diese Anleitung zeigt zwei robuste Methoden, um dieses Problem zu umgehen: eine erweiterte read-Schleife und den readarray-Befehl.
Das Problem: read und die fehlende letzte Zeile
Eine Standard-while read-Schleife beendet sich, sobald read einen nicht-null Exit-Code zurückgibt, was passiert, wenn das Ende der Datei (EOF) erreicht wird. Wenn die letzte Zeile jedoch keinen abschließenden Zeilenumbruch hat, liest read den Inhalt, erreicht aber sofort EOF und gibt einen nicht-null Exit-Code zurück, wodurch der Schleifenkörper für diese letzte Zeile nicht mehr ausgeführt wird.
Methode 1: Robuste while read-Schleife
Die Lösung besteht darin, die Schleifenbedingung so zu erweitern, dass sie auch dann noch einmal ausgeführt wird, wenn read fehlschlägt, aber die Variable dennoch Inhalt hat.
#!/usr/bin/env bash
# Beispiel: Berechnet den Durchschnitt von n Zahlen, die über stdin eingegeben werden
# Liest die erste Zeile, um die Anzahl der folgenden Zahlen zu erfahren
read -r TOTAL_NUMBERS
SUM=0
COUNT=0
# Die Schleife läuft weiter, solange `read` erfolgreich ist ODER die Variable `$line` nicht leer ist
while read -r line || [ -n "$line" ]; do
# Addiert die gelesene Zahl zur Summe
SUM=$((SUM + line))
COUNT=$((COUNT + 1))
done
# Führt die Berechnung mit 'bc' für Fließkommagenauigkeit durch
AVERAGE=$(echo "scale=3; $SUM / $COUNT" | bc -l)
# Gibt das Ergebnis formatiert aus
printf "%.3f\n" "$AVERAGE"
read -r line: Liest eine Zeile in die Variableline.|| [ -n "$line" ]: Der ODER-Teil. Fallsreadfehlschlägt (z.B. bei der letzten Zeile ohne\n), wird der Schleifenkörper trotzdem ausgeführt, solange die Variable$linenoch Inhalt hat.
Methode 2: readarray (oder mapfile)
Der readarray-Befehl (auch als mapfile bekannt) ist eine modernere und oft einfachere Lösung. Er liest alle Zeilen von stdin auf einmal in ein Bash-Array. Dies ist in der Regel performanter für große Eingaben, da nicht für jede Zeile ein neuer Prozess gestartet wird.
#!/usr/bin/env bash
# Beispiel: Gleiche Aufgabe wie oben
read -r TOTAL_NUMBERS
SUM=0
# Liest alle nachfolgenden Zeilen in das Array 'lines'
readarray -t lines
# Iteriert über das Array
for line in "${lines[@]}"; do
SUM=$((SUM + line))
done
AVERAGE=$(echo "scale=3; $SUM / ${#lines[@]}" | bc -l)
printf "%.3f\n" "$AVERAGE"
readarray -t lines: Die Option-tentfernt den abschließenden Zeilenumbruch von jeder Zeile.
Fazit
- Verwenden Sie die robuste
while read-Schleife, wenn Sie große Dateien Zeile für Zeile verarbeiten müssen, um den Speicherverbrauch gering zu halten. - Verwenden Sie
readarray, wenn die gesamte Eingabe bequem in den Speicher passt. Es ist oft einfacher zu schreiben und schneller in der Ausführung.
Beide Methoden lösen zuverlässig das Problem der fehlenden letzten Zeile und sind wesentliche Werkzeuge für die Verarbeitung von stdin in Bash.