Beste Praktiken fĂŒr Bash-Skripte: Ein kurzes Handbuch fĂŒr zuverlĂ€ssige und leistungsfĂ€hige Bash-Skripte

Beste Praktiken fĂŒr Bash-Skripte: Ein kurzes Handbuch fĂŒr zuverlĂ€ssige und leistungsfĂ€hige Bash-Skripte
Shell-Hintergrundbild von manapi

Das Debuggen von Bash-Skripten ist wie die Suche nach einer Nadel im Heuhaufen, insbesondere wenn neue ErgĂ€nzungen in eine bestehende Codebasis eingefĂŒhrt werden, ohne rechtzeitige Überlegungen zu Struktur, Protokollierung und ZuverlĂ€ssigkeit. In solchen Situationen kann man sowohl aufgrund eigener Fehler als auch beim Umgang mit komplizierten SkripthĂ€ufungen in Schwierigkeiten geraten.

Team Mail.ru Cloud Solutions Ich habe einen Artikel mit Empfehlungen ĂŒbersetzt, mit denen Sie besser schreiben, debuggen und Ihre Skripte warten können. Glauben Sie es oder nicht, aber nichts kann mit der Zufriedenheit verglichen werden, die Sie empfinden, wenn Sie sauberen, einsatzbereiten Bash-Code 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. Dies ist wichtig, denn jeder Softwareentwickler arbeitet irgendwann in seiner Karriere an Skripten zur Automatisierung alltĂ€glicher Aufgaben.

Fallenbearbeiter

Die meisten Bash-Skripte, mit denen ich zu tun hatte, haben nie einen effektiven Bereinigungsmechanismus verwendet, wenn wĂ€hrend der AusfĂŒhrung des Skripts etwas Unerwartetes passiert.

Überraschungen können von außen kommen, zum Beispiel durch das Empfangen eines Signals vom Kernel. Die Verarbeitung solcher FĂ€lle ist Ă€ußerst wichtig, damit die Skripte zuverlĂ€ssig genug fĂŒr den Einsatz in Produktionssystemen sind. Ich verwende hĂ€ufig Exit-Handler, um auf solche Szenarien zu reagieren:

function handle_exit() {
  	// FĂŒgen Sie hier den Bereinigungscode hinzu
  	// z.B. rm -f "/tmp/${lock_file}.lock"
  	// mit einem geeigneten Statuscode beenden
}
  
// trap  
trap handle_exit 0 SIGHUP SIGINT SIGQUIT SIGABRT SIGTERM

trap ist ein eingebauter Befehl der Shell, der Ihnen hilft, eine Bereinigungsfunktion zu registrieren, die bei bestimmten Signalen aufgerufen wird. Allerdings sollte bei solchen Handlern besondere Vorsicht walten, wie bei SIGINT, der das Skript abbricht.

DarĂŒber hinaus sollte man in den meisten FĂ€llen nur EXIT, abfangen, aber die Idee ist, dass Sie das Verhalten des Skripts fĂŒr jedes einzelne Signal wirklich anpassen können.

Eingebaute Funktionen setzen — schnelles Beenden bei Fehlern

Es ist sehr wichtig, auf Fehler zu reagieren, sobald sie auftreten, und die AusfĂŒhrung schnell zu beenden. Nichts ist schlimmer, als einen Befehl wie diesen weiter auszufĂŒhren:

rm -rf ${directory_name}

Bitte beachten Sie, dass die Variable directory_name nicht definiert ist.

Um solche Szenarien zu behandeln, ist es wichtig, die integrierten Funktionen set, wie set -o errexit, set -o pipefail oder set -o nounset am Anfang des Skripts zu verwenden. Diese Funktionen stellen sicher, dass Ihr Skript beendet wird, sobald es auf einen nicht nullen RĂŒckgabewert, unbestimmte Variablen, falsche ĂŒbergebene Befehle und dergleichen stĂ¶ĂŸt:

#!/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: eingebaute Funktionen wie set -o errexit, beenden das Skript, sobald ein "nicht behandelter" RĂŒckgabewert (außer null) auftritt. Daher ist es besser, eine benutzerdefinierte Fehlerbehandlung einzufĂŒhren, wie 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

Eine derartige Skripterstellung zwingt Sie dazu, das Verhalten aller Befehle im Skript sorgfĂ€ltiger zu betrachten und die Möglichkeit von Fehlern vorherzusehen, bevor sie Sie ĂŒberraschen.

ShellCheck zur Fehlererkennung wÀhrend der Entwicklung

Es lohnt sich, etwas wie ShellCheck in Ihre Entwicklungs- und Testpipelines zu integrieren, um Ihren Bash-Code auf die Einhaltung bewĂ€hrter Verfahren zu ĂŒberprĂŒfen.

Ich verwende es in meinen lokalen Entwicklungsumgebungen, um Berichte ĂŒber Syntax-, Semantik- und einige Fehler im Code zu erhalten, die ich möglicherweise wĂ€hrend der Entwicklung ĂŒbersehen habe. Es ist ein statisches Analysewerkzeug fĂŒr Ihre Bash-Skripte, und ich empfehle es nachdrĂŒcklich.

Verwendung Ihrer Exit-Codes

RĂŒckgabewerte in POSIX sind nicht nur null oder eins, sondern null oder ein nicht null Wert. Nutzen Sie diese Möglichkeit, um benutzerdefinierte Fehlercodes (zwischen 201-254) fĂŒr verschiedene FehlerfĂ€lle zurĂŒckzugeben.

Diese Informationen können dann von anderen Skripten verwendet werden, die Ihres umhĂŒllen, 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 Variablennamen, die Sie definieren, um nicht versehentlich Umgebungsvariablen zu ĂŒberschreiben.

Logger-Funktionen

Eine schöne und strukturierte Protokollierung ist wichtig, um die Ergebnisse der AusfĂŒhrung Ihres Skripts leicht zu verstehen. Wie in anderen Hochsprachen verwende ich in meinen Bash-Skripten immer eigene Protokollierungsfunktionen, wie zum Beispiel __msg_info, __msg_error und so weiter.

Das hilft, eine standardisierte Struktur fĂŒr die Protokollierung zu gewĂ€hrleisten, indem Änderungen nur an einem Ort 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"

Ich versuche normalerweise, in meinen Skripten irgendeinen Mechanismus zu haben __init, wo solche Logger-Variablen und andere Systemvariablen initialisiert oder auf Standardwerte gesetzt werden. Diese Variablen können auch wÀhrend des Skriptaufrufs aus den Befehlszeilenparametern gesetzt werden.

Zum Beispiel so etwas wie:

$ ./run-script.sh --debug

Wenn ein solches Skript ausgefĂŒhrt wird, ist garantiert, dass die allgemeinen Systemeinstellungen auf die Standardwerte eingestellt sind, falls diese erforderlich sind, oder zumindest mit etwas entsprechendem initialisiert werden, wenn nötig.

Ich basiere meine Entscheidung, was zu initialisieren und was nicht, auf einem Kompromiss zwischen BenutzeroberflÀche und den Konfigurationsdetails, mit denen der Benutzer sich befassen kann/sollte.

Architektur fĂŒr Wiederverwendbarkeit und saubere SystemzustĂ€nde

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 Bash-Projekts/Skripts verwendet werden kann, das ich entwickeln möchte. Alles, was wiederverwendet werden kann, kann im Repository gespeichert und in anderen Projekten abgerufen werden, die solche FunktionalitĂ€ten nutzen möchten. Eine solche Projektorientierung reduziert erheblich die GrĂ¶ĂŸe anderer Skripte und stellt auch sicher, dass die Codebasis klein und leicht testbar ist.

Wie im obigen Beispiel enthalten alle Protokollierungsfunktionen, wie __msg_info, __msg_error und andere, wie Berichte ĂŒber Slack, separat in common/* und werden dynamisch in anderen Szenarien eingebunden, wie daily_database_operation.sh.

Hinterlassen Sie ein sauberes System

Wenn Sie wĂ€hrend der AusfĂŒhrung des Skripts Ressourcen laden, wird empfohlen, alle solchen Daten in einem gemeinsamen Verzeichnis mit einem zufĂ€lligen Namen zu speichern, zum Beispiel /tmp/AlRhYbD97/*. Sie können Zufalls-Textgeneratoren verwenden, um einen 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 Bereinigung solcher Verzeichnisse in den oben besprochenen Trap-Handlern sichergestellt werden. Wenn die temporÀren Verzeichnisse nicht entfernt werden, sammeln sie sich an und verursachen irgendwann unerwartete Probleme auf dem Host, beispielsweise ein voller DatentrÀger.

Verwendung von Lock-Dateien

Es ist oft notwendig, nur eine Instanz des Skripts zu einem bestimmten Zeitpunkt auf dem Host auszufĂŒhren. Dies kann durch Lock-Dateien erreicht werden.

Ich erstelle normalerweise Lock-Dateien in /tmp/project_name/*.lock und ĂŒberprĂŒfe deren Existenz am Anfang des Skripts. Dies hilft, das Skript korrekt zu beenden und unerwartete Änderungen des Systemzustands durch ein parallel laufendes Skript zu vermeiden. Lock-Dateien sind nicht nötig, wenn Sie möchten, dass dasselbe Skript parallel auf diesem Host ausgefĂŒhrt wird.

Messen und Verbessern

Wir mĂŒssen oft mit Skripten arbeiten, die ĂŒber lĂ€ngere ZeitrĂ€ume ausgefĂŒhrt werden, wie z.B. bei tĂ€glichen Datenbankoperationen. Solche VorgĂ€nge beinhalten normalerweise eine Sequenz von Schritten: Daten laden, auf Anomalien prĂŒfen, Daten importieren, Statusberichte senden usw.

In solchen FĂ€llen versuche ich immer, das Skript in separate kleine Skripte zu unterteilen und ihren Status und die AusfĂŒhrungszeit mit folgendem zu protokollieren:

time source "${filepath}" "${args}" >> "${LOG_DIR}/RUN_LOG" 2>&1

SpĂ€ter kann ich die AusfĂŒhrungszeit mit folgendem ĂŒ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 GlĂŒck!

Was Sie sonst noch lesen sollten:

  1. Go und GPU-Caches.
  2. Beispiel fĂŒr eine ereignisgesteuerte Anwendung auf Basis von Webhooks im objektbasierten S3-Speicher von Mail.ru Cloud Solutions.
  3. Unser Telegram-Kanal ĂŒber digitale Transformation.

Quelle: habr.com

60GB SSD 8Gb DDR4