Bash Scripting Best Practices: Rychlý průvodce spolehlivými a výkonnými Bash skripty

Bash Scripting Best Practices: Rychlý průvodce spolehlivými a výkonnými Bash skripty
Shell tapety od manapi

Ladění bash skriptů je jako hledání jehly v kupce sena, zvláště když se ve stávající kódové základně objeví nové doplňky bez včasného zvážení otázek struktury, protokolování a spolehlivosti. V takových situacích se můžete ocitnout buď vlastními chybami, nebo při správě složitých hromad skriptů.

Tým Cloudová řešení Mail.ru přeložil článek s doporučeními, která vám pomohou lépe psát, ladit a udržovat vaše skripty. Věřte tomu nebo ne, nic nepřekoná uspokojení z psaní čistého bash kódu připraveného k použití, který funguje pokaždé.

V článku autor sdílí, co se za posledních pár let naučil, a také některé běžné chyby, které ho zaskočily. To je důležité, protože každý softwarový vývojář v určité fázi své kariéry pracuje se skripty k automatizaci rutinních pracovních úkolů.

Manipulátoři pastí

Většina bash skriptů, se kterými jsem se setkal, nikdy nepoužije účinný mechanismus čištění, když se během provádění skriptu stane něco neočekávaného.

Překvapení mohou vzniknout zvenčí, jako je příjem signálu z jádra. Řešení takových případů je extrémně důležité, aby bylo zajištěno, že skripty jsou dostatečně spolehlivé, aby je bylo možné spustit na produkčních systémech. K reakci na scénáře, jako je tento, často používám ovladače ukončení:

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 vestavěný příkaz shellu, který vám pomůže zaregistrovat funkci čištění, která je volána v případě jakýchkoli signálů. Zvláštní opatrnosti je však třeba věnovat psovodům jako např SIGINT, což způsobí přerušení skriptu.

Navíc byste ve většině případů měli pouze chytat EXIT, ale myšlenka je taková, že ve skutečnosti můžete přizpůsobit chování skriptu pro každý jednotlivý signál.

Vestavěné funkce sady - rychlé ukončení při chybě

Je velmi důležité reagovat na chyby, jakmile k nim dojde, a rychle zastavit provádění. Nic nemůže být horší než pokračovat ve spouštění příkazu, jako je tento:

