Best Practices für Bash-Skripte: Eine Kurzanleitung für zuverlässige und leistungsstarke Bash-Skripte

Best Practices für Bash-Skripte: Eine Kurzanleitung für zuverlässige und leistungsstarke Bash-Skripte
Muscheltapete von manapi

Das Debuggen von Bash-Skripten ist wie die Suche nach der Nadel im Heuhaufen, insbesondere wenn neue Ergänzungen in der vorhandenen Codebasis auftauchen, ohne dass Fragen der Struktur, Protokollierung und Zuverlässigkeit rechtzeitig berücksichtigt werden. Sie können sich in solchen Situationen befinden, entweder aufgrund Ihrer eigenen Fehler oder bei der Verwaltung komplexer Skriptstapel.

Team 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, nichts geht über die Befriedigung, sauberen, gebrauchsfertigen Bash-Code zu schreiben, der jedes Mal funktioniert.

In dem Artikel teilt der Autor mit, was er in den letzten Jahren gelernt hat, sowie einige häufige Fehler, die ihn überrascht haben. Dies ist wichtig, da jeder Softwareentwickler irgendwann in seiner Karriere mit Skripten arbeitet, um routinemäßige Arbeitsaufgaben zu automatisieren.

Fallenhandler

Die meisten Bash-Skripte, die mir begegnet sind, verwenden nie einen wirksamen Bereinigungsmechanismus, wenn während der Skriptausführung etwas Unerwartetes passiert.

Von außen können Überraschungen entstehen, etwa durch den Empfang eines Signals vom Kern. Der Umgang mit solchen Fällen ist äußerst wichtig, um sicherzustellen, dass die Skripte zuverlässig genug sind, um auf Produktionssystemen ausgeführt zu werden. Ich verwende häufig Exit-Handler, um auf Szenarien wie dieses zu reagieren:

function handle_exit() {
  // Add cleanup code here
  // for eg. rm -f "/tmp/${lock_file}.lock"
  // exit with an appropriate status code
}
  
// trap <HANDLER_FXN> <LIST OF SIGNALS TO TRAP>
trap handle_exit 0 SIGHUP SIGINT SIGQUIT SIGABRT SIGTERM

trap ist ein in die Shell integrierter Befehl, der Ihnen hilft, eine Bereinigungsfunktion zu registrieren, die im Falle von Signalen aufgerufen wird. Besondere Vorsicht ist jedoch bei Handlern wie z SIGINT, was zum Abbruch des Skripts führt.

Darüber hinaus sollten Sie in den meisten Fällen nur fangen EXIT, aber die Idee ist, dass Sie das Verhalten des Skripts tatsächlich für jedes einzelne Signal anpassen können.

Integrierte Set-Funktionen – schnelle Beendigung bei Fehler

Es ist sehr wichtig, sofort auf auftretende Fehler zu reagieren und die Ausführung schnell zu stoppen. Nichts könnte schlimmer sein, als einen Befehl wie diesen weiterhin auszuführen:

rm -rf ${directory_name}/*

Bitte beachten Sie, dass die Variable directory_name unentschlossen.

Es ist wichtig, integrierte Funktionen zu verwenden, um solche Szenarien zu bewältigen set, Sowie set -o errexit, set -o pipefail oder set -o nounset am Anfang des Drehbuchs. Diese Funktionen stellen sicher, dass Ihr Skript beendet wird, sobald es auf einen Exit-Code ungleich Null, die Verwendung undefinierter Variablen, ungültige über eine Pipe übergebene Befehle usw. 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: Integrierte Funktionen wie z set -o errexit, beendet das Skript, sobald ein „roher“ Rückkehrcode (ungleich Null) vorliegt. 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

Wenn Sie Skripte auf diese Weise schreiben, müssen Sie sorgfältiger auf das Verhalten aller Befehle im Skript achten und die Möglichkeit eines Fehlers antizipieren, bevor er Sie überrascht.

ShellCheck zur Erkennung von Fehlern während der Entwicklung

Es lohnt sich, so etwas zu integrieren Shell-Check in Ihre Entwicklungs- und Testpipelines ein, um Ihren Bash-Code anhand von Best Practices zu überprüfen.

Ich verwende es in meinen lokalen Entwicklungsumgebungen, um Berichte über Syntax, Semantik und einige Fehler im Code zu erhalten, die mir bei der Entwicklung möglicherweise entgangen sind. Dies ist ein statisches Analysetool für Ihre Bash-Skripte und ich empfehle dringend, es zu verwenden.

Verwenden Sie Ihre eigenen Exit-Codes

Rückgabecodes in POSIX sind nicht nur Null oder Eins, sondern Null oder ein Wert ungleich Null. Verwenden Sie diese Funktionen, um benutzerdefinierte Fehlercodes (zwischen 201 und 254) für verschiedene Fehlerfälle zurückzugeben.

Diese Informationen können dann von anderen Skripten verwendet werden, die Ihr Skript umschließen, um genau zu verstehen, welche Art von Fehler aufgetreten ist, und entsprechend reagieren zu können:

#!/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: Seien Sie bitte besonders vorsichtig mit den von Ihnen definierten Variablennamen, um ein versehentliches Überschreiben von Umgebungsvariablen zu vermeiden.

Protokollierungsfunktionen

Eine schöne und strukturierte Protokollierung ist wichtig, um die Ergebnisse Ihres Skripts leicht zu verstehen. Wie bei anderen höheren Programmiersprachen verwende ich in meinen Bash-Skripten immer native Protokollierungsfunktionen, z __msg_info, __msg_error und so weiter.

Dies trägt dazu bei, eine standardisierte Protokollierungsstruktur bereitzustellen, indem Änderungen nur an 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"

Normalerweise versuche ich, in meinen Skripten eine Art Mechanismus zu haben __init, wobei solche Logger-Variablen und andere Systemvariablen initialisiert oder auf Standardwerte gesetzt werden. Diese Variablen können auch über Befehlszeilenoptionen während des Skriptaufrufs festgelegt werden.

Zum Beispiel so etwas wie:

$ ./run-script.sh --debug

Wenn ein solches Skript ausgeführt wird, stellt es sicher, dass systemweite Einstellungen bei Bedarf auf Standardwerte gesetzt oder bei Bedarf zumindest auf einen geeigneten Wert initialisiert werden.

Normalerweise stütze ich mich bei der Entscheidung, was initialisiert werden soll und was nicht, auf einen Kompromiss zwischen der Benutzeroberfläche und den Details der Konfigurationen, mit denen sich der Benutzer befassen kann/sollte.

Architektur für Wiederverwendung und sauberen Systemzustand

Modularer/wiederverwendbarer Code

├── framework
│   ├── common
│   │   ├── loggers.sh
│   │   ├── mail_reports.sh
│   │   └── slack_reports.sh
│   └── daily_database_operation.sh

Ich behalte ein separates Repository, das ich zum Initialisieren eines neuen Projekts/Bash-Skripts verwenden kann, das ich entwickeln möchte. Alles, was wiederverwendet werden kann, kann in einem Repository gespeichert und von anderen Projekten abgerufen werden, die diese Funktionalität nutzen möchten. Durch die Organisation von Projekten auf diese Weise wird die Größe anderer Skripte erheblich reduziert und außerdem sichergestellt, dass die Codebasis klein und einfach zu testen ist.

Wie im obigen Beispiel sind alle Protokollierungsfunktionen wie z __msg_info, __msg_error und andere, wie z. B. Slack-Berichte, sind separat in enthalten common/* und dynamisch in anderen Szenarien verbinden wie daily_database_operation.sh.

Hinterlassen Sie ein sauberes System

Wenn Sie Ressourcen laden, während das Skript ausgeführt wird, wird empfohlen, alle diese Daten in einem freigegebenen Verzeichnis mit einem zufälligen Namen zu speichern, z. B. /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 Arbeiten kann die Bereinigung solcher Verzeichnisse in den oben besprochenen Hook-Handlern erfolgen. Wenn temporäre Verzeichnisse nicht gepflegt werden, sammeln sie sich an und verursachen irgendwann unerwartete Probleme auf dem Host, beispielsweise eine volle Festplatte.

Verwendung von Sperrdateien

Oftmals müssen Sie sicherstellen, dass jeweils nur eine Instanz eines Skripts auf einem Host ausgeführt wird. Dies kann über Sperrdateien erfolgen.

Normalerweise erstelle ich Sperrdateien in /tmp/project_name/*.lock und prüfen Sie, ob sie am Anfang des Skripts vorhanden sind. Dies trägt dazu bei, dass das Skript ordnungsgemäß beendet wird und unerwartete Änderungen des Systemstatus durch ein anderes parallel ausgeführtes Skript vermieden werden. Sperrdateien werden nicht benötigt, wenn dasselbe Skript parallel auf einem bestimmten Host ausgeführt werden soll.

Messen und verbessern

Wir müssen häufig mit Skripten arbeiten, die über einen längeren Zeitraum ausgeführt werden, beispielsweise für tägliche Datenbankoperationen. Solche Vorgänge umfassen typischerweise eine Abfolge von Schritten: Laden von Daten, Prüfung auf Anomalien, Importieren von Daten, Senden von Statusberichten usw.

In solchen Fällen versuche ich immer, das Skript in einzelne kleine Skripte aufzuteilen und deren Status und Ausführungszeit zu melden, indem ich Folgendes verwende:

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

Später kann ich die Ausführungszeit sehen mit:

tac "${LOG_DIR}/RUN_LOG.txt" | grep -m1 "real"

Dies hilft mir, problematische/langsame Bereiche in Skripten zu identifizieren, die optimiert werden müssen.

Good luck!

Was es sonst noch zu lesen gibt:

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

Source: habr.com

Kommentar hinzufügen