ohne externe Bibliothek, nur mit Bordmitteln
Ein Fortschrittsbalken macht lange Batch-Skripte sichtbar statt gefühlt eingefroren. Dieser Artikel zeigt, wie ein Fortschrittsbalken mit reinem Bash entsteht, von der einfachen Prozentanzeige über Carriage Return und ANSI-Farben bis zur dynamischen Terminalbreite und ETA-Schätzung, ganz ohne Zusatzpaket.
Inhaltsverzeichnis
- 1. Warum ein eigener Fortschrittsbalken sich lohnt
- 2. Grundprinzip: Carriage Return statt neuer Zeilen
- 3. Der erste einfache Fortschrittsbalken
- 4. Prozentberechnung ohne externe Tools
- 5. Dynamische Terminalbreite mit tput
- 6. ETA-Schätzung: verbleibende Zeit berechnen
- 7. Farben und visuelle Zustände einbauen
- 8. Sonderfälle: Pipes, Logfiles und Abbruch mit trap
- 9. Eigenbau im Vergleich zu Zusatzwerkzeugen
- 10. Zusammenfassung
- 11. FAQ
1. Warum ein eigener Fortschrittsbalken sich lohnt
Ein Batch-Skript, das minutenlang ohne Ausgabe läuft, wirkt für den Anwender wie eingefroren, selbst wenn im Hintergrund alles planmäßig abläuft. Ein Fortschrittsbalken löst genau dieses Wahrnehmungsproblem, indem er kontinuierlich sichtbar macht, wie viel Arbeit bereits erledigt ist und wie viel noch aussteht. Für Skripte, die hunderte oder tausende Dateien verarbeiten, etwa bei Backups, Migrationen oder Batch-Konvertierungen, ist das kein kosmetisches Detail, sondern ein handfester Vertrauensfaktor.
Externe Werkzeuge wie pv oder Bibliotheken in anderen Sprachen bieten fertige Fortschrittsbalken, sind aber nicht überall installiert und bringen zusätzliche Abhängigkeiten mit. Ein selbst gebauter Fortschrittsbalken in reinem Bash läuft dagegen auf jedem System, das Bash selbst mitbringt, ohne ein einziges Zusatzpaket. Dieser Artikel zeigt Schritt für Schritt, wie aus einer einfachen Prozentanzeige ein vollwertiger Fortschrittsbalken mit ETA-Schätzung und dynamischer Breite wird.
2. Grundprinzip: Carriage Return statt neuer Zeilen
Das technische Fundament jedes Fortschrittsbalkens in der Shell ist das Carriage-Return-Zeichen \r. Anders als \n, das den Cursor eine Zeile weiter nach unten bewegt, setzt \r den Cursor an den Anfang der aktuellen Zeile zurück, ohne eine neue Zeile zu beginnen. Wird die nächste Ausgabe direkt danach geschrieben, überschreibt sie die vorherige Zeile vollständig, statt darunter zu erscheinen. Genau dieser Effekt erzeugt die Illusion einer sich aktualisierenden Anzeige.
Damit das funktioniert, muss echo beziehungsweise printf ohne abschließenden Zeilenumbruch aufgerufen werden, also mit printf und ohne \n am Ende, oder mit echo -n. Ein häufiger Anfängerfehler ist, die alte Zeile nicht vollständig zu überschreiben, wenn die neue Ausgabe kürzer als die vorherige ist. Reste der alten Zeile bleiben dann sichtbar. Die Lösung ist, die Zeile immer auf eine feste Breite zu polstern oder vor der neuen Ausgabe explizit mit Leerzeichen zu löschen.
#!/usr/bin/env bash
set -euo pipefail
# Carriage return demo — \r resets cursor to line start, no newline
for i in {1..5}; do
printf "\rSchritt %d von 5..." "$i"
sleep 0.5
done
printf "\n"
3. Der erste einfache Fortschrittsbalken
Aus dem Carriage-Return-Prinzip lässt sich mit wenigen Zeilen ein erster funktionierender Fortschrittsbalken bauen. Der Balken selbst besteht aus einer festen Anzahl Zeichen, typischerweise Rauten oder Blöcke, wobei die Anzahl gefüllter Zeichen proportional zum aktuellen Fortschritt wächst. Die restlichen Zeichen bis zur Gesamtbreite bleiben leer oder werden mit einem anderen Zeichen wie einem Punkt dargestellt, um den unfertigen Teil optisch abzugrenzen.
Die Kernfunktion nimmt zwei Parameter entgegen: die aktuelle Position und die Gesamtanzahl der Schritte. Aus diesen beiden Werten berechnet sie sowohl den Prozentsatz als auch die Anzahl der zu füllenden Zeichen. Wichtig ist, diese Berechnung als eigene, wiederverwendbare Funktion zu kapseln, statt die Logik direkt in der Verarbeitungsschleife zu verstreuen. Das macht den Fortschrittsbalken später leicht in andere Skripte übertragbar.
#!/usr/bin/env bash
set -euo pipefail
draw_progress_bar() {
local current=$1
local total=$2
local width=40
local percent=$(( current * 100 / total ))
local filled=$(( current * width / total ))
local empty=$(( width - filled ))
local bar
bar=$(printf '%*s' "$filled" '' | tr ' ' '#')
bar+=$(printf '%*s' "$empty" '' | tr ' ' '-')
printf "\r[%s] %3d%% (%d/%d)" "$bar" "$percent" "$current" "$total"
}
total_files=25
for ((i = 1; i <= total_files; i++)); do
draw_progress_bar "$i" "$total_files"
sleep 0.1
done
printf "\n"
4. Prozentberechnung ohne externe Tools
Bash unterstützt ausschließlich Integer-Arithmetik, was bei der Prozentberechnung für einen Fortschrittsbalken zu Rundungsproblemen führen kann. Die Division current * 100 / total rundet immer ab, weil Nachkommastellen bei ganzzahliger Division verworfen werden. Für die reine Prozentanzeige ist das meist unproblematisch, bei der Berechnung der Balkenlänge kann es aber dazu führen, dass der Balken bei 100 Prozent nicht ganz voll erscheint, wenn Rundungsfehler sich akkumulieren.
Eine robuste Lösung rundet die letzte Iteration explizit auf die volle Breite, sobald current gleich total ist, statt sich auf die Formel zu verlassen. Wer echte Nachkommastellen benötigt, etwa für eine Anzeige mit einer Dezimalstelle, muss auf awk oder bc zurückgreifen, da Bash selbst keine Fließkommaarithmetik beherrscht. Für die meisten Fortschrittsbalken reicht jedoch die einfache Integer-Berechnung völlig aus, solange der Sonderfall des letzten Schritts explizit behandelt wird.
#!/usr/bin/env bash
set -euo pipefail
calculate_percent() {
local current=$1
local total=$2
# Guard against division by zero
if (( total == 0 )); then
echo 0
return
fi
# Force exactly 100% on the final iteration, avoiding rounding artifacts
if (( current >= total )); then
echo 100
return
fi
echo $(( current * 100 / total ))
}
echo "$(calculate_percent 33 100)%" # 33%
echo "$(calculate_percent 100 100)%" # 100% — no rounding artifact
5. Dynamische Terminalbreite mit tput
Ein fest codierter Balken mit 40 Zeichen sieht auf einem breiten Terminal schmal aus und bricht auf einem schmalen Terminal möglicherweise um, was den Carriage-Return-Trick zerstört. Das Kommando tput cols liefert die aktuelle Terminalbreite in Spalten und ermöglicht es, die Balkenbreite dynamisch daran anzupassen. Ein Fortschrittsbalken, der auf die Terminalbreite reagiert, bleibt auch bei einer Größenänderung des Fensters lesbar.
Wichtig ist, von der ermittelten Breite Platz für die Prozentanzeige, die Klammern und eventuell zusätzlichen Text abzuziehen, statt die gesamte Terminalbreite für den Balken selbst zu verwenden. Läuft das Skript nicht in einem echten Terminal, etwa umgeleitet in eine Logdatei, liefert tput cols häufig einen leeren oder falschen Wert. Deshalb sollte immer ein sinnvoller Standardwert als Fallback definiert werden, falls die Ermittlung fehlschlägt.
#!/usr/bin/env bash
set -euo pipefail
get_bar_width() {
local reserved=20 # space for percent, brackets and counter text
local cols
cols=$(tput cols 2>/dev/null) || cols=80
[[ -z "$cols" ]] && cols=80
local width=$(( cols - reserved ))
(( width < 10 )) && width=10 # never go below a usable minimum
echo "$width"
}
bar_width=$(get_bar_width)
echo "Balkenbreite fuer dieses Terminal: $bar_width Zeichen"
6. ETA-Schätzung: verbleibende Zeit berechnen
Eine reine Prozentanzeige beantwortet nicht die Frage, die Anwender in der Praxis am meisten interessiert: Wie lange dauert es noch? Eine ETA-Schätzung, also die geschätzte verbleibende Zeit, lässt sich aus der bisher verstrichenen Zeit und dem bisherigen Fortschritt hochrechnen. Die Formel ist einfach: verstrichene Zeit geteilt durch die Anzahl bereits erledigter Schritte ergibt die durchschnittliche Zeit pro Schritt, multipliziert mit den verbleibenden Schritten ergibt die geschätzte Restzeit.
Diese Schätzung ist nur eine Näherung und wird bei stark schwankender Verarbeitungsgeschwindigkeit ungenau, etwa wenn frühe Dateien klein und spätere Dateien groß sind. Für die meisten praktischen Anwendungsfälle, bei denen die Verarbeitungszeit pro Element relativ konstant ist, liefert die einfache lineare Hochrechnung trotzdem einen brauchbaren Richtwert. Die Startzeit wird dazu einmalig vor der Schleife mit $(date +%s) als Unix-Timestamp erfasst und bei jeder Iteration mit der aktuellen Zeit verglichen.
#!/usr/bin/env bash
set -euo pipefail
start_time=$(date +%s)
total=20
for ((i = 1; i <= total; i++)); do
sleep 0.2 # simulate work
now=$(date +%s)
elapsed=$(( now - start_time ))
if (( i > 0 && elapsed > 0 )); then
remaining=$(( elapsed * (total - i) / i ))
else
remaining=0
fi
printf "\r[%d/%d] Verstrichen: %ds, ETA: ~%ds " "$i" "$total" "$elapsed" "$remaining"
done
printf "\n"
7. Farben und visuelle Zustände einbauen
Ein monochromer Fortschrittsbalken lässt sich mit ANSI-Escape-Codes leicht um Farbe erweitern, um zusätzliche Information zu transportieren. Grün für laufenden Fortschritt, Gelb ab einer bestimmten Fehlerquote und Rot bei kritischen Zuständen sind ein bewährtes Muster, das dem Anwender auf einen Blick zeigt, ob ein Lauf normal verläuft. Die Codes selbst sind kurze Escape-Sequenzen wie \e[32m für Grün, gefolgt von \e[0m, um die Formatierung wieder zurückzusetzen.
Wichtig ist, Farbcodes nicht hart zu verdrahten, sondern über Variablen zu kapseln und ihre Verwendung von der Verfügbarkeit eines Terminals abhängig zu machen. Läuft ein Skript in einer Umgebung ohne Terminalunterstützung, etwa in einer CI-Pipeline mit reiner Log-Ausgabe, sollten Escape-Codes deaktiviert werden, da sie sonst als kryptische Zeichenfolgen im Log erscheinen, statt korrekt gerendert zu werden.
8. Sonderfälle: Pipes, Logfiles und Abbruch mit trap
Ein Fortschrittsbalken, der mit Carriage Return arbeitet, verhält sich in einer Logdatei anders als im Terminal: Statt einer sich aktualisierenden Zeile entsteht eine lange Kette von \r-Zeichen, die in vielen Editoren nicht sauber dargestellt wird. Der robuste Ansatz prüft mit [[ -t 1 ]], ob stdout an ein echtes Terminal angeschlossen ist, und schaltet bei einer Umleitung in eine Datei auf eine einfachere, zeilenweise Ausgabe um, etwa nur bei jedem zehnten Prozent.
Ein zweiter wichtiger Aspekt ist das Verhalten bei Abbruch. Bricht der Anwender das Skript mit Strg-C mitten in der Balkenanzeige ab, bleibt der Cursor unter Umständen mitten in der Zeile stehen, und die nächste Terminal-Ausgabe erscheint an einer ungünstigen Position. Ein trap auf INT und TERM, der vor dem Beenden einen abschließenden Zeilenumbruch ausgibt, sorgt dafür, dass das Terminal in einem sauberen Zustand zurückbleibt, unabhängig davon, wann der Abbruch erfolgt.
#!/usr/bin/env bash
set -euo pipefail
# Ensure a clean terminal state even if the user interrupts mid-bar
trap 'printf "\n"; echo "Abgebrochen." >&2; exit 130' INT TERM
is_tty=0
[[ -t 1 ]] && is_tty=1
for ((i = 1; i <= 50; i++)); do
if (( is_tty )); then
printf "\r[%3d%%]" "$i"
elif (( i % 10 == 0 )); then
echo "Fortschritt: ${i}%"
fi
sleep 0.05
done
(( is_tty )) && printf "\n"
9. Eigenbau im Vergleich zu Zusatzwerkzeugen
Ein selbst gebauter Fortschrittsbalken ist nicht in jedem Fall die beste Wahl. Für einfache Datei-Transfers bietet pv eine ausgereifte, sofort einsatzbereite Lösung, die zusätzlich Durchsatz in Megabyte pro Sekunde anzeigt. Der Eigenbau lohnt sich vor allem dort, wo die Fortschrittslogik eng mit eigener Verarbeitungslogik verzahnt ist und ein externes Tool nicht ohne Umwege eingebunden werden kann.
| Ansatz | Abhängigkeit | Flexibilität | Bester Einsatzfall |
|---|---|---|---|
| Eigenbau (printf + \r) | Keine | Vollständig anpassbar | Eigene Batch-Verarbeitung mit ETA und Farben |
| pv | Extern (Paketmanager) | Nur für Datenströme | Datei-Transfers, Pipes mit Durchsatzanzeige |
| gum spin | Extern (separat installieren) | Nur unbestimmter Fortschritt | Einzelner Befehl ohne bekannten Endpunkt |
| Einfaches echo pro Schritt | Keine | Gering, unübersichtlich bei vielen Zeilen | Sehr kurze Skripte mit wenigen Schritten |
Für die meisten internen Wartungsskripte ist der Eigenbau die pragmatischste Lösung, weil er ohne Zusatzinstallation auskommt und sich exakt an die eigene Verarbeitungslogik anpassen lässt. Sobald allerdings reine Datenströme wie Datei-Kopien im Vordergrund stehen, ist pv die schnellere und robustere Alternative, weil es Durchsatz und Zeitschätzung bereits eingebaut mitbringt.
Mironsoft
Shell-Automatisierung und CLI-Tooling für Entwicklerteams
Lange Batch-Skripte, die wie eingefroren wirken?
Wir bauen robuste Fortschrittsanzeigen mit ETA-Schätzung und dynamischer Terminalbreite direkt in eure Bash-Skripte ein, ohne Zusatzabhängigkeiten und mit sauberem Verhalten in Logfiles.
Fortschrittsanzeigen
Fortschrittsbalken mit ETA und dynamischer Breite in Batch-Skripte einbauen
Log-taugliches Verhalten
Ausgaben, die auch bei Umleitung in Logdateien sauber lesbar bleiben
Batch-Verarbeitung
Robuste Verarbeitungsskripte für große Datei- und Datensatzmengen
10. Zusammenfassung
Ein Fortschrittsbalken in reinem Bash basiert auf einem einzigen technischen Prinzip: dem Carriage-Return-Zeichen \r, das den Cursor an den Zeilenanfang zurücksetzt und so eine sich aktualisierende Anzeige erzeugt. Aus dieser Grundlage entsteht Schritt für Schritt ein vollwertiges Werkzeug, mit Prozentberechnung über Integer-Arithmetik, dynamischer Breite über tput cols und einer ETA-Schätzung aus verstrichener Zeit und bisherigem Fortschritt.
Entscheidend für den Produktionseinsatz sind die Sonderfälle: Farbe nur einsetzen, wenn ein echtes Terminal vorhanden ist, bei Umleitung in Logfiles auf eine einfachere zeilenweise Ausgabe umschalten, und mit trap sicherstellen, dass ein Abbruch mitten in der Anzeige das Terminal nicht in einem unsauberen Zustand zurücklässt. Wer diese Details beachtet, bekommt einen Fortschrittsbalken, der sich in jeder Umgebung genauso zuverlässig verhält wie eine ausgereifte externe Bibliothek, ganz ohne zusätzliche Abhängigkeit.
Fortschrittsbalken in Bash — Das Wichtigste auf einen Blick
Grundprinzip
printf "\r..." ohne abschließenden Zeilenumbruch überschreibt die aktuelle Zeile bei jedem Aufruf.
Dynamische Breite
tput cols mit Fallback auf 80, Reserveplatz für Prozentanzeige und Klammern abziehen.
ETA-Schätzung
Verstrichene Zeit durch erledigte Schritte, multipliziert mit verbleibenden Schritten, als lineare Näherung.
Robustheit
[[ -t 1 ]] für Terminal-Erkennung, trap für sauberes Verhalten bei Abbruch mit Strg-C.