Die Fehlersuche in Skripten erscheint um vieles bodenständiger als in kompilierten Programmen. Jenseits der “bash -x”- Hausmannskost hält die Bash aber einige Überraschungen auf der Debugging-Karte bereit.
Debugging war bereits Thema beim Bash Bashing [1]. Redakteur Nils Magnus auf Reisen löste das Problem eines Infrastrukturskripts mit klassischen Bash-Debugging-Methoden. Trace beispielsweise ist altbekannt und arbeitet recht grob. Mit »bash -x Skriptname« zeigt die Bash beim Ausführen jede Zeile an.
Da die ausgedruckten Zeilen alle Variablen in aufgelöster Form enthalten, sieht das oft grafisch interessanter aus, als dass es nützlich wäre (siehe Abbildung 1). Fürs Debuggen kleiner Wrapperskripte eignet sich prima die Bash-Option »-x« , quasi das Fleischermesser des Entwicklers. Richtige Bash-Anwendungen verlangen allerdings feinere Instrumente.
Lokales Tracing
Den Ausgabewust kann der Programmierer reduzieren, indem er jene Stellen im Code, an denen er Fehler vermutet, mit
set -x[...] set +x
umgibt und dadurch den Trace-Bereich lokal einschränkt. Voraussetzung ist die einigermaßen sichere Vorstellung, wo der Fehler sitzt. Das erweist sich gelegentlich als nicht trivial, denn manche Skripte verhalten sich anders, je nachdem, von wo und wer sie aufruft. Das Kopieren eines Skripts etwa von »/usr/bin« nach »$HOME/bin« ist zwar möglich, ändert aber eventuell sein Verhalten.
Ein weiteres Problem ergibt sich beim Identifizieren des jeweils betroffenen Teils des Quellcodes. Die Debugausgabe
+ '[' 0 -gt 0 ']'
einer Skriptzeile zuzuordnen, bedarf einiger Fantasie. Leichter tut sich, wer vorher den PS4-Prompt setzt:
export PS4='+${BASH_SOURCE##*/}:${LINENO}:${FUNCNAME[0]}: '
Damit verändert sich die Zeile oben zu:
+qp_funcs.inc:74:execQuery: '[' 0 -gt 0 ']'
Der Bugjäger sieht jetzt, dass es sich um die Zeile 74 der Datei »qp_funcs.inc« handelt, also um die Funktion »execQuery« . Gerade bei Problemen in großen If-Blöcken hilft diese Information.
Der Trace-Output landet auf dem Standard-Fehlerkanal. Wer dies ändern will, setzt den Kanal für den Trace wie in Listing 1. Natürlich funktioniert die Umleitung auch ohne den Exec-Befehl über den Skriptaufruf per Kommandozeile.
Listing 1
Umleiten des Trace-Outputs
01 #!/bin/bash
02 exec 5<>trace.log
03 BASH_XTRACEFD=5
04 set -x
05 str=""
06 for i in {1..9}; do
07 str="$str$i"
08 echo "$str" >&2
09 done
10 set +x
Echtes Debugging
Die Fehlersuche über das Dotieren des Quellcodes erinnert an das Klein-Klein früherer Tage mit eingefügten »printf()« -Kommandos in C-Code. Compiler für Hochsprachen erzeugen heute per Option Debuginformationen innerhalb des Codes. Ein eigenständiges Programm – der Debugger – führt mit deren Hilfe das Programm kontrolliert aus, insbesondere auch Schritt für Schritt. Bash-Skripte dagegen laufen interpretativ und nicht kompiliert. Trotzdem gibt es für die Bash einen Debugger. Der Bash-Maintainer und der Autor des Debuggers arbeiten zusammen, viele der für den Debugger notwendigen Hooks sind auf diese Weise in den Bash-Code gelangt.
Wenn mal ein Paketrepository den Debugger nicht bereitstellt, so kompiliert und installiert der Anwender den Quellcode von [2] mit dem üblichen Dreischritt (die Datei »INSTALL« erklärt die Details). Für Spezialfunktionen bedarf es der Gegenwart des Bash-Quellcodes.
Der Bash-Debugger folgt dem Stil des Klassikers »gdb« , ist also zeilenorientiert und ohne grafische Oberfläche – und damit ein braves Schlachtross für Ritter der Kommandozeile oder ein Ackergaul für Admins, die remote per Shell tätig sind. Wer schon mit dem Gdb gearbeitet hat, kommt sofort klar (Abbildung 2).
Ähnlich wie der Gdb lässt sich der Bash-Debugger alternativ über grafische Oberflächen steuern. So existiert ein Emacs-Paket, das den Debugger in die Emacs-Infrastruktur integriert. Eine Alternative zum Bash-Debugger ist das Bash-Plugin für Eclipse [3]. Wer aber sowieso nicht schon diese extrem Ressourcen-hungrige Entwicklungsumgebung offen hat, wird vermutlich lieber Abstand wahren.
Loggen statt debuggen
Tracing und Debugging gelten zu Recht als mächtige Verfahren, um auch schwierige Fehler aufzuspüren. Allerdings gibt es Situationen, in denen sie nutzlos sind. Wer Skripte programmiert und öffentlich bereitstellt, muss bedenken, dass sein Code möglicherweise auf fremden Rechnern fehlerhaft abläuft. Das Problematische daran ist, dass normale Benutzer gewöhnlich keine Shellskript-Spezialisten sind und für die Fehlersuche weder den Quellcode gezielt ändern noch einen Debugger anwerfen werden.
Eine zweite Fußangel legen parallelisierte Skripte [4] aus. Hier tritt der Effekt auf, dass Tracing und Debugging das Timing oft so stark verändern, dass theoretisch mögliche Race Conditions am eignen PC nie auftreten. In beiden Fällen hilft Logging. Im einfachsten Fall sind das Meldungen per »echo« auf den Fehlerkanal:
echo "Konvertiere..." >&2
Manche Skripte, beispielsweise »rescan -scsi-bus.sh« aus dem »sg« -Paket (SCSI Generic Driver, [5]) schreiben hier leider auf die Standardausgabe und mischen damit normale Programmausgabe mit Logging-Meldungen. Das Skript lässt sich dann kaum mehr als Teil einer Pipe nutzen.
Log4bash (zwei unabhängige Versionen, [6], [7]) schnürt gleich eine Logging-Komplettlösung für die Bash. Pate stand Log4j [8], ein für Java entwickeltes Framework, das als Blaupause für ähnliche Pakete fast aller Programmiersprachen diente. Log4bash aus [6] implementiert aber nicht das komplette Design-Pattern von Log4j mit seinen verschiedenen hierarchischen Loggern und Appendern, sondern eine recht einfache Ausgabefunktion für Logmeldungen.
Das Log4bash-Paket aus [7] implementiert sehr nahe am Original, erscheint aber deutlich überdimensioniert. Beide Pakete machen jedoch den gleichen Fehler und geben die Meldungen auf der Standardausgabe aus – das erste Paket immer und das zweite, wenn die Konsole als Ausgabegerät definiert ist.
Kurz und knapp
Dabei ist ein eigenes Logging-Framework an dieser Stelle nicht zwingend. In der Praxis tun es auch zwei Funktionen wie in Listing 2. Das Ganze funktioniert wie folgt: Beim Skriptaufruf gibt der Anwender neben einer optionalen Verbose-Option einen String mit, der die auszugebenden Quellen – etwa Funktionen oder Ähnliches – angibt:
myscript [-v] -d "func1,func2" [...]
Im Skript selbst, zum Beispiel in der Funktion »func1« , ruft der Programmierer die »msg()« -Funktion aus Listing 2 dann etwa so auf:
msg "func1" "info" "Verarbeite $x" msg "func1" "debug" "Wert von x: $x"
Wenn die »-v« -Option gesetzt ist, gibt »msg« dank der Listingzeile 6 die erste Meldung aus. Die Ausgabe der zweite Meldung hängt davon ab, ob die Quelle (erstes Argument) im Debugstring »dsrc« aufgeführt ist. Warnungen und Fehler schreibt die Funktion unabhängig vom Message-Typ aus (Zeile 8). Die Skriptfunktion nimmt Logmeldungen auch per Pipe entgegen – alle Sätze bekommen dann den gleichen Zeitstempel. Das ist hilfreich, um die Kommandoausgaben oder Fehlermeldungen sauber in das Log zu schreiben.
Listing 2
Einfaches Logging
01 # read messages from arguments or from stdin
02
03 msg() {
04 local src="$1" type="$2"
05 shift 2
06 if [ "$type" = "info" -a $verbose -eq 0 -o \
07 "$type" = "debug" -a "${dsrc/$src}" != "${dsrc}" -o \
08 "$type" = "warning" -o "$type" = "error" ]; then
09 :
10 else
11 return
12 fi
13
14 local ts=`date "+%Y%m%d-%T"`
15
16 if [ -n "$1" ]; then
17 # take message from $@
18 put_msg "$src" "$type" "$ts" "$@"
19 else
20 # read messages from stdin
21 local line
22 while read line; do
23 put_msg "$src" "$type" "$ts" "$line"
24 done
25 fi
26 }
27
28 # print message to stderr
29
30 put_msg() {
31 local src="$1" type="$2" ts="$3"
32 shift 3
33 echo -e "[$ts] [$type] [$src] $@" >&2
34 }
Fazit
Die Komplexität beim Debuggen von Bash-Skripten ist geringer als bei zu kompilierenden Hochsprachen. Das heißt aber nicht, dass der Entwickler mit »echo« und »bash -x« auf schwierige Fälle losgehen müsste oder sollte. Denn die hier gezeigten Tools helfen ihm beim Ausrotten hartnäckiger Bugs. (jk)
Infos
- Nils Magnus, “Debugging und Konfiguration”, Bash Bashing, Folge 15: Linux-Magazin 07/11, S. 92, https://www.linux-magazin.de/Heft-Abo/Ausgaben/2011/07/Bash-Bashing
- Bash-Debugger: http://bashdb.sourceforge.net
- Bash Eclipse – Bash-Entwicklung innerhalb von Eclipse: http://sourceforge.net/projects/basheclipse/
- Bernhard Bablok, “Bash-Skripte, die Multicore-Prozessoren auslasten”: Linux-Magazin 02/09, S. 70, https://www.linux-magazin.de/Heft-Abo/Ausgaben/2009/02/Parallelarbeit
- Rescan-SCSI-Bus: http://www.garloff.de/kurt/linux/#rescan-scsi
- Log4bash – simple Version: http://www.gossiplabs.org/log4bash.html
- Log4bash – komplexere Version: http://code.google.com/p/log4bash/
- Log4j: http://logging.apache.org/log4







