Aus Linux-Magazin 10/2012

Shellskripte aus der Stümper-Liga – Folge 22: Debuggen von Bash-Skripten

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.

Abbildung 1: Meldungen im Übermaß: Mit »bash -x« schaltet die Shell ihre Tracefunktion ein.

Abbildung 1: Meldungen im Übermaß: Mit »bash -x« schaltet die Shell ihre Tracefunktion ein.

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.

Abbildung 2: Die Bedienung des Bash-Debuggers ähnelt der von Gdb.

Abbildung 2: Die Bedienung des Bash-Debuggers ähnelt der von Gdb.

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

  1. 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
  2. Bash-Debugger: http://bashdb.sourceforge.net
  3. Bash Eclipse – Bash-Entwicklung innerhalb von Eclipse: http://sourceforge.net/projects/basheclipse/
  4. 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
  5. Rescan-SCSI-Bus: http://www.garloff.de/kurt/linux/#rescan-scsi
  6. Log4bash – simple Version: http://www.gossiplabs.org/log4bash.html
  7. Log4bash – komplexere Version: http://code.google.com/p/log4bash/
  8. Log4j: http://logging.apache.org/log4

Der Autor

Bernhard Bablok betreut bei der Allianz Managed&Operations Services SE ein großes Datawarehouse mit technischen Performancemessdaten von Mainframes bis zu Servern. Wenn er nicht Musik hört, mit dem Radl oder zu Fuß unterwegs ist, beschäftigt er sich mit Themen rund um Linux und Objektorientierung.

DIESEN ARTIKEL ALS PDF KAUFEN
EXPRESS-KAUF ALS PDFUmfang: 3 HeftseitenPreis €0,99
(inkl. 19% MwSt.)
LINUX-MAGAZIN KAUFEN
EINZELNE AUSGABE Print-Ausgaben Digitale Ausgaben
ABONNEMENTS Print-Abos Digitales Abo
TABLET & SMARTPHONE APPS Readly Logo
E-Mail Benachrichtigung
Benachrichtige mich zu:
0 Kommentare
Älteste
Neuste Beste Bewertung
Inline Feedbacks
Alle Kommentare anzeigen
Nach oben