TASIOMIND.DEV — OPERATIONAL▸▸▸FULL STACK DEVELOPER @ GWQ SERVICEPLUS AG▸▸▸FOUNDER — K8SGPT.AI▸▸▸OPEN SOURCE: ACTIVE▸▸▸DISTRIBUTED SYSTEMS / KUBERNETES / AI▸▸▸RUST + GO + PYTHON▸▸▸FIELD TESTED / STATUS — NOMINAL▸▸▸LOCATION: EUROPE/BERLIN▸▸▸TASIOMIND.DEV — OPERATIONAL▸▸▸FULL STACK DEVELOPER @ GWQ SERVICEPLUS AG▸▸▸FOUNDER — K8SGPT.AI▸▸▸OPEN SOURCE: ACTIVE▸▸▸DISTRIBUTED SYSTEMS / KUBERNETES / AI▸▸▸RUST + GO + PYTHON▸▸▸FIELD TESTED / STATUS — NOMINAL▸▸▸LOCATION: EUROPE/BERLIN▸▸▸

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.

code
#!/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 Variable line.
  • || [ -n "$line" ]: Der ODER-Teil. Falls read fehlschlägt (z.B. bei der letzten Zeile ohne \n), wird der Schleifenkörper trotzdem ausgeführt, solange die Variable $line noch 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.

code
#!/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 -t entfernt 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.