- Wenn sich ein Bash-Skript anders als erwartet verhält, lässt sich die Ursache oft schon leichter finden, indem man sich einfach die tatsächlich ausgeführten Befehle anzeigen lässt
set -xgibt jede Zeile nach der Variablenexpansion aus und zeigt so, welche Befehle das Skript tatsächlich ausführt- Wenn man es auf der Kommandozeile mit
bash -x script.shausführt, hat das denselben Effekt, als würde man am Anfang vonscript.shset -xeinfügen - Mit dem
DEBUG-Trap zusammen mitreadkann man vor der Ausführung jeder Zeile anhalten und Dateiname, Zeilennummer und den nächsten Befehl prüfen - Die Funktion
die() { echo $1 >&2; exit 1; }vereinfacht den Ablauf, nach einem fehlgeschlagenen Befehl eine Standardfehlermeldung auszugeben und zu beenden
Den Ausführungsablauf mit eigenen Augen prüfen
set -xgibt die Zeilen aus, die das Skript ausführt, und zeigt Variablen mit ihren expandierten Werten- Man kann es verwenden, indem man
set -xam Anfang des Skripts einfügt - Dasselbe Verhalten ist auch auf der Kommandozeile möglich
$ bash -x script.sh- Das ist gleichbedeutend damit, am Anfang von
script.shset -xeinzufügen
Bei jeder Zeile anhalten und prüfen
- Der
DEBUG-Trap wird ausgeführt, bevor jede Codezeile ausgeführt wird - Wenn man den folgenden Code am Anfang des Skripts einfügt, wartet es vor der Ausführung des nächsten Befehls auf die Eingabe von Enter
trap '(read -p "\[$BASH_SOURCE: $LINENO] $BASH_COMMAND")' DEBUGread -pgibt eine Meldung aus und wartet auf die Eingabe von Enter$BASH_SOURCEist der Name der Skriptdatei$LINENOist die Zeilennummer$BASH_COMMANDist der Befehl, der als Nächstes ausgeführt wird
Beim Fehlschlag eine Meldung ausgeben und beenden
- Die Funktion
diekann verwendet werden, um bei einem Befehlsfehler eine Meldung auszugeben und das Programm zu beendendie() { echo $1 >&2; exit 1; }- Man hängt sie einfach an einen Befehl an, der fehlschlagen kann, etwa so:
some_command || die "oh no!"
- Diese Funktion sendet die Meldung an stderr und beendet sich mit
exit 1
1 Kommentare
Meinungen auf Hacker News
Überall im Code steckt die Logging-Funktion
zdebug: https://github.com/zbm-dev/zfsbootmenu/blob/master/zfsbootme...Wenn Debug-Logging aktiviert ist und man im Hauptmenü Ctrl-T drückt, erscheint so ein Bildschirm: https://i.imgur.com/Ge75zkP.png
Außerdem gibt es Flamegraph-Profiling, das sich über https://github.com/zbm-dev/zfsbootmenu/blob/master/zfsbootme... aktivieren lässt; wenn man die über die serielle Schnittstelle ausgegebenen Daten wieder zusammensetzt, kann man Graphen wie https://raw.githubusercontent.com/zbm-dev/zfsbootmenu/master... erzeugen.
Bash ist überraschend flexibel.
set -xistPS4='+ ${BASH_SOURCE:-}:${FUNCNAME[0]:-}:L${LINENO:-}: 'sehr nützlich.Damit werden Dateiname, Funktionsname und Zeilennummer angezeigt, was beim Debuggen großer Bash-Skripte ziemlich hilft.
shellcheckist empfehlenswert. Selbst wenn es ein Problem nicht direkt findet, weist es auf potenzielle Probleme hin.Außerdem ist es empfehlenswert, Skripte in einer anderen Sprache neu zu schreiben. In unserer Firma stellen wir Bash-Skripte auf Rust um; die Einstiegshürde ist hoch, aber der resultierende Code ist viel leichter zu warten und zuverlässiger.
Bash ist für schnelle Skripte weiterhin gut, aber ab etwa 100 Zeilen lohnt sich eine Sprache mit stärkeren Garantien.
die()ist eine Hilfsfunktion, die eine Fehlermeldung auf die Standardfehlerausgabe schreibt und dann mit dem angegebenen Fehlercode beendet.Weitere Exit-Codes und Hilfsfunktionen für Shell-Skripte gibt es hier: https://github.com/SixArm/unix-shell-script-kit/blob/main/un...
Allerdings habe ich an der Philosophie dieses
dieein wenig auszusetzen. Einedie-Funktion sollte grundsätzlich den Exit-Code des fehlgeschlagenen Befehls weitergeben und auch dessen Fehlerausgabe nicht unterdrücken.Wenn ich einem Befehlsfehler in einem großen Skript selbst eine Bedeutung geben will, würde ich ein spezialisierteres
dieseparat verwenden. Meindiesieht grob wie__errex "$?" "${LINENO}" "$0"aus: Es gibt einen fatalen Fehler, die Zeilennummer, den Skriptnamen und eine Nachricht aus und beendet mit diesem Exit-Code.Ein Implementierungsbeispiel gibt es hier: https://github.com/TritonDataCenter/sdc-headnode/blob/master...
Wenn man sie wie
some-command || fail "message"verwendet, erzeugt sie einen Stacktrace und beendet die Shell, sobaldsome-commandeinen Exit-Status ungleich 0 zurückgibt.Wenn man in einer Funktion einen Stacktrace erzeugen und zurückkehren möchte, kann man etwas wie
some-command || softfail "message" || return $?verwenden.Man kann damit erledigen, was nötig ist, aber sie ist klobig und die Syntax ist schrecklich. Sobald ein Skript eine gewisse Größe oder Komplexität erreicht, zwingt sie einen praktisch dazu, auf eine richtige Sprache umzusteigen; vielleicht war das ja sogar beabsichtigtes Design.
Auf modernen Distributionen gibt es in der Regel eine recht aktuelle Bash-Version, und wenn man nicht gerade Dinge wie Arrays verwenden will, muss man sich auch nicht groß um die Version kümmern.
Der Reiz von Bash liegt in der Position, die sie zwischen anderen Sprachen und Tools einnimmt. Sie ist ideal, um andere Tools zu verbinden, nah genug am Betriebssystem, um bequem zu sein, und verlangt nicht wie Python die Installation von Bibliotheken.
Bei komplexerem Scripting hört man oft, man solle auf eine Sprache wie Python wechseln, aber dadurch kommt eine zusätzliche Komplexitätsschicht hinzu, die langfristig nicht unbedingt hilft. Ein Bash-Skript von vor 20 Jahren läuft heute immer noch problemlos, während ein Python-Programm von vor 20 Jahren wahrscheinlich Versionsprobleme bekommt.
Plan 9s
rcist etwas sauberer, aber wegen „ähnlich, nur sauberer“ wechselt niemand. Man kann schon jetzt unter https://pkgsrc.se/shells etwas Ähnliches, aber Besseres installieren, tut es aber nicht, und für andere ändert sich dadurch auch nicht die Ausführungsweise.Um eine etablierte Technologie zu ersetzen, muss man in zentralen Aspekten um ein Vielfaches besser sein. Auch Plan 9 war besser als UNIX-artige Systeme, aber nicht gut genug, um sie zu ersetzen.
Es ist schwer, etwas zu schaffen, das gut genug ist, um die Nische des Bourne-Shell-Scriptings zu ersetzen. Bevor es so gut wird, landet es bereits im ökologischen Platz oder Problembereich echter Skriptsprachen wie Perl, Python oder Ruby.
In engen Problembereichen saugt ein lokales Optimum die ganze Luft ab und macht es schwer, dass Konkurrenten entstehen, die einem theoretischen globalen Optimum nahekommen.
Die jüngst hinzugekommenen Funktionen fügen sich zwar gut auf
sh/Bash ein, aber letztlich ist Shell-Scripting ein Mittel zum Zweck und sollte sich deutlich langsamer weiterentwickeln als allgemeine ProgrammiersprachenDas zentrale Merkmal von Bash/
shist, dass es anti-entropisch ist. Es gibt kaum Entwicklung oder Evolution, sodass die Wahrscheinlichkeit gering ist, wegen Abhängigkeiten oder neuer Funktionen Probleme zu bekommen, und Dinge, die vor 20 Jahren funktionierten, bleiben weiterhin StandardwerkzeugeDurch dieses Design wird es zu einem System, das Veränderungen ablehnt, und wenn man an Grenzen stößt, entsteht ein Anreiz, außerhalb davon weiterzumachen
Die meisten Skripte in FreeBSD sind für
shgeschrieben, undshist Teil des POSIX-Standards, daher wirkt es deutlich breiter unterstützt. Bash halte ich einfach für ziemlich populärAber Bash war so schlecht, dass ich eine Menge Utilities mit reduziertem Namensraum gebaut habe, um Groovy-Skripte zu verwenden. Man konnte in einer IDE entwickeln, das Bibliothekssystem war sicher, und Groovy hat fast alle Unbequemlichkeiten von Java geglättet, was deutlich besser war
Es gibt ein paar Einschränkungen, aber im Allgemeinen kann er nützlich sein: https://github.com/ketancmaheshwari/pd
die()-Technik ist gut, aber Bash hat eine nervige Eigenschaft. Wenn man innerhalb einer Subshellexitausführen will, wird nur die Subshell beendet, und der Rest des Skripts läuft weiterWenn man zum Beispiel in einer Pipeline wie
cat myfile | while read line; do ... die "Found match" ... donedieaufruft, wird das nachfolgendeecho "I don't want this line"trotzdem ausgegebenOft kann man Subshells vermeiden, und in diesem Beispiel weist
shellcheckzu Recht auf UUOC hin; behebt man das, ist auch dasdie-Problem in der Subshell gelöstManchmal lassen sich Subshells aber nicht vermeiden, oder das Skript würde dadurch zu kompliziert. In solchen Fällen kann man am Anfang des Skripts mit
MYPID=$$die PID festhalten und es dann etwa mitdie() { echo "$1" >&2; kill -9 $MYPID; exit 1; }beendenNatürlich ist auch das ein Kompromiss. Diese Art des Beendens ist ziemlich grob, und aus unbekannten Gründen war sie auch nicht völlig zuverlässig
set -esorgt dafür, dass das Skript mit beendet wird, wenn eine Subshell mit einem Fehlercode ungleich 0 endetMir fällt kaum ein Grund ein, warum man in irgendeinem Shell-Skript auf
set -everzichten sollteset -euxo pipefailBedingungstests werden dadurch etwas schwieriger, aber insbesondere
pipefailallein hat sich schon mehrfach bezahlt gemachtset +xwieder ausschaltenWenn es dauerhaft eingeschaltet bleibt, wird es ziemlich ermüdend
Allerdings spare ich mir
-xauf, bis ich wirklich die ganze unordentliche Debug-Ausgabe sehen muss