Best Practices für Bash-Scripting: Eine Kurzanleitung für zuverlässiges und performantes Bash-Scripting



Shell Wallpaper von manapi Das



Debuggen von Bash-Skripten ähnelt der Suche nach einer Nadel im Heuhaufen, insbesondere wenn neue Ergänzungen in einer vorhandenen Codebasis erscheinen, ohne dass Struktur-, Protokollierungs- und Zuverlässigkeitsprobleme rechtzeitig berücksichtigt werden. Sie können sich in solchen Situationen sowohl aufgrund Ihrer eigenen Fehler als auch bei der Verwaltung komplexer Durcheinander von Skripten befinden.



Das Mail.ru Cloud Solutions- Team hateinen Artikel mit Richtlinien übersetzt, mit denen Sie Ihre Skripte besser schreiben, debuggen und pflegen können. Ob Sie es glauben oder nicht, nichts geht über die Zufriedenheit, sauberen, gebrauchsfertigen Bash-Code zu schreiben, der jedes Mal funktioniert.



In diesem Artikel berichtet der Autor über das, was er in den letzten Jahren gelernt hat, sowie über einige häufige Fehler, die ihn überrascht haben. Dies ist wichtig, da jeder Softwareentwickler zu einem bestimmten Zeitpunkt seiner Karriere mit Skripten arbeitet, um Routineaufgaben zu automatisieren.



FallenfĂĽhrer



Die meisten Bash-Skripte, auf die ich gestoßen bin, haben noch nie einen effizienten Bereinigungsmechanismus verwendet, wenn während der Skriptausführung etwas Unerwartetes passiert.



Unerwartete Dinge können von außen entstehen, beispielsweise wenn sie ein Signal vom Kernel empfangen. Die Behandlung solcher Fälle ist äußerst wichtig, um sicherzustellen, dass Skripte robust genug sind, um auf Produktionssystemen ausgeführt zu werden. Ich benutze oft Exit-Handler, um auf folgende Szenarien 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


trapIst ein integrierter Shell-Befehl, mit dem Sie eine Bereinigungsfunktion registrieren können, die bei Signalen aufgerufen werden soll. Besondere Vorsicht ist jedoch bei Handlern geboten, die SIGINTdas Skript unterbrechen.



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



Integrierte Funktionen festlegen - Schnellbeenden bei Fehler



Es ist sehr wichtig, auf auftretende Fehler zu reagieren und die Ausführung schnell zu beenden. Nichts könnte schlimmer sein, als mit einem Befehl wie diesem fortzufahren:



rm -rf ${directory_name}/*


Bitte beachten Sie, dass die Variable directory_nameundefiniert ist.



Um solche Szenarien zu behandeln, ist es wichtig , eingebaut in Anwendung Funktionen setwie set -o errexit, set -o pipefailoder set -o nounsetam Anfang des Skripts. Diese Funktionen stellen sicher, dass Ihr Skript beendet wird, sobald es auf einen Nicht-Null-Exit-Code, undefinierte Variablen, ungültige Pipe-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 set -o errexitdas Beenden des Skripts, sobald ein "roher" RĂĽckkehrcode (auĂźer Null) angezeigt wird. Daher ist es besser, eine benutzerdefinierte Fehlerbehandlung einzufĂĽhren, wie:



#!/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


Ein solches Skript zwingt Sie dazu, das Verhalten aller Befehle im Skript genauer zu betrachten und die Möglichkeit eines Fehlers zu antizipieren, bevor er überrascht wird.



ShellCheck zum Abfangen von Fehlern während der Entwicklung



Es lohnt sich, etwas wie ShellCheck in Ihre Entwicklungs- und Test-Pipelines zu integrieren, um Ihren Bash-Code auf Best Practices zu ĂĽberprĂĽfen.



Ich verwende es in meinen lokalen Entwicklungsumgebungen, um Berichte über Syntax, Semantik und einige Codefehler abzurufen, die ich möglicherweise während der Entwicklung übersehen habe. Es ist ein statisches Analysetool für Ihre Bash-Skripte und ich empfehle dringend, es zu verwenden.



Verwenden Sie Ihre Exit-Codes



POSIX-Rückkehrcodes sind nicht nur Null oder Eins, sondern Null oder Nicht-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 Ihre umschließen, um genau zu verstehen, welche Art von Fehler aufgetreten ist, und entsprechend 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: Seien Sie besonders vorsichtig mit den von Ihnen definierten Variablennamen, um ein versehentliches Ăśberschreiben von Umgebungsvariablen zu vermeiden.



Logger-Funktionen



Eine gute und strukturierte Protokollierung ist wichtig, um die Ergebnisse Ihrer SkriptausfĂĽhrung leicht zu verstehen. Wie bei anderen High-Level - Programmiersprachen, habe ich immer meine eigenen Logging - Funktionen in meinem Bash - Skripte wie verwenden __msg_info, __msg_errorund so weiter.



Dies hilft bei der Bereitstellung einer standardisierten Protokollierungsstruktur, 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, einen Mechanismus in meinen Skripten zu haben, bei __initdem solche Logger-Variablen und andere Systemvariablen initialisiert oder auf Standardwerte gesetzt werden. Diese Variablen können auch über Befehlszeilenparameter während des Skriptaufrufs festgelegt werden.



Zum Beispiel so etwas wie:



$ ./run-script.sh --debug


Wenn ein solches Skript ausgefĂĽhrt wird, wird garantiert, dass die systemweiten Einstellungen bei Bedarf auf ihre Standardeinstellungen gesetzt oder bei Bedarf zumindest mit einer geeigneten Option initialisiert werden.



Normalerweise entscheide ich mich dafür, was initialisiert werden soll und was kein Kompromiss zwischen der Benutzeroberfläche und den Details der Konfiguration sein soll, mit denen sich der Benutzer befassen kann / sollte.



Architektur fĂĽr Wiederverwendung und sauberen Systemstatus



Modularer / wiederverwendbarer Code



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


Ich habe ein separates Repository, mit dem ich ein neues Bash-Projekt / Skript initialisieren kann, das ich entwickeln möchte. Alles, was wiederverwendet werden kann, kann im Repository gespeichert und in anderen Projekten abgerufen werden, die diese Funktionalität nutzen möchten. Diese Organisation von Projekten reduziert die Größe anderer Skripte erheblich und stellt außerdem sicher, dass die Codebasis klein und leicht testbar ist.



Wie im obigen Beispiel alle Logging - Funktionen, wie zum Beispiel __msg_info, __msg_errorund andere, wie Berichte von Slack, gehalten separat in common/*und dynamisch auf andere Szenarien verbinden, wie daily_database_operation.sh.



Lassen Sie ein sauberes System zurĂĽck



Wenn Sie einige Ressourcen laden, während das Skript ausgeführt wird, wird empfohlen, alle diese Daten beispielsweise in einem freigegebenen Verzeichnis mit einem zufälligen Namen zu speichern /tmp/AlRhYbD97/*. Sie können zufällige 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 Arbeiten kann die Reinigung solcher Verzeichnisse in den oben diskutierten Hook-Handlern bereitgestellt werden. Wenn Sie sich nicht darum kümmern, temporäre Verzeichnisse zu löschen, sammeln sie sich an und verursachen irgendwann unerwartete Probleme auf dem Host, z. B. eine vollständige Festplatte.



Verwenden von Sperrdateien



Oft mĂĽssen Sie sicherstellen, dass jeweils nur eine Instanz eines Skripts auf einem Host ausgefĂĽhrt wird. Dies kann mithilfe von Sperrdateien erfolgen.



Normalerweise erstelle ich Sperrdateien /tmp/project_name/*.lockund überprüfe, ob sie am Anfang des Skripts vorhanden sind. Dies hilft, das Skript korrekt zu beenden und unerwartete Systemstatusänderungen durch ein anderes parallel ausgeführtes Skript zu vermeiden. Sperrdateien werden nicht benötigt, wenn Sie dasselbe Skript benötigen, um auf einem bestimmten Host parallel ausgeführt zu werden.



Messen und verbessern



Wir müssen häufig mit Skripten arbeiten, die über einen langen Zeitraum ausgeführt werden, z. B. tägliche Datenbankoperationen. Solche Vorgänge umfassen normalerweise eine Abfolge von Schritten: Laden von Daten, Überprüfen auf Anomalien, Importieren von Daten, Senden von Statusberichten usw.



In solchen Fällen versuche ich immer, das Skript in separate kleine Skripte zu unterteilen und deren Status und Ausführungszeit mit folgenden Angaben zu melden:



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


Später kann ich die Laufzeit 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.



Viel GlĂĽck!



Was noch zu lesen:



  1. Go und GPU-Caches.
  2. Ein Beispiel fĂĽr eine ereignisgesteuerte Webhook-basierte Anwendung im Objektspeicher von Mail.ru Cloud Solutions S3.
  3. Unser Telegrammkanal zur digitalen Transformation.



All Articles