rm -rf ${directory_name}/*

Vezměte prosím na vědomí, že proměnná directory_name není určeno.

Pro zvládnutí takových scénářů je důležité používat vestavěné funkce set, Jako set -o errexit, set -o pipefail nebo set -o nounset na začátku scénáře. Tyto funkce zajišťují, že se váš skript ukončí, jakmile narazí na jakýkoli nenulový výstupní kód, použití nedefinovaných proměnných, neplatných příkazů předávaných přes rouru atd.

#!/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: vestavěné funkce jako např set -o errexit, ukončí skript, jakmile se objeví "surový" návratový kód (jiný než nula). Proto je lepší zavést vlastní zpracování chyb, napří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

Psaní skriptů tímto způsobem vás nutí být opatrnější na chování všech příkazů ve skriptu a předvídat možnost chyby dříve, než vás překvapí.

ShellCheck pro detekci chyb během vývoje

Vyplatí se integrovat něco takového Shell Check do vašich vývojových a testovacích kanálů, abyste porovnali svůj bash kód s osvědčenými postupy.

Používám jej ve svých lokálních vývojových prostředích k získávání zpráv o syntaxi, sémantice a některých chybách v kódu, které jsem mohl při vývoji přehlédnout. Toto je nástroj pro statickou analýzu pro vaše bash skripty a vřele jej doporučuji používat.

Pomocí vlastních výstupních kódů

Návratové kódy v POSIX nejsou jen nula nebo jedna, ale nulová nebo nenulová hodnota. Tyto funkce použijte k vrácení vlastních chybových kódů (mezi 201-254) pro různé případy chyb.

Tyto informace pak mohou být použity jinými skripty, které obalí váš, aby přesně pochopily, jaký typ chyby nastal, a podle toho zareagovaly:

#!/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 prosím obzvláště opatrní s názvy proměnných, které definujete, aby nedošlo k náhodnému přepsání proměnných prostředí.

Logovací funkce

Krásné a strukturované protokolování je důležité pro snadné pochopení výsledků vašeho skriptu. Stejně jako u jiných programovacích jazyků na vysoké úrovni používám ve svých bash skriptech vždy nativní logovací funkce, jako např __msg_info, __msg_error a tak dále.

To pomáhá zajistit standardizovanou strukturu protokolování prováděním změn pouze na jednom místě:

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

Obvykle se snažím mít ve svých skriptech nějaký mechanismus __init, kde jsou takové loggerové proměnné a další systémové proměnné inicializovány nebo nastaveny na výchozí hodnoty. Tyto proměnné lze také nastavit z voleb příkazového řádku během vyvolání skriptu.

Například něco jako:

$ ./run-script.sh --debug

Když je takový skript spuštěn, zajišťuje, že nastavení celého systému jsou nastavena na výchozí hodnoty, pokud jsou vyžadovány, nebo alespoň inicializovány na něco vhodného, ​​pokud je to nutné.

Volbu toho, co inicializovat a co nedělat, obvykle zakládám na kompromisu mezi uživatelským rozhraním a podrobnostmi konfigurací, do kterých se uživatel může/měl ponořit.

Architektura pro opětovné použití a čistý stav systému

Modulární/opakovaně použitelný kód

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

Mám samostatné úložiště, které mohu použít k inicializaci nového skriptu projektu/bash, který chci vyvinout. Vše, co lze znovu použít, může být uloženo v úložišti a načteno jinými projekty, které chtějí tuto funkci používat. Uspořádání projektů tímto způsobem výrazně snižuje velikost ostatních skriptů a také zajišťuje, že základna kódu je malá a snadno se testuje.

Stejně jako v příkladu výše, všechny funkce protokolování jako např __msg_info, __msg_error a další, jako jsou zprávy Slack, jsou obsaženy samostatně common/* a dynamicky se připojovat v jiných scénářích, jako je daily_database_operation.sh.

Nechte za sebou čistý systém

Pokud načítáte nějaké prostředky za běhu skriptu, doporučuje se všechna taková data ukládat do sdíleného adresáře s náhodným názvem, např. /tmp/AlRhYbD97/*. K výběru názvu adresáře můžete použít 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 lze provést vyčištění takových adresářů ve výše popsaných obslužných programech háčků. Pokud se o dočasné adresáře nestará, hromadí se a v určité fázi způsobují na hostiteli neočekávané problémy, jako je například plný disk.

Použití zamykacích souborů

Často se potřebujete ujistit, že na hostiteli běží v daný okamžik pouze jedna instance skriptu. To lze provést pomocí souborů zámku.

Obvykle vytvářím soubory zámku v /tmp/project_name/*.lock a zkontrolujte jejich přítomnost na začátku skriptu. To pomáhá skriptu řádně ukončit a vyhnout se neočekávaným změnám stavu systému jiným paralelně spuštěným skriptem. Soubory zámku nejsou potřeba, pokud potřebujete, aby byl stejný skript spuštěn paralelně na daném hostiteli.

Měřit a zlepšovat

Často potřebujeme pracovat se skripty, které běží po dlouhou dobu, jako jsou každodenní operace s databází. Takové operace obvykle zahrnují sekvenci kroků: načítání dat, kontrola anomálií, import dat, odesílání zpráv o stavu a tak dále.

V takových případech se vždy snažím rozdělit skript na samostatné malé skripty a hlásit jejich stav a dobu provádění pomocí:

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

Později vidím čas provedení s:

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

To mi pomáhá identifikovat problémové/pomalé oblasti ve skriptech, které vyžadují optimalizaci.

Good luck!

Co ještě číst:

  1. Go a mezipaměti GPU.
  2. Příklad aplikace řízené událostmi založené na webhoocích v objektovém úložišti S3 Cloud Solutions Mail.ru.
  3. Náš telegramový kanál o digitální transformaci.

Zdroj: www.habr.com

Přidat komentář