tput und ANSI-Codes portabel einsetzen, ohne kaputte Logdateien
Rohe ANSI-Escape-Codes wie \e[31m sehen im eigenen Terminal gut aus, brechen aber, sobald ein Skript auf einem anderen Terminaltyp läuft oder seine Ausgabe in eine Logdatei umgeleitet wird. tput fragt stattdessen die tatsächlichen Fähigkeiten des aktuellen Terminals ab und liefert die passende Escape-Sequenz, während ein einfacher isatty-Check dafür sorgt, dass Farbe automatisch verschwindet, sobald niemand mehr sie interaktiv liest.
Inhaltsverzeichnis
- 1. Warum rohe ANSI-Escape-Codes in Skripten fragil sind
- 2. tput-Grundlagen: setaf, bold und sgr0
- 3. Terminal-Fähigkeiten vor der Ausgabe prüfen: tput colors
- 4. isatty-Check: [ -t 1 ] und wann Ausgabe kein Terminal ist
- 5. Farbausgabe automatisch deaktivieren bei Umleitung in eine Datei
- 6. Eine wiederverwendbare Farbfunktion für das ganze Skript bauen
- 7. Der NO_COLOR-Standard und weitere Konventionen
- 8. Fallstricke: tput in Subshells, fehlendes terminfo und CI-Umgebungen
- 9. tput im Vergleich zu rohen ANSI-Codes
- 10. Zusammenfassung
- 11. FAQ
1. Warum rohe ANSI-Escape-Codes in Skripten fragil sind
Ein hart kodierter Escape-Code wie \e[31m für Rot geht davon aus, dass das Zielterminal genau diese Sequenz versteht. Das trifft auf die meisten modernen Terminals wie xterm oder gnome-terminal zwar zu, aber nicht zwingend auf ältere Terminals, serielle Konsolen oder minimale Umgebungen, in denen der genaue Funktionsumfang vom Wert der Umgebungsvariable TERM abhängt und stark variieren kann.
Noch problematischer wird es, sobald die Ausgabe eines Skripts nicht mehr direkt im Terminal landet, sondern in eine Datei umgeleitet oder an ein anderes Programm weitergeleitet wird. Rohe Escape-Codes tauchen dann als kryptische Steuerzeichen mitten im Text auf, stören das Parsen mit anderen Tools und machen Logdateien unnötig schwer lesbar, weil sie blind gesendet werden, unabhängig davon, ob überhaupt ein Terminal zuschaut.
2. tput-Grundlagen: setaf, bold und sgr0
tput schlägt in der terminfo-Datenbank nach, welche Escape-Sequenz für den in $TERM hinterlegten Terminaltyp tatsächlich die gewünschte Wirkung erzielt, und gibt genau diese Sequenz aus. tput setaf 1 liefert die Sequenz für Rot als Vordergrundfarbe, tput bold aktiviert fetten Text, und tput sgr0 setzt alle Textattribute zurück auf den Ausgangszustand, zuverlässiger als ein manuell zusammengebauter Reset-Code.
Weil jeder tput-Aufruf einen eigenen Subprozess startet, lohnt es sich, die benötigten Sequenzen einmal am Skriptanfang in Variablen zu speichern und diese Variablen danach beliebig oft wiederzuverwenden, statt tput in einer Schleife bei jeder Ausgabezeile erneut aufzurufen. Das spart merklich Prozessstarts, besonders bei Skripten mit vielen farbigen Log-Zeilen.
#!/usr/bin/env bash
set -euo pipefail
RED=$(tput setaf 1)
GREEN=$(tput setaf 2)
BOLD=$(tput bold)
RESET=$(tput sgr0)
echo "${BOLD}${GREEN}Deployment successful${RESET}"
echo "${RED}Error: connection refused${RESET}"
3. Terminal-Fähigkeiten vor der Ausgabe prüfen: tput colors
tput colors liefert die Anzahl der Farben, die das aktuelle Terminal laut terminfo-Eintrag unterstützt, üblicherweise 8, 16 oder 256. Ein Skript, das Farbausgabe anbietet, sollte diesen Wert vor dem ersten setaf-Aufruf prüfen, statt blind von Farbunterstützung auszugehen, weil ein Aufruf mit einem zu hohen Farbindex auf einem Terminal mit weniger unterstützten Farben zu unerwarteten Ergebnissen führen kann.
Ein robustes Prüfmuster lautet if [[ $(tput colors 2>/dev/null || echo 0) -ge 8 ]]; then, das sowohl den Fall abdeckt, dass tput colors einen validen Wert unter 8 liefert, als auch den Fall, dass der Aufruf ganz fehlschlägt, etwa weil kein passender terminfo-Eintrag existiert.
#!/usr/bin/env bash
set -euo pipefail
color_count="$(tput colors 2>/dev/null || echo 0)"
if [[ "$color_count" -ge 8 ]]; then
echo "Terminal supports at least 8 colors ($color_count total)"
else
echo "Terminal has no usable color support"
fi
4. isatty-Check: [ -t 1 ] und wann Ausgabe kein Terminal ist
Der Ausdruck [ -t 1 ] prüft, ob der Datei-Deskriptor 1, also stdout, mit einem echten Terminal verbunden ist. Wird die Ausgabe eines Skripts mit > log.txt in eine Datei umgeleitet oder mit | an ein anderes Programm weitergereicht, liefert dieser Test false, weil stdout dann kein Terminal mehr ist, sondern eine reguläre Datei oder eine Pipe.
Für zuverlässige Farbausgabe gehören tput colors und [ -t 1 ] zusammen: Nur wenn beide Bedingungen erfüllt sind, das Terminal Farbe unterstützt und die Ausgabe tatsächlich interaktiv gelesen wird, sollten Escape-Sequenzen überhaupt gesendet werden. Eine der beiden Prüfungen allein reicht nicht aus, um Fehlausgaben in allen Situationen zuverlässig zu vermeiden.
#!/usr/bin/env bash
set -euo pipefail
supports_color() {
[[ -t 1 ]] || return 1
local n
n="$(tput colors 2>/dev/null || echo 0)"
[[ "$n" -ge 8 ]]
}
if supports_color; then
echo "$(tput setaf 2)Color enabled$(tput sgr0)"
else
echo "Color disabled (no TTY or no color support)"
fi
5. Farbausgabe automatisch deaktivieren bei Umleitung in eine Datei
Statt an jeder einzelnen Ausgabezeile eine Bedingung für Farbe einzubauen, ist es deutlich wartungsfreundlicher, die Farbvariablen einmal am Skriptanfang entweder mit den echten tput-Sequenzen oder mit leeren Strings zu belegen, je nachdem, ob [ -t 1 ] und tput colors erfolgreich sind. Der restliche Skriptcode verwendet dann überall dieselben Variablen wie $RED und $RESET, ohne selbst noch einmal zu prüfen, ob Farbe aktiv ist.
Zusätzlich zur automatischen Erkennung erwarten viele Nutzer explizite Override-Flags wie --no-color oder --color=always, um die automatische Erkennung bei Bedarf zu übersteuern, etwa wenn eine CI-Pipeline die Ausgabe trotz fehlendem echtem Terminal farbig einfärben soll, weil ein nachgeschalteter Log-Viewer die Codes selbst wieder interpretiert.
#!/usr/bin/env bash
set -euo pipefail
force_color=0
no_color=0
for arg in "$@"; do
case "$arg" in
--color=always) force_color=1 ;;
--no-color) no_color=1 ;;
esac
done
if [[ "$no_color" -eq 1 ]]; then
use_color=0
elif [[ "$force_color" -eq 1 ]]; then
use_color=1
elif [[ -t 1 ]] && [[ "$(tput colors 2>/dev/null || echo 0)" -ge 8 ]]; then
use_color=1
else
use_color=0
fi
if [[ "$use_color" -eq 1 ]]; then
RED=$(tput setaf 1); GREEN=$(tput setaf 2); RESET=$(tput sgr0)
else
RED=""; GREEN=""; RESET=""
fi
echo "${GREEN}Ready${RESET}"
6. Eine wiederverwendbare Farbfunktion für das ganze Skript bauen
Statt Farbvariablen direkt in jeder Ausgabezeile zu kombinieren, bündeln kleine Wrapper-Funktionen wie log_info() und log_error() die komplette Formatierung an einer einzigen Stelle. Der restliche Code ruft nur noch log_info "Deployment gestartet" auf, ohne selbst mit Escape-Sequenzen in Berührung zu kommen, was Tippfehler bei Farbnamen und vergessene RESET-Aufrufe von vornherein ausschließt.
Bei mehreren Deployment-Skripten im selben Projekt lohnt es sich, diese Funktionen in eine einzige, gemeinsam eingebundene Datei auszulagern, die jedes Skript am Anfang mit source einbindet. So bleibt das Farbschema über alle Skripte hinweg konsistent, und eine spätere Anpassung, etwa ein anderer Grundton, muss nur an einer Stelle gepflegt werden.
#!/usr/bin/env bash
set -euo pipefail
log_info() { echo "${GREEN}[INFO]${RESET} $*"; }
log_error() { echo "${RED}[ERROR]${RESET} $*" >&2; }
log_info "Starting deployment"
log_error "Configuration file not found"
7. Der NO_COLOR-Standard und weitere Konventionen
Die Community-Konvention NO_COLOR (siehe no-color.org) legt fest, dass jedes gesetzte, nicht-leere Wert dieser Umgebungsvariable jede Farbausgabe eines Programms deaktivieren soll, unabhängig davon, was das Terminal eigentlich unterstützen würde. Viele moderne CLI-Tools respektieren diese Variable inzwischen, weshalb es sich lohnt, sie auch in eigenen Bash-Skripten zu berücksichtigen.
Die sinnvolle Prüfreihenfolge ist: zuerst NO_COLOR respektieren und Farbe bei gesetzter Variable sofort abschalten, danach explizite Skript-Flags wie --no-color auswerten, und erst danach die automatische Erkennung über [ -t 1 ] und tput colors greifen lassen. So gewinnt immer die explizitere Nutzerabsicht gegenüber der automatischen Erkennung.
#!/usr/bin/env bash
set -euo pipefail
if [[ -n "${NO_COLOR:-}" ]]; then
use_color=0
elif [[ -t 1 ]] && [[ "$(tput colors 2>/dev/null || echo 0)" -ge 8 ]]; then
use_color=1
else
use_color=0
fi
echo "use_color=$use_color"
8. Fallstricke: tput in Subshells, fehlendes terminfo und CI-Umgebungen
Minimale Docker-Basis-Images enthalten oft keine vollständige terminfo-Datenbank, wodurch tput-Aufrufe fehlschlagen oder eine Fehlermeldung auf stderr ausgeben können, statt einfach eine leere Sequenz zu liefern. Jeder tput-Aufruf in einem portablen Skript sollte deshalb mit 2>/dev/null abgesichert und mit einem sinnvollen Fallback-Wert wie || echo 0 kombiniert werden.
CI-Umgebungen setzen TERM häufig auf dumb oder lassen die Variable ganz leer, wodurch tput colors einen niedrigen oder fehlerhaften Wert zurückgibt. Ein Skript, das diesen Fall nicht abfängt, könnte sonst mit einem harten Fehler die ganze Pipeline stoppen, nur weil eine kosmetische Funktion wie Farbausgabe nicht verfügbar ist, weshalb der Fallback-Pfad immer getestet werden sollte.
9. tput im Vergleich zu rohen ANSI-Codes
Die Wahl zwischen rohen ANSI-Codes und tput ist im Grunde eine Abwägung zwischen minimalem Aufwand und echter Portabilität. Für ein Wegwerf-Skript, das ausschließlich im eigenen Terminal läuft, mögen hart kodierte Codes ausreichen, für jedes Skript, das weitergegeben, in CI-Pipelines eingesetzt oder in eine Logdatei geschrieben werden könnte, ist die Kombination aus tput, isatty-Check und NO_COLOR-Unterstützung die robustere Wahl.
| Ansatz | Portabilität | Terminal-Erkennung | Empfehlung |
|---|---|---|---|
| Rohe ANSI-Codes fest kodiert | Gering | Keine | Nur für kurze, rein interaktive Wegwerf-Skripte |
tput ohne Prüfung |
Mittel | Keine automatische Deaktivierung | Interaktive Skripte ohne Umleitungsrisiko |
tput + isatty + NO_COLOR |
Hoch | Vollständig, inklusive Nutzer-Override | Produktionsreife CLI-Tools und Deployment-Skripte |
| Drittanbieter-Farbbibliotheken | Hoch | Abhängig von der Bibliothek | Große CLI-Projekte mit vielen Ausgabeformaten |
Mironsoft
Shell-Automatisierung, DevOps-Tooling und Deployment-Infrastruktur
Shell-Skripte, die in der Produktion zuverlässig laufen?
Wir analysieren bestehende Bash-Skripte, erkennen fragile Muster und ersetzen sie durch robuste Bash-Patterns: mit vollständiger Fehlerbehandlung, Logging und sicherer Parallelisierung für euren Deployment-Stack.
Code-Review
ShellCheck-Analyse und manuelle Prüfung auf kritische Bash-Pattern-Verstöße.
Refactoring
Fehlerbehandlung, Logging und sichere Dateioperationen nachrüsten.
CI-Integration
ShellCheck und BATS in Pipelines integrieren und Regressionstests aufbauen.
10. Zusammenfassung
tput und Farbausgabe in Bash: Das Wichtigste auf einen Blick
tput-Grundlagen
tput setaf, tput bold und tput sgr0 fragen die tatsächliche terminfo-Fähigkeit ab, statt Escape-Codes hart zu kodieren.
Fähigkeiten prüfen
tput colors liefert die Anzahl unterstützter Farben, mit 2>/dev/null || echo 0 gegen fehlende terminfo-Einträge abgesichert.
isatty-Check
[ -t 1 ] erkennt, ob stdout an ein echtes Terminal geht, und deaktiviert Farbe automatisch bei Umleitung in eine Datei.
NO_COLOR beachten
Die NO_COLOR-Umgebungsvariable und explizite --no-color/--color=always-Flags haben Vorrang vor der automatischen Erkennung.