HTML-Reports direkt aus Bash generieren
AI generated
$_
#!/
Bash · Reporting · Automatisierung
HTML-Reports direkt aus Bash generieren
Reporting ohne Backend, ohne Templating-Engine

Ein Cronjob, der nachts läuft, muss morgens ein lesbares Ergebnis liefern. Statt eine Log-Datei zu wälzen, erzeugt ein Bash-Report direkt eine fertige HTML-Seite mit Tabellen, Balken und Status-Ampeln, die sich per Mail verschicken oder auf einem internen Server ablegen lässt, ganz ohne PHP, Python oder eine Templating-Bibliothek.

17 Min. Lesezeit Heredoc · inline CSS · Balken ohne JS · Cron-Mailversand Bash 4.x · 5.x · Linux · macOS

1. Warum Bash für HTML-Reports genügt

Wer täglich Cronjobs betreibt, Backups prüft oder Log-Dateien auswertet, kennt das Problem: Die Ausgabe landet in einer Textdatei, die niemand freiwillig liest. Ein Bash HTML-Report löst das, ohne dass ein eigenes Backend, eine Datenbank oder eine Templating-Engine wie Jinja oder Twig eingerichtet werden muss. Bash kann HTML-Strings zusammenbauen wie jede andere Textausgabe auch, und weil ein Browser HTML unabhängig von der erzeugenden Sprache rendert, ist ein Bash HTML-Report technisch gleichwertig zu einem, der von einer Webanwendung stammt.

Der entscheidende Vorteil gegenüber einer vollwertigen Anwendung ist der fehlende Overhead. Ein Report, der nach einem nächtlichen Backup-Lauf oder einer Datenbank-Migration verschickt wird, braucht keinen Webserver, keinen Application-Server und keine Deploy-Pipeline. Das Skript, das die eigentliche Aufgabe erledigt (Backup, Sync, Health-Check), kann direkt im Anschluss den Bash HTML-Report aus denselben Variablen erzeugen, die während des Laufs bereits gesammelt wurden. Es entsteht keine zusätzliche Schicht, kein zusätzlicher Prozess und kein zusätzlicher Fehlerpunkt.

In diesem Artikel geht es darum, wie ein Bash HTML-Report von einem einfachen Heredoc-Grundgerüst bis zu einem vollständigen Dashboard mit Tabellen, Balkendiagrammen und automatischem Mailversand wächst. Alle Beispiele sind produktionsnah und laufen mit reinem Bash, ohne Python, ohne Node und ohne externe Templating-Bibliothek.

2. Grundgerüst: HTML per Heredoc erzeugen

Das Werkzeug der Wahl für einen Bash HTML-Report ist der Heredoc. Mit cat <<HTML > report.html wird ein mehrzeiliger String direkt in eine Datei geschrieben, wobei Bash-Variablen innerhalb des Heredocs normal expandiert werden. Das unterscheidet einen Heredoc von einem Quoted-Heredoc (<<'HTML'), bei dem keine Expansion stattfindet, was für statische Vorlagenteile nützlich ist, aber für einen dynamischen Report nicht infrage kommt.

Wichtig für jeden Bash HTML-Report ist die korrekte Trennung zwischen statischem Markup und dynamischen Werten. Statische Teile wie <!DOCTYPE html>, den <head>-Bereich und die Grundstruktur schreibt man einmal in ein Heredoc, dynamische Werte wie Zeitstempel, Zählwerte oder Statusfarben werden als Bash-Variablen eingebettet. Wer die Variablen nicht mit Anführungszeichen quotiert, riskiert Wortaufspaltung, sobald ein Wert Leerzeichen enthält, etwa ein Hostname mit Domainanteil oder ein Pfad.


#!/usr/bin/env bash
# generate-report.sh — build a self-contained HTML report with a heredoc
set -euo pipefail

readonly REPORT_FILE="/var/reports/backup-$(date +%Y%m%d).html"
readonly RUN_DATE="$(date '+%Y-%m-%d %H:%M:%S')"
readonly HOSTNAME_LOCAL="$(hostname -f)"

# Values collected earlier in the script (backup routine, health check, ...)
backup_status="OK"
backup_size_mb=482
backup_duration_sec=97

cat <<HTML > "$REPORT_FILE"
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>Backup Report — ${HOSTNAME_LOCAL}</title>
</head>
<body>
  <h1>Backup Report</h1>
  <p>Host: ${HOSTNAME_LOCAL}</p>
  <p>Generated: ${RUN_DATE}</p>
  <p>Status: ${backup_status}</p>
  <p>Size: ${backup_size_mb} MB</p>
  <p>Duration: ${backup_duration_sec} seconds</p>
</body>
</html>
HTML

echo "Report written to $REPORT_FILE"

Dieses Grundgerüst reicht bereits für den einfachsten Bash HTML-Report, ist aber noch weit von einem brauchbaren Dashboard entfernt. Der nächste Schritt ist, mehrere Datensätze in eine Tabelle zu bringen, statt einzelne Werte als Absätze aufzulisten. Das ist der Punkt, an dem ein Bash HTML-Report beginnt, echten Mehrwert gegenüber einer reinen Log-Datei zu bieten.

3. Daten sammeln und in Tabellenzeilen verwandeln

Die meisten Reports bestehen aus wiederholten Datensätzen: eine Zeile pro Server, pro Datenbank-Tabelle oder pro fehlgeschlagenem Job. Für einen Bash HTML-Report bedeutet das, eine Schleife über die Datenquelle zu bauen und pro Iteration eine <tr>-Zeile an einen wachsenden String anzuhängen. Der Trick dabei ist, die Zeilen nicht direkt in die Ausgabedatei zu schreiben, sondern zuerst in einer Variablen zu sammeln, damit Kopf- und Fußbereich der Tabelle sauber getrennt bleiben.

Für die Iteration selbst gilt dieselbe Regel wie bei jedem robusten Bash-Skript: Niemals for x in $(command) ohne Anführungszeichen verwenden, wenn die Werte Leerzeichen enthalten könnten. Bei einem Bash HTML-Report, der zum Beispiel Dateinamen oder Pfade auflistet, ist das besonders wichtig, weil ein kaputt formatiertes HTML-Dokument schwerer zu debuggen ist als eine fehlerhafte Konsolenausgabe.


#!/usr/bin/env bash
set -euo pipefail

# Build table rows from disk usage per directory, safely quoted
rows=""
while IFS= read -r -d '' dir; do
  size_kb="$(du -sk "$dir" 2>/dev/null | cut -f1)"
  size_mb=$(( size_kb / 1024 ))
  status_class="ok"
  (( size_mb > 5000 )) && status_class="warn"

  rows+="<tr class=\"row-${status_class}\"><td>$(basename "$dir")</td><td>${size_mb} MB</td></tr>"
done < <(find /var/www -maxdepth 1 -mindepth 1 -type d -print0)

cat <<HTML > /var/reports/disk-usage.html
<!DOCTYPE html>
<html lang="en">
<head><meta charset="UTF-8"><title>Disk Usage Report</title></head>
<body>
  <h1>Disk Usage per Directory</h1>
  <table border="1" cellpadding="6">
    <thead><tr><th>Directory</th><th>Size</th></tr></thead>
    <tbody>${rows}</tbody>
  </table>
</body>
</html>
HTML

Ein Detail, das in vielen Skripten übersehen wird: Die Variable rows muss außerhalb der Schleife initialisiert werden, sonst existiert sie erst nach der ersten Iteration und ein set -u würde beim ersten String-Anhängen fehlschlagen, falls die Schleife leer bleibt. Für einen zuverlässigen Bash HTML-Report ist das genau der Fall, der später in Abschnitt acht separat behandelt wird, wenn eine Datenquelle komplett leer ist.

4. Inline CSS für portable, eigenständige Reports

Ein Bash HTML-Report, der per Mail verschickt oder als Anhang geöffnet wird, darf sich nicht auf eine externe CSS-Datei verlassen. E-Mail-Clients blockieren externe Stylesheets standardmäßig, und ein Report, der lokal ohne Webserver geöffnet wird, findet eine relative CSS-Datei oft gar nicht. Die Lösung ist, das komplette Styling als <style>-Block direkt im <head> einzubetten, sodass die HTML-Datei als einzelne, in sich geschlossene Datei funktioniert.

Für einen Bash HTML-Report reicht ein kompaktes CSS-Grundgerüst: eine Systemschriftart, dezente Rahmen für Tabellen, und Statusfarben für OK, Warnung und Fehler. Die Statusfarben lassen sich direkt aus Bash heraus als CSS-Klassen setzen, wie im vorigen Beispiel mit row-ok und row-warn bereits angedeutet. Damit bleibt die Logik, welcher Wert als kritisch gilt, im Bash-Skript und nicht in einer separaten Konfigurationsdatei versteckt.

Ein häufiger Stolperstein: Wird das CSS selbst über eine Bash-Variable eingebettet, die geschweifte Klammern enthält, etwa .row-warn { background: #fff3cd; }, funktioniert das in einer normalen Bash-Datei problemlos, solange dieser String niemals durch eine Template-Engine wie Magentos CMS-Direktiven-Parser läuft. In einem reinen HTML-Report, der per Mail oder statischem Webserver ausgeliefert wird, ist das kein Thema.

5. Balkendiagramme ohne JavaScript zeichnen

Ein Bash HTML-Report mit reinen Zahlenwerten liest sich schwerer als einer mit visuellen Balken. Der naheliegende Reflex, eine Chart-Bibliothek wie Chart.js einzubinden, scheitert oft an denselben Restriktionen wie beim CSS: Kein externes Script in E-Mails, kein CDN-Zugriff hinter Firewalls, keine zusätzliche Abhängigkeit in einem Skript, das sonst komplett autark läuft. Die Alternative ist simpler, als es klingt: Ein Balken ist nichts anderes als ein <div> mit einer prozentualen Breite, berechnet direkt in Bash mit Integer-Arithmetik.

Die Berechnung selbst nutzt die arithmetische Erweiterung $(( wert * 100 / maximum )), die in Bash ganzzahlig rechnet. Für einen Bash HTML-Report, der zum Beispiel CPU-Auslastung, Speicherbelegung oder Fehlerquoten über mehrere Server zeigt, reicht diese einfache Prozentrechnung völlig aus, solange keine Nachkommastellen benötigt werden. Wer echte Fließkommazahlen braucht, greift auf awk oder bc zurück, ohne die Grundstruktur des Reports zu ändern.


#!/usr/bin/env bash
set -euo pipefail

# Render a CSS-only bar chart from a list of "label:value" pairs
declare -A metrics=(
  [web-01]=42
  [web-02]=78
  [web-03]=91
  [db-01]=35
)

bar_rows=""
for label in "${!metrics[@]}"; do
  value="${metrics[$label]}"
  color="#16a34a"
  (( value >= 70 )) && color="#dc2626"
  (( value >= 50 && value < 70 )) && color="#d97706"

  bar_rows+="<div style=\"margin:6px 0;\">"
  bar_rows+="<span style=\"display:inline-block;width:80px;\">${label}</span>"
  bar_rows+="<span style=\"display:inline-block;width:${value}%;max-width:400px;background:${color};height:16px;border-radius:3px;\"></span>"
  bar_rows+="<span style=\"margin-left:8px;\">${value}%</span>"
  bar_rows+="</div>"
done

cat <<HTML > /var/reports/cpu-usage.html
<!DOCTYPE html>
<html lang="en">
<head><meta charset="UTF-8"><title>CPU Usage Report</title></head>
<body>
  <h1>CPU Usage per Host</h1>
  <div style="font-family:monospace;">${bar_rows}</div>
</body>
</html>
HTML

Dieses Pattern skaliert erstaunlich gut. Ein Bash HTML-Report mit zwanzig oder dreißig Balken sieht in jedem modernen Browser und in fast jedem E-Mail-Client identisch aus, weil er ausschließlich Inline-Styles ohne externe Abhängigkeiten nutzt. Für gestapelte oder mehrfarbige Balken lässt sich dasselbe Prinzip mit mehreren <span>-Elementen nebeneinander erweitern, jedes mit seiner eigenen berechneten Breite.

6. Reports automatisiert per Cron verschicken

Ein Bash HTML-Report, der nur lokal auf der Platte liegt, wird selten angeschaut. Der eigentliche Mehrwert entsteht, wenn der Report nach jedem Lauf automatisch im Postfach des zuständigen Teams landet. Dafür eignet sich mutt mit dem Flag -e "set content_type=text/html" deutlich besser als das klassische mail-Kommando, weil mail standardmäßig Plain-Text erwartet und HTML-Tags roh anzeigen würde.

Alternativ funktioniert auch sendmail direkt mit selbst gesetzten MIME-Headern, was mehr Kontrolle gibt, aber auch mehr Boilerplate erfordert. Für einen Bash HTML-Report in einer Cron-Umgebung ist wichtig, den Versand als letzten Schritt im selben Skript zu platzieren, das den Report auch erzeugt hat, damit keine Race Condition zwischen Reporterzeugung und Versand entsteht.


#!/usr/bin/env bash
set -euo pipefail

readonly REPORT_FILE="/var/reports/nightly-$(date +%Y%m%d).html"
readonly RECIPIENT="ops-team@example.com"
readonly SUBJECT="Nightly Report — $(date +%Y-%m-%d)"

# ... report generation happens above this line ...

if command -v mutt >/dev/null 2>&1; then
  mutt -e "set content_type=text/html" \
       -s "$SUBJECT" \
       "$RECIPIENT" < "$REPORT_FILE"
else
  # Fallback: build MIME headers manually for sendmail
  {
    echo "To: $RECIPIENT"
    echo "Subject: $SUBJECT"
    echo "MIME-Version: 1.0"
    echo "Content-Type: text/html; charset=UTF-8"
    echo
    cat "$REPORT_FILE"
  } | sendmail -t
fi

echo "Report sent to $RECIPIENT"

In der Cron-Tabelle selbst reicht ein einziger Eintrag: 0 6 * * * /opt/scripts/generate-report.sh >> /var/log/report.log 2>&1. Weil der Versand Teil desselben Skripts ist, gibt es keinen zweiten Cronjob, der auf das Vorhandensein der Datei wartet oder ein zeitliches Delay abschätzen müsste. Ein Bash HTML-Report mit integriertem Versand ist damit sowohl einfacher zu betreiben als auch weniger fehleranfällig als eine mehrstufige Pipeline.

7. Wiederverwendbare Report-Sektionen als Funktionen

Sobald ein Bash HTML-Report mehr als eine Handvoll Abschnitte enthält, etwa Backup-Status, Diskbelegung und Fehlerliste in einem einzigen Dashboard, lohnt sich eine Funktion pro Sektion. Jede Funktion gibt einen HTML-String über echo zurück, der Aufrufer fängt ihn in einer Variable ab und fügt alle Sektionen am Ende in einem finalen Heredoc zusammen. Das trennt die Datenlogik jeder Sektion von der Gesamtstruktur des Reports.

Dieses Muster macht einen Bash HTML-Report auch testbar: Jede Sektions-Funktion lässt sich isoliert mit Beispieldaten aufrufen und die HTML-Ausgabe manuell prüfen, ohne den kompletten Report neu zu generieren. Für Teams, die mehrere Reports mit ähnlichem Aufbau pflegen, bietet sich zusätzlich eine gemeinsame lib/report-helpers.sh an, die über source eingebunden wird und Funktionen wie render_status_badge oder render_table für alle Reports bereitstellt.


#!/usr/bin/env bash
set -euo pipefail

render_status_badge() {
  local status="$1"
  local color="#6b7280"
  case "$status" in
    ok)   color="#16a34a" ;;
    warn) color="#d97706" ;;
    fail) color="#dc2626" ;;
  esac
  echo "<span style=\"background:${color};color:#fff;padding:2px 8px;border-radius:4px;\">${status^^}</span>"
}

render_backup_section() {
  local status="$1" size_mb="$2"
  cat <<SECTION
<h3>Backup</h3>
<p>Status: $(render_status_badge "$status")</p>
<p>Size: ${size_mb} MB</p>
SECTION
}

render_disk_section() {
  local usage_percent="$1"
  local status="ok"
  (( usage_percent > 85 )) && status="fail"
  cat <<SECTION
<h3>Disk Usage</h3>
<p>Status: $(render_status_badge "$status")</p>
<p>Used: ${usage_percent}%</p>
SECTION
}

backup_html="$(render_backup_section "ok" 482)"
disk_html="$(render_disk_section 91)"

cat <<HTML > /var/reports/combined.html
<!DOCTYPE html>
<html lang="en">
<head><meta charset="UTF-8"><title>Combined Report</title></head>
<body>${backup_html}${disk_html}</body>
</html>
HTML

8. Leere Daten und Fehlerfälle im Report abfangen

Ein Bash HTML-Report, der stillschweigend eine leere Tabelle anzeigt, wenn die eigentliche Datenquelle fehlgeschlagen ist, ist gefährlicher als gar kein Report. Wenn zum Beispiel find keine Treffer liefert, weil ein Verzeichnis nicht gemountet ist, sieht eine leere Tabelle für den Betrachter identisch aus wie eine Tabelle, die korrekt "keine Probleme gefunden" meldet. Der Report muss deshalb explizit zwischen "keine Daten, weil alles in Ordnung ist" und "keine Daten, weil die Erhebung fehlgeschlagen ist" unterscheiden.

In der Praxis bedeutet das, den Exit-Code jeder Datensammlung zu prüfen, bevor der entsprechende Abschnitt des Bash HTML-Report gerendert wird. Schlägt ein Sammelschritt fehl, wird statt der leeren Tabelle ein deutlich sichtbarer Fehlerblock mit rotem Rand eingefügt, der auf das Problem hinweist. Das verhindert, dass ein Report over mehrere Tage hinweg fälschlich grünes Licht gibt, während im Hintergrund eine Datenquelle bereits ausgefallen ist.


#!/usr/bin/env bash
set -euo pipefail

collect_disk_data() {
  local dir="$1"
  du -sk "$dir" 2>/dev/null
}

section_html=""
if data="$(collect_disk_data /mnt/backup)"; then
  size_kb="$(echo "$data" | cut -f1)"
  section_html="<p>Backup volume size: $(( size_kb / 1024 )) MB</p>"
else
  # Collection failed — surface a visible error instead of an empty table
  section_html='<div style="border:2px solid #dc2626;padding:12px;background:#fef2f2;">
    <strong>ERROR:</strong> could not read /mnt/backup — is the volume mounted?
  </div>'
fi

echo "$section_html"

Ein weiterer typischer Fehler ist das Vergessen von HTML-Escaping bei Werten, die aus externen Quellen stammen, etwa Dateinamen oder Log-Zeilen. Enthält ein Dateiname zufällig ein < oder &, kann das die Struktur des Reports beschädigen. Eine einfache sed-Ersetzung von &, < und > vor dem Einfügen in den Bash HTML-Report beugt diesem Problem zuverlässig vor.

9. Bash-Reports im Vergleich zu anderen Ansätzen

Ein Bash HTML-Report ist nicht für jeden Anwendungsfall die richtige Wahl. Für interaktive Dashboards mit Filterfunktion oder Live-Updates ist eine echte Webanwendung überlegen. Für einfache, wiederkehrende Statusmeldungen aus Cronjobs, Backup-Routinen oder Wartungsskripten ist Bash dagegen oft die pragmatischste Lösung, weil keine zusätzliche Laufzeitumgebung installiert werden muss.

Anforderung Bash HTML-Report Python/Jinja Web-Dashboard
Zusätzliche Runtime nötig Nein Python-Interpreter Server, DB, Frontend
Direkt aus Cron heraus Nativ Möglich mit venv Nicht ohne Deploy
Interaktivität, Filter Nicht vorgesehen Begrenzt Voll unterstützt
Mail-Anhang / eigenständige Datei Optimal Möglich Erfordert Export
Setup-Aufwand Minimal Mittel Hoch

Die Tabelle zeigt den Kernkompromiss: Ein Bash HTML-Report gewinnt bei Einfachheit und Portabilität, verliert aber bei Interaktivität. Für die überwiegende Mehrheit der operativen Reports, die einmal am Tag oder nach einem Ereignis verschickt werden, überwiegt der Vorteil der Einfachheit deutlich, weil niemand eine zusätzliche Anwendung warten oder absichern muss.

Mironsoft

Shell-Automatisierung, Reporting und Deployment-Infrastruktur

Reports, die niemand mehr manuell zusammenklicken muss?

Wir bauen automatisierte Bash-Reports für Backup-Status, Deployments und Server-Health, komplett integriert in eure bestehenden Cronjobs und Deployment-Skripte, ohne zusätzliche Infrastruktur.

Report-Design

HTML-Reports mit Tabellen, Balken und Statusfarben, direkt aus euren Skripten

Automatisierter Versand

Integration in Cron mit mutt oder sendmail, inklusive Fehlerfall-Erkennung

Wartbare Struktur

Wiederverwendbare Funktionen und Bibliotheken statt monolithischer Skripte

10. Zusammenfassung

Ein Bash HTML-Report löst ein sehr konkretes Problem: Rohe Log-Ausgaben aus Cronjobs und Wartungsskripten werden niemand freiwillig gelesen, ein fertiges HTML-Dokument mit Tabellen und Statusfarben dagegen schon. Das Grundgerüst ist ein Heredoc, das statisches Markup mit dynamisch expandierten Bash-Variablen kombiniert. Für wiederholte Datensätze wird eine Schleife genutzt, die Tabellenzeilen in einer Variable sammelt, bevor sie in den finalen Heredoc eingefügt werden.

Inline CSS macht den Report als eigenständige Datei portabel für Mail-Anhänge und lokale Ansicht ohne Webserver. Balkendiagramme lassen sich ohne jede JavaScript-Bibliothek als <div>-Elemente mit prozentualer Breite umsetzen, berechnet direkt in Bash mit Integer-Arithmetik. Der automatische Versand per mutt oder sendmail gehört als letzter Schritt in dasselbe Skript, das den Report erzeugt hat. Funktionen pro Sektion halten größere Reports wartbar, und eine explizite Behandlung leerer oder fehlgeschlagener Datenquellen verhindert, dass ein Report fälschlich grünes Licht gibt.

Bash HTML-Reports: Das Wichtigste auf einen Blick

Grundgerüst

Heredoc mit expandierten Variablen erzeugt HTML direkt in eine Datei, ohne Templating-Engine.

Portabilität

Inline CSS statt externer Stylesheets, damit der Report als Mail-Anhang und lokal funktioniert.

Diagramme ohne JS

Balken als div mit berechneter Prozentbreite, keine Chart-Bibliothek nötig.

Automatisierung

Versand per mutt oder sendmail direkt im selben Skript, inklusive Fehlerfall-Erkennung.

11. FAQ: Bash-HTML-Reports

1Warum HTML-Report aus Bash statt Python?
Spart den Kontextwechsel und eine zusätzliche Abhängigkeit, wenn das datensammelnde Skript ohnehin Bash ist.
2Warum Heredoc statt echo-Zeilen?
Mehrzeiliges HTML mit normaler Variablenexpansion in einem einzigen Block statt dutzender einzelner echo-Aufrufe.
3Warum kein externes Stylesheet?
E-Mail-Clients blockieren externe CSS-Dateien meist. Inline CSS im head macht die Datei eigenständig funktionsfähig.
4Balkendiagramm ohne JavaScript?
Ein div mit in Bash berechneter Prozentbreite als Inline-Style, Farbe je nach Schwellenwert per Bedingung.
5Welches Mail-Tool für HTML-Reports?
mutt mit set content_type=text/html. mail erwartet Plain-Text. Alternativ sendmail mit manuellen MIME-Headern.
6Leerer Report täuscht grünes Licht vor?
Exit-Code jeder Datensammlung prüfen, bei Fehlschlag sichtbaren Fehlerblock statt leerer Tabelle rendern.
7Werte aus Dateinamen escapen?
Ja, &, < und > vor dem Einfügen mit sed ersetzen, sonst kann die HTML-Struktur beschädigt werden.
8Wie größere Reports wartbar halten?
Eine Funktion pro Sektion, gemeinsame Helfer in einer Bibliotheksdatei per source einbinden.
9Eignet sich Bash für interaktive Dashboards?
Nein, Bash-Reports sind statisch. Für Filter und Live-Updates ist eine echte Webanwendung die richtige Wahl.
10Wie oft sollte generiert werden?
Immer nach einem prüfbaren Ereignis: Backups, Deployments oder tägliche Health-Checks.