Bash Scripting Best Practices: Rýchly sprievodca spoľahlivými a výkonnými Bash skriptami

Bash Scripting Best Practices: Rýchly sprievodca spoľahlivými a výkonnými Bash skriptami
Shell tapeta od manapi

Ladenie bash skriptov je ako hľadanie ihly v kope sena, najmä keď sa v existujúcej kódovej základni objavia nové doplnky bez včasného zváženia otázok štruktúry, protokolovania a spoľahlivosti. V takýchto situáciách sa môžete ocitnúť buď kvôli vlastným chybám, alebo pri spravovaní zložitých kopov skriptov.

Tím Cloudové riešenia Mail.ru preložil článok s odporúčaniami, ktoré vám pomôžu lepšie písať, ladiť a udržiavať vaše skripty. Verte tomu alebo nie, nič neprekoná spokojnosť s písaním čistého bash kódu pripraveného na použitie, ktorý funguje vždy.

V článku sa autor delí o to, čo sa naučil za posledných pár rokov, ako aj o niektoré bežné chyby, ktoré ho zaskočili. Je to dôležité, pretože každý vývojár softvéru v určitom bode svojej kariéry pracuje so skriptami na automatizáciu rutinných pracovných úloh.

Manipulátory pascí

Väčšina bash skriptov, s ktorými som sa stretol, nikdy nepoužíva účinný mechanizmus čistenia, keď sa počas vykonávania skriptu stane niečo neočakávané.

Prekvapenia môžu vzniknúť zvonku, ako napríklad príjem signálu z jadra. Spracovanie takýchto prípadov je mimoriadne dôležité, aby sa zabezpečilo, že skripty budú dostatočne spoľahlivé na to, aby sa dali spustiť na produkčných systémoch. Často používam obslužné nástroje ukončenia, aby som reagoval na takéto scenáre:

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 je vstavaný príkaz shellu, ktorý vám pomôže zaregistrovať funkciu čistenia, ktorá sa volá v prípade akýchkoľvek signálov. Osobitný pozor si však treba dávať pri manipulátoroch ako napr SIGINT, čo spôsobí prerušenie skriptu.

Okrem toho by ste mali vo väčšine prípadov iba chytať EXIT, ale myšlienka je taká, že v skutočnosti môžete prispôsobiť správanie skriptu pre každý jednotlivý signál.

Vstavané funkcie sady - rýchle ukončenie pri chybe

Je veľmi dôležité reagovať na chyby hneď, ako sa vyskytnú, a rýchlo zastaviť vykonávanie. Nič nemôže byť horšie ako pokračovať v spúšťaní príkazu, ako je tento:

rm -rf ${directory_name}/*

Upozorňujeme, že premenná directory_name neurčené.

Na zvládnutie takýchto scenárov je dôležité používať vstavané funkcie setako napr set -o errexit, set -o pipefail alebo set -o nounset na začiatku scenára. Tieto funkcie zaisťujú, že váš skript sa ukončí hneď, ako narazí na akýkoľvek nenulový kód ukončenia, použitie nedefinovaných premenných, neplatné príkazy prenesené cez potrubie atď.

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

Poznámka: vstavané funkcie ako napr set -o errexit, ukončí skript hneď, ako sa objaví "surový" návratový kód (iný ako nula). Preto je lepšie zaviesť vlastné spracovanie chýb, napríklad:

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

Písanie skriptov týmto spôsobom vás núti byť opatrnejší na správanie všetkých príkazov v skripte a predvídať možnosť chyby skôr, ako vás prekvapí.

ShellCheck na zistenie chýb počas vývoja

Stojí za to integrovať niečo také ShellCheck do vašich vývojových a testovacích kanálov, aby ste porovnali svoj bash kód s osvedčenými postupmi.

Používam ho vo svojich lokálnych vývojových prostrediach na získanie správ o syntaxi, sémantike a niektorých chybách v kóde, ktoré som mohol vynechať pri vývoji. Toto je nástroj na statickú analýzu pre vaše bash skripty a veľmi ho odporúčam používať.

Použitie vlastných výstupných kódov

Návratové kódy v POSIX nie sú len nula alebo jedna, ale nulová alebo nenulová hodnota. Použite tieto funkcie na vrátenie vlastných chybových kódov (medzi 201-254) pre rôzne prípady chýb.

Tieto informácie potom môžu použiť iné skripty, ktoré obalia váš, aby presne pochopili, aký typ chyby sa vyskytol, a podľa toho zareagovali:

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

Poznámka: buďte obzvlášť opatrní pri názvoch premenných, ktoré definujete, aby ste predišli náhodnému prepísaniu premenných prostredia.

Logovacie funkcie

Krásne a štruktúrované protokolovanie je dôležité na ľahké pochopenie výsledkov vášho skriptu. Rovnako ako pri iných programovacích jazykoch na vysokej úrovni, vždy vo svojich bash skriptoch používam natívne funkcie logovania, ako napr __msg_info, __msg_error a tak ďalej.

Pomáha to poskytnúť štandardizovanú štruktúru protokolovania vykonávaním zmien iba na jednom mieste:

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

Zvyčajne sa snažím mať v skriptoch nejaký mechanizmus __init, kde sú takéto premenné zapisovača a iné systémové premenné inicializované alebo nastavené na predvolené hodnoty. Tieto premenné je možné nastaviť aj z volieb príkazového riadka počas vyvolávania skriptu.

Napríklad niečo ako:

$ ./run-script.sh --debug

Keď sa takýto skript spustí, zaisťuje, že nastavenia celého systému sú nastavené na predvolené hodnoty, ak sú potrebné, alebo aspoň inicializované na niečo vhodné, ak je to potrebné.

Pri výbere toho, čo inicializovať a čo nie, zvyčajne zakladám na kompromise medzi používateľským rozhraním a detailmi konfigurácií, do ktorých sa používateľ môže/mal ponoriť.

Architektúra pre opätovné použitie a čistý stav systému

Modulárny/opakovane použiteľný kód

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

Mám samostatné úložisko, ktoré môžem použiť na inicializáciu nového skriptu projektu/bash, ktorý chcem vyvinúť. Čokoľvek, čo sa dá znova použiť, môže byť uložené v úložisku a načítané inými projektmi, ktoré chcú túto funkciu použiť. Organizácia projektov týmto spôsobom výrazne znižuje veľkosť iných skriptov a tiež zaisťuje, že základňa kódu je malá a ľahko sa testuje.

Ako v príklade vyššie, všetky logovacie funkcie ako napr __msg_info, __msg_error a ďalšie, ako napríklad správy Slack, sú uvedené samostatne common/* a dynamicky sa pripájajte v iných scenároch, napr daily_database_operation.sh.

Nechajte za sebou čistý systém

Ak načítavate nejaké zdroje počas behu skriptu, odporúča sa všetky takéto dáta ukladať do zdieľaného adresára s náhodným názvom, napr. /tmp/AlRhYbD97/*. Na výber názvu adresára môžete použiť generátory náhodného textu:

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

Po dokončení práce je možné vykonať vyčistenie takýchto adresárov v obslužných programoch háčikov diskutovaných vyššie. Ak sa o dočasné adresáre nestará, hromadia sa a v určitej fáze spôsobujú na hostiteľovi neočakávané problémy, ako je napríklad plný disk.

Používanie uzamknutých súborov

Často sa musíte uistiť, že na hostiteľovi beží v danom čase iba jedna inštancia skriptu. To je možné vykonať pomocou zámkových súborov.

Zvyčajne vytváram uzamknuté súbory v /tmp/project_name/*.lock a skontrolujte ich prítomnosť na začiatku skriptu. To pomáha ladne ukončiť skript a vyhnúť sa neočakávaným zmenám stavu systému iným paralelne spusteným skriptom. Zámkové súbory nie sú potrebné, ak potrebujete, aby sa rovnaký skript vykonával paralelne na danom hostiteľovi.

Merať a zlepšovať

Často potrebujeme pracovať so skriptami, ktoré bežia počas dlhých časových období, ako sú napríklad denné operácie s databázou. Takéto operácie zvyčajne zahŕňajú postupnosť krokov: načítanie údajov, kontrola anomálií, import údajov, odosielanie správ o stave atď.

V takýchto prípadoch sa vždy snažím rozdeliť skript na samostatné malé skripty a nahlásiť ich stav a čas vykonania pomocou:

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

Neskôr vidím čas vykonania s:

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

To mi pomáha identifikovať problémové/pomalé oblasti v skriptoch, ktoré potrebujú optimalizáciu.

Good luck!

Čo ešte čítať:

  1. Go a GPU cache.
  2. Príklad aplikácie riadenej udalosťami založenej na webhookoch v objektovom úložisku S3 cloudových riešení Mail.ru.
  3. Náš telegramový kanál o digitálnej transformácii.

Zdroj: hab.com

Pridať komentár