
Das Debuggen von Bash-Skripten ist wie die Suche nach einer Nadel im Heuhaufen, besonders wenn neue ErgĂ€nzungen in eine bestehende Codebasis eingebracht werden, ohne rechtzeitige ĂberprĂŒfung von Struktur, Logging und ZuverlĂ€ssigkeit. In solchen Situationen können sowohl eigene Fehler als auch das Management komplexer SkriptanhĂ€ufungen zu Problemen fĂŒhren.
Der Befehl hat einen Artikel mit Empfehlungen ĂŒbersetzt, die Ihnen helfen, Ihre Skripte besser zu schreiben, zu debuggen und zu warten. Ob Sie es glauben oder nicht, aber nichts kann mit der Zufriedenheit verglichen werden, sauberen, einsatzbereiten Bash-Code zu schreiben, der jedes Mal funktioniert.
Im Artikel teilt der Autor, was er in den letzten Jahren gelernt hat, sowie einige hĂ€ufige Fehler, die ihn ĂŒberrascht haben. Das ist wichtig, denn jeder Softwareentwickler arbeitet irgendwann in seiner Karriere mit Skripten zur Automatisierung wiederkehrender Aufgaben.
Fallenhandler
Die meisten Bash-Skripte, mit denen ich gearbeitet habe, haben nie einen effektiven Bereinigungsmechanismus verwendet, wenn wĂ€hrend der AusfĂŒhrung des Skripts etwas Unerwartetes passiert.
Unerwartete Ereignisse können von auĂen kommen, wie das Empfangen eines Signals vom Kernel. Die Handhabung solcher FĂ€lle ist entscheidend, damit Skripte ausreichend zuverlĂ€ssig fĂŒr den Einsatz in Produktionssystemen sind. Ich verwende hĂ€ufig Exit-Handler, um auf solche Szenarien zu reagieren:
function handle_exit() {
// Hier Bereinigungscode hinzufĂŒgen
// z.B. rm -f "/tmp/${lock_file}.lock"
// mit einem entsprechenden Statuscode beenden
}
// trap
trap handle_exit 0 SIGHUP SIGINT SIGQUIT SIGABRT SIGTERM
trap ist ein eingebauter Shell-Befehl, der Ihnen hilft, eine Bereinigungsfunktion zu registrieren, die bei bestimmten Signalen aufgerufen wird. Dabei ist jedoch besondere Vorsicht bei solchen Handlern wie SIGINT, der das Skript unterbricht.
DarĂŒber hinaus sollten in den meisten FĂ€llen nur EXITabgefangen werden, aber die Idee ist, dass Sie das Verhalten des Skripts fĂŒr jedes einzelne Signal tatsĂ€chlich anpassen können.
Eingebaute Funktionen set â schnelles Beenden bei Fehlern
Es ist Ă€uĂerst wichtig, Fehler sofort zu beheben, wenn sie auftreten, und die AusfĂŒhrung schnell zu stoppen. Nichts ist schlimmer, als einen Befehl wie diesen fortzusetzen:
rm -rf ${directory_name}/*
Bitte beachten Sie, dass die Variable directory_name nicht definiert ist.
Um mit solchen Szenarien umzugehen, ist es wichtig, die integrierten Funktionen zu verwenden: set, wie set -o errexit, set -o pipefail oder set -o nounset am Anfang des Skripts. Diese Funktionen stellen sicher, dass Ihr Skript beendet wird, sobald es einen beliebigen Fehlercode trifft, den Einsatz von undefinierten Variablen, falsche Befehle, die ĂŒber eine Pipeline ĂŒbergeben werden, usw.:
#!/usr/bin/env bash
set -o errexit
set -o nounset
set -o pipefail
function print_var() {
echo "${var_value}"
}
print_var
$ ./sample.sh
./sample.sh: line 8: var_value: unbound variable
Hinweis: Integrierte Funktionen wie set -o errexit, beenden das Skript sofort, sobald ein "unerwarteter" RĂŒckgabewert (auĂer null) auftritt. Daher ist es besser, eine benutzerdefinierte Fehlerbehandlung einzufĂŒhren, zum Beispiel:
#!/bin/bash
error_exit() {
line=$1
shift 1
echo "ERROR: non zero return code from line: $line -- $@"
exit 1
}
a=0
let a++ || error_exit "$LINENO" "let operation returned non 0 code"
echo "you will never see me"
# run it, now we have useful debugging output
$ bash foo.sh
ERROR: non zero return code from line: 9 -- let operation returned non 0 code
Solche Skriptschreibweisen zwingen dazu, das Verhalten aller Befehle im Skript sorgfĂ€ltiger zu ĂŒberwachen und die Möglichkeit von Fehlern vorauszusehen, bevor sie zu einer Ăberraschung werden.
Verwenden Sie ShellCheck zur Fehlererkennung wÀhrend der Entwicklung.
Es lohnt sich, etwas wie in Ihre Entwicklungs- und Test-Pipelines, um Ihren Bash-Code auf die besten Praktiken zu ĂŒberprĂŒfen.
Ich verwende es in meinen lokalen Entwicklungsumgebungen, um Berichte ĂŒber Syntax, Semantik und einige Fehler im Code zu erhalten, die ich wĂ€hrend der Entwicklung möglicherweise ĂŒbersehen habe. Es ist ein statisches Analysewerkzeug fĂŒr Ihre Bash-Skripte, und ich kann es nur empfehlen.
Verwendung eigener Exit-Codes
RĂŒckgabewerte in POSIX sind nicht nur null oder eins, sondern null oder ein nicht nullwertiges Ergebnis. Nutzen Sie diese Möglichkeiten, um benutzerdefinierte Fehlermeldungscodes (zwischen 201-254) fĂŒr verschiedene Fehlerszenarien zurĂŒckzugeben.
Diese Informationen können dann von anderen Skripten verwendet werden, die Ihr Skript kapseln, um genau zu verstehen, welche Art von Fehler aufgetreten ist, und entsprechend zu reagieren:
#!/usr/bin/env bash
SUCCESS=0
FILE_NOT_FOUND=240
DOWNLOAD_FAILED=241
function read_file() {
if ${file_not_found}; then
return ${FILE_NOT_FOUND}
fi
}
Hinweis: Bitte seien Sie besonders vorsichtig mit den von Ihnen definierten Variablennamen, um ein versehentliches Ăberschreiben von Umgebungsvariablen zu vermeiden.
Logger-Funktionen
Eine ansprechende und strukturierte ProtokollfĂŒhrung ist entscheidend, um die Ergebnisse des Skriptablaufs leicht zu verstehen. Wie in anderen Hochsprachen verwende ich in meinen Bash-Skripten stets eigene Logging-Funktionen, wie zum Beispiel __msg_info, __msg_error usw.
Das hilft dabei, eine standardisierte Struktur der ProtokollfĂŒhrung zu gewĂ€hrleisten, indem Ănderungen an nur einer Stelle vorgenommen werden:
#!/usr/bin/env bash
function __msg_error() {
[[ "${ERROR}" == "1" ]] && echo -e "[ERROR]: $*"
}
function __msg_debug() {
[[ "${DEBUG}" == "1" ]] && echo -e "[DEBUG]: $*"
}
function __msg_info() {
[[ "${INFO}" == "1" ]] && echo -e "[INFO]: $*"
}
__msg_error "File could not be found. Cannot proceed"
__msg_debug "Starting script execution with 276MB of available RAM"
In der Regel versuche ich, in meinen Skripten einen Mechanismus zu haben __init, in dem solche Logger-Variablen und andere systemweite Variablen initialisiert oder auf Standardwerte gesetzt werden. Diese Variablen können auch ĂŒber die Kommandozeilenparameter beim Aufruf des Skripts gesetzt werden.
Zum Beispiel so etwas wie:
$ ./run-script.sh --debug
Wenn ein solches Skript ausgefĂŒhrt wird, ist sichergestellt, dass die allgemeinen Systemeinstellungen auf Standardwerte gesetzt werden, wenn dies erforderlich ist, oder zumindest mit passenden Werten initialisiert werden, wenn dies notwendig ist.
Ich treffe in der Regel eine Entscheidung darĂŒber, was initialisiert werden soll und was nicht, basierend auf einem Kompromiss zwischen BenutzeroberflĂ€che und den Konfigurationsdetails, in die der Benutzer einsteigen kann/soll.
Architektur fĂŒr Wiederverwendbarkeit und einen sauberen Systemzustand
Modularer / wiederverwendbarer Code
âââ framework
â âââ common
â â âââ loggers.sh
â â âââ mail_reports.sh
â â âââ slack_reports.sh
â âââ daily_database_operation.sh
Ich halte ein separates Repository, das zur Initialisierung eines neuen Projekts/ eines Bash-Skripts, das ich entwickeln möchte, verwendet werden kann. Alles, was wiederverwendet werden kann, sollte im Repository gespeichert werden und kann in anderen Projekten, die solche Funktionen nutzen möchten, abgerufen werden. Eine solche Struktur der Projekte reduziert erheblich die GröĂe anderer Skripte und stellt auch sicher, dass die Codebasis klein und leicht testbar ist.
Wie im oben genannten Beispiel enthalten sind alle Logging-Funktionen, wie __msg_info, __msg_error und andere, wie z.B. Slack-Berichte, separat in common/* und werden dynamisch in anderen Skripten, wie daily_database_operation.sh.
Hinterlassen Sie ein sauberes System
Wenn Sie wĂ€hrend der AusfĂŒhrung eines Skripts Ressourcen hochladen, empfiehlt es sich, alle Daten in einem allgemeinen Verzeichnis mit einem zufĂ€lligen Namen zu speichern, zum Beispiel /tmp/AlRhYbD97/*. Sie können Zufallstextgeneratoren verwenden, um den Verzeichnisnamen auszuwĂ€hlen:
rand_dir_name="$(cat /dev/urandom | tr -dc 'a-zA-Z0-9' | fold -w 16 | head -n 1)"
Nach Abschluss der Arbeit kann die Reinigung solcher Verzeichnisse in den oben besprochenen Trap-Handlern gewĂ€hrleistet werden. Wenn Sie sich nicht um die temporĂ€ren Verzeichnisse kĂŒmmern, sammeln sie sich an und fĂŒhren irgendwann zu unerwarteten Problemen auf dem Host, wie zum Beispiel einem vollen DatentrĂ€ger.
Verwendung von Lock-Dateien
HĂ€ufig muss sichergestellt werden, dass nur eine Instanz eines Skripts zu einem bestimmten Zeitpunkt auf dem Host ausgefĂŒhrt wird. Dies kann mit Lock-Dateien erreicht werden.
Ich erstelle normalerweise Lock-Dateien in /tmp/project_name/*.lock und ĂŒberprĂŒfe deren Vorhandensein zu Beginn des Skripts. Dies hilft, das Skript ordnungsgemÀà zu beenden und unerwartete SystemzustandsĂ€nderungen durch ein parallel laufendes anderes Skript zu vermeiden. Lock-Dateien sind nicht erforderlich, wenn das gleiche Skript parallel auf diesem Host ausgefĂŒhrt werden soll.
Messen und verbessern
Wir mĂŒssen oft mit Szenarien arbeiten, die ĂŒber einen lĂ€ngeren Zeitraum ausgefĂŒhrt werden, wie zum Beispiel tĂ€gliche Datenbankoperationen. Solche VorgĂ€nge umfassen in der Regel eine Abfolge von Schritten: Daten laden, Anomalien ĂŒberprĂŒfen, Daten importieren, Statusberichte senden usw.
In solchen FĂ€llen versuche ich immer, das Skript in einzelne kleinere Skripte zu unterteilen und ĂŒber ihren Status und die AusfĂŒhrungszeit mithilfe von:
time source "${filepath}" "${args}" >> "${LOG_DIR}/RUN_LOG" 2>&1
SpĂ€ter kann ich die AusfĂŒhrungszeit mit folgendem Befehl ĂŒberprĂŒfen:
tac "${LOG_DIR}/RUN_LOG.txt" | grep -m1 "real"
Das hilft mir, problematische/langsame Bereiche in den Skripten zu identifizieren, die optimiert werden mĂŒssen.
Viel Erfolg!
Weitere Leseempfehlungen:
Quelle: habr.com
