Beste Praktiken fĂŒr Bash-Skripte: Kurzanleitung fĂŒr zuverlĂ€ssige und leistungsstarke Bash-Skripte

Beste Praktiken fĂŒr Bash-Skripte: Kurzanleitung fĂŒr zuverlĂ€ssige und leistungsstarke Bash-Skripte
Shell-Hintergrundbild von manapi

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 Mail.ru Cloud Lösungen 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 ShellCheck 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:

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

Quelle: habr.com

Erwerben Sie zuverlĂ€ssiges Hosting fĂŒr Websites mit DDoS-Schutz, VPS VDS-Server đŸ”„ Kaufen Sie zuverlĂ€ssiges Hosting fĂŒr Websites mit DDoS-Schutz, VPS VDS-Server | ProHoster