Fortschrittsbalken in Bash von Grund auf selbst bauen
AI generated
$_
#!/
Bash · Progress Bar · CLI UX · Linux
Fortschrittsbalken in Bash von Grund auf selbst bauen
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.

18 Min. Lesezeit Carriage Return · ANSI · tput · ETA Bash 4.x · 5.x · Linux · macOS

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.

11. FAQ: Fortschrittsbalken in Bash

1Wie funktioniert der Balken technisch?
Über \r, das den Cursor an den Zeilenanfang zurücksetzt. Die naechste Ausgabe überschreibt die vorherige Zeile.
2Reste der alten Zeile sichtbar?
Neue Ausgabe zu kurz. Mit fester Breite polstern oder vorher mit Leerzeichen löschen.
3Prozente ohne Fließkommazahlen?
current * 100 / total rundet ab. Letzten Schritt explizit auf 100 Prozent setzen gegen Rundungsartefakte.
4Breite an Terminal anpassen?
tput cols auslesen, Reserveplatz abziehen, Fallback auf 80 bei fehlendem Terminal.
5ETA berechnen?
Verstrichene Zeit durch erledigte Schritte, mal verbleibende Schritte. Lineare Näherung.
6Ausgabe in Logdatei umgeleitet?
[[ -t 1 ]] prüfen, bei Umleitung auf einfache Zeilenausgabe statt Carriage Return umschalten.
7Farben hinzufügen?
ANSI-Codes wie \e[32m, mit \e[0m zurücksetzen. Nur bei erkanntem Terminal einsetzen.
8Abbruch mitten im Balken?
trap auf INT und TERM mit abschließendem Zeilenumbruch hält das Terminal sauber.
9pv statt Eigenbau?
Bei reinen Datenstrom-Operationen ja, wegen eingebauter Durchsatzanzeige. Sonst Eigenbau flexibler.
10Fließkommaarithmetik nötig?
Meist nicht, Integer-Arithmetik reicht. Nur für Dezimalstellen wird awk oder bc gebraucht.