Beste praktijken voor bash-scripts: een kort overzicht van betrouwbare en efficiënte bash-scripts

Beste praktijken voor bash-scripts: een kort overzicht van betrouwbare en efficiënte bash-scripts
Achtergrondafbeelding van manapi

Het debuggen van bash-scripts is als het zoeken naar een speld in een hooiberg, vooral wanneer nieuwe toevoegingen in een bestaande codebasis verschijnen zonder tijdige aandacht voor structuur, logging en betrouwbaarheid. In zulke situaties kun je te maken krijgen met zowel eigen fouten als het beheren van complexe stapels scripts.

Opdracht Mail.ru Cloud Solutions Ik heb een artikel vertaald met aanbevelingen waarmee je beter kunt schrijven, debuggen en je scripts kunt onderhouden. Geloof het of niet, maar niets kan tippen aan de voldoening die je voelt bij het schrijven van schone, bruikbare bash-code die elke keer werkt.

In het artikel deelt de auteur wat hij de afgelopen jaren heeft geleerd, evenals enkele veelvoorkomende fouten die hem onverwachts overkwamen. Dit is belangrijk omdat elke softwareontwikkelaar op een bepaald moment in zijn carrière met scripts werkt voor het automatiseren van routinetaken.

Signaalhandlers

De meeste bash-scripts waarmee ik te maken heb gehad, maakten nooit gebruik van een effectief opruimmekanisme wanneer er tijdens de uitvoering van het script iets onverwachts gebeurde.

Onverwachte situaties kunnen van buitenaf ontstaan, zoals het ontvangen van een signaal van de kernel. Het afhandelen van dergelijke situaties is van cruciaal belang om ervoor te zorgen dat scripts betrouwbaar genoeg zijn voor productieomgevingen. Ik gebruik vaak exit-handlers om op dergelijke scenario's te reageren:

function handle_exit() {
  // Voeg hier opruimcode toe
  // bijvoorbeeld rm -f "/tmp/${lock_file}.lock"
  // exit met een geschikte statuscode
}
  
// trap  
trap handle_exit 0 SIGHUP SIGINT SIGQUIT SIGABRT SIGTERM

trap is een ingebouwde shell-opdracht die je helpt een opruimfunctie te registreren die wordt aangeroepen bij het ontvangen van bepaalde signalen. Het is echter belangrijk om voorzichtig te zijn met zulke handlers, zoals SIGINT, welke het script onderbreekt.

Bovendien is het in de meeste gevallen aan te raden om alleen te vangen EXIT, maar het idee is dat je het gedrag van het script echt kunt afstemmen voor elk afzonderlijk signaal.

Ingebouwde functies set - snelle beëindiging bij fout

Het is erg belangrijk om op fouten te reageren zodra ze zich voordoen en snel het proces te stoppen. Niets is erger dan door te gaan met een opdracht zoals deze:

rm -rf ${directory_name}/*

Let op dat de variabele directory_name niet is gedefinieerd.

Voor het omgaan met dergelijke scenario's is het belangrijk om ingebouwde functies te gebruiken set, zoals set -o errexit, set -o pipefail of set -o nounset aan het begin van het script. Deze functies zorgen ervoor dat uw script stopt zodra het een niet-nul exitcode, het gebruik van niet-gedefinieerde variabelen, verkeerde commando's die via een pijp zijn doorgegeven, enzovoort, tegenkomt:

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

Opmerking: ingebouwde functies zoals set -o errexit, verlaten het script zodra er een ‘onverwerkte’ retourcode (behalve nul) verschijnt. Daarom is het beter om een gebruikerspecifieke foutafhandeling te introduceren, zoals:

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

Een dergelijk schrijven van scripts dwingt je om aandachtiger om te gaan met het gedrag van alle commando's in het script en om rekening te houden met de mogelijkheid van fouten voordat ze je verrassen.

ShellCheck voor het opsporen van fouten tijdens de ontwikkeling

Het is de moeite waard om iets te integreren zoals ShellCheck in uw ontwikkelings- en testpijplijnen om uw bash-code te controleren op het toepassen van best practices.

Ik gebruik het in mijn lokale ontwikkelomgevingen om rapporten te krijgen over de syntaxis, semantiek en enkele fouten in de code die ik tijdens de ontwikkeling zou kunnen missen. Het is een statische analyse-tool voor uw bash-scripts, en ik raad ten zeerste aan het te gebruiken.

Het gebruik van uw exit-codes

Retourcodes in POSIX zijn niet alleen nul of één, maar nul of een niet-nul waarde. Gebruik deze mogelijkheden om aangepaste foutcodes (tussen 201-254) terug te geven voor verschillende foutgevallen.

Deze informatie kan vervolgens door andere scripts die het uwe omhullen, worden gebruikt om precies te begrijpen welk type fout is opgetreden en passend te reageren:

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

Opmerking: Wees vooral voorzichtig met de namen van de variabelen die u definieert, om te voorkomen dat u per ongeluk omgevingsvariabelen overschrijft.

Logger-functies

Een mooie en gestructureerde logging is belangrijk om de resultaten van je script eenvoudig te begrijpen. Zoals in andere high-level programmeertalen gebruik ik altijd mijn eigen loggingfuncties in mijn bash-scripts, zoals __msg_info, __msg_error en zo verder.

Dit helpt een gestandaardiseerde structuur voor logging te waarborgen, waarbij wijzigingen alleen op één plaats worden aangebracht:

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

Ik probeer meestal om in mijn scripts een mechanisme te hebben __init, waar dergelijke logger-variabelen en andere systeemvariabelen worden geïnitialiseerd of op standaardwaarden worden ingesteld. Deze variabelen kunnen ook worden ingesteld vanuit commandoregelparameters tijdens het aanroepen van het script.

Bijvoorbeeld, iets als:

$ ./run-script.sh --debug

Wanneer zo'n script wordt uitgevoerd, is gegarandeerd dat systeeminstellingen zijn ingesteld op standaardwaarden, als deze verplicht zijn, of op zijn minst zijn geïnitialiseerd met iets passends, indien nodig.

Ik baseer meestal de keuze wat te initialiseren en wat niet op een compromis tussen de gebruikersinterface en de detailniveaus van configuraties waar de gebruiker in kan/moet duiken.

Architectuur voor hergebruik en een schone systeemstatus

Modulaire / herbruikbare code

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

Ik houd een aparte repository aan die kan worden gebruikt voor het initialiseren van een nieuw bash-project/script dat ik wil ontwikkelen. Alles wat herbruikbaar is, kan in de repository worden opgeslagen en in andere projecten worden opgehaald die dergelijke functionaliteiten willen gebruiken. Deze organisatie van projecten vermindert aanzienlijk de grootte van andere scripts en zorgt ervoor dat de codebasis klein en gemakkelijk testbaar is.

Zoals in het bovenstaande voorbeeld, worden alle loggingfuncties, zoals __msg_info, __msg_error en andere, zoals Slack-rapporten, apart bewaard in common/* en dynamisch gekoppeld in andere scripts, zoals daily_database_operation.sh.

Laat een schone systeemstatus achter

Als je tijdens de uitvoering van het script bronnen laadt, wordt aanbevolen om alle dergelijke gegevens in een openbare map met een willekeurige naam te bewaren, bijvoorbeeld /tmp/AlRhYbD97/*U kunt willekeurige tekstgeneratoren gebruiken om een directorynaam te kiezen:

rand_dir_name="$(cat /dev/urandom | tr -dc 'a-zA-Z0-9' | fold -w 16 | head -n 1)"

Na voltooiing kan het opruimen van dergelijke mappen worden geregeld in de traphandlers die hierboven zijn besproken. Als er niet voor gezorgd wordt dat tijdelijke directories worden verwijderd, hebben ze de neiging zich op te stapelen en kunnen ze op een gegeven moment onverwachte problemen op de host veroorzaken, zoals een volle schijf.

Gebruik van lock-bestanden

Vaak moet ervoor gezorgd worden dat er op elk moment maar één exemplaar van het script op de host wordt uitgevoerd. Dit kan worden bereikt met lock-bestanden.

Ik maak meestal lock-bestanden aan in /tmp/project_name/*.lock en controleer hun aanwezigheid aan het begin van het script. Dit helpt om het script correct af te ronden en onverwachte wijzigingen in de systeemstatus door een ander parallel script te vermijden. Lock-bestanden zijn niet nodig als u enkele dezelfde scripts parallel op deze host wilt uitvoeren.

Meten en verbeteren

We hebben vaak te maken met scripts die voor langere tijd draaien, zoals dagelijkse database-operaties. Dergelijke operaties omvatten meestal een reeks stappen: gegevens laden, controleren op anomalieën, gegevens importeren, statusrapporten verzenden, enz.

In zulke gevallen probeer ik altijd het script op te splitsen in aparte kleine scripts en meld ik hun status en uitvoeringstijd met behulp van:

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

Later kan ik de uitvoeringstijd bekijken met:

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

Dit helpt me om probleemgebieden/traagheidszones in scripts te identificeren die geoptimaliseerd moeten worden.

Veel succes!

Wat verder te lezen:

  1. Go en GPU-caches.
  2. Voorbeeld van een event-driven applicatie op basis van webhooks in de S3-objectopslag van Mail.ru Cloud Solutions.
  3. Ons Telegram-kanaal over digitale transformatie.

Bron: habr.com

Koop betrouwbare webhosting met bescherming tegen DDoS, VPS VDS servers 🔥 Koop betrouwbare webhosting met bescherming tegen DDoS, VPS VDS servers | ProHoster