Najbolji primjeri iz prakse Bash skriptiranja: Brzi vodič za pouzdane i učinkovite Bash skripte

Najbolji primjeri iz prakse Bash skriptiranja: Brzi vodič za pouzdane i učinkovite Bash skripte
Shell wallpaper by manapi

Otklanjanje grešaka u bash skriptama je poput traženja igle u plastu sijena, posebno kada se novi dodaci pojavljuju u postojećoj bazi koda bez pravovremenog razmatranja pitanja strukture, evidentiranja i pouzdanosti. Možete se naći u takvim situacijama ili zbog vlastitih grešaka ili kada upravljate složenim hrpama skripti.

tim Mail.ru Cloud rješenja preveo je članak s preporukama koje će vam pomoći da bolje pišete, otklanjate greške i održavate svoje skripte. Vjerovali ili ne, ništa nije bolje od zadovoljstva pisanja čistog, spremnog za korištenje bash koda koji radi svaki put.

U članku autor iznosi ono što je naučio u proteklih nekoliko godina, kao i neke uobičajene greške koje su ga uhvatile nespremnog. Ovo je važno jer svaki programer softvera, u nekom trenutku svoje karijere, radi sa skriptama za automatizaciju rutinskih radnih zadataka.

Rukovaoci zamkama

Većina bash skripti koje sam sreo nikada ne koristi efikasan mehanizam čišćenja kada se nešto neočekivano dogodi tokom izvršavanja skripte.

Iznenađenja mogu nastati izvana, kao što je primanje signala iz jezgre. Rukovanje takvim slučajevima je izuzetno važno kako bi se osiguralo da su skripte dovoljno pouzdane za rad na proizvodnim sistemima. Često koristim izlazne rukovatelje da odgovorim na ovakve scenarije:

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 naredba ugrađena u ljusku koja vam pomaže da registrirate funkciju čišćenja koja se poziva u slučaju bilo kakvih signala. Međutim, posebnu pažnju treba posvetiti rukovaocima kao što su npr SIGINT, što uzrokuje prekid skripte.

Osim toga, u većini slučajeva trebate samo uhvatiti EXIT, ali ideja je da zapravo možete prilagoditi ponašanje skripte za svaki pojedinačni signal.

Ugrađene funkcije - brzi prekid u slučaju greške

Veoma je važno odgovoriti na greške čim se pojave i brzo zaustaviti izvršenje. Ništa ne može biti gore od nastavka pokretanja naredbe poput ove:

rm -rf ${directory_name}/*

Imajte na umu da varijabla directory_name nije utvrđeno.

Važno je koristiti ugrađene funkcije za rukovanje takvim scenarijima set, kao što su set -o errexit, set -o pipefail ili set -o nounset na početku scenarija. Ove funkcije osiguravaju da će vaša skripta izaći čim naiđe na bilo koji izlazni kod različit od nule, korištenje nedefiniranih varijabli, nevažeće komande proslijeđene preko cijevi i tako dalje:

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

Napomena: ugrađene funkcije kao što su set -o errexit, izaći će iz skripte čim postoji "sirovi" povratni kod (osim nule). Stoga je bolje uvesti prilagođeno rukovanje greškama, na primjer:

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

Pisanje skripti na ovaj način prisiljava vas da budete pažljiviji u pogledu ponašanja svih naredbi u skripti i da predvidite mogućnost greške prije nego što vas iznenadi.

ShellCheck za otkrivanje grešaka tokom razvoja

Vrijedi integrirati nešto poput ShellCheck u vaše razvojne i testne kanale kako biste provjerili vaš bash kod u odnosu na najbolje prakse.

Koristim ga u svojim lokalnim razvojnim okruženjima da dobijem izvještaje o sintaksi, semantici i nekim greškama u kodu koje sam možda propustio dok sam razvijao. Ovo je alat za statičku analizu vaših bash skripti i toplo preporučujem da ga koristite.

Korištenje vlastitih izlaznih kodova

Povratni kodovi u POSIX-u nisu samo nula ili jedan, već nula ili različita od nule. Koristite ove funkcije da vratite prilagođene kodove grešaka (između 201-254) za različite slučajeve greške.

Ovu informaciju zatim mogu koristiti druge skripte koje pokrivaju vašu kako bi razumjeli tačno koja se vrsta greške dogodila i reagirali u skladu s tim:

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

Napomena: budite posebno pažljivi s imenima varijabli koje definirate kako biste izbjegli slučajno nadjačavanje varijabli okruženja.

Funkcije zapisivanja

Lijepo i strukturirano evidentiranje je važno za lako razumijevanje rezultata vaše skripte. Kao i sa drugim programskim jezicima visokog nivoa, uvijek koristim izvorne funkcije evidentiranja u svojim bash skriptama, kao što je __msg_info, __msg_error i tako dalje.

Ovo pomaže u obezbjeđivanju standardizirane strukture evidentiranja unosom promjena na samo jednom mjestu:

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

Obično pokušavam da imam neku vrstu mehanizma u svojim skriptama __init, gdje su takve varijable logera i druge sistemske varijable inicijalizirane ili postavljene na zadane vrijednosti. Ove varijable se također mogu postaviti iz opcija komandne linije tokom pozivanja skripte.

Na primjer, nešto poput:

$ ./run-script.sh --debug

Kada se takva skripta izvrši, ona osigurava da su postavke za cijeli sistem postavljene na zadane vrijednosti ako su potrebne, ili barem inicijalizirane na nešto prikladno ako je potrebno.

Obično zasnivam izbor šta da inicijalizujem, a šta ne radim na kompromisu između korisničkog interfejsa i detalja konfiguracija u koje korisnik može/treba da se udubi.

Arhitektura za ponovnu upotrebu i čisto stanje sistema

Modularni kod za višekratnu upotrebu

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

Čuvam odvojeno spremište koje mogu koristiti za inicijalizaciju nove project/bash skripte koju želim da razvijem. Sve što se može ponovo koristiti može se pohraniti u spremište i preuzeti od strane drugih projekata koji žele koristiti tu funkcionalnost. Organiziranje projekata na ovaj način značajno smanjuje veličinu drugih skripti i također osigurava da je baza koda mala i laka za testiranje.

Kao u gornjem primjeru, sve funkcije evidentiranja kao što su __msg_info, __msg_error a drugi, kao što su Slack izvještaji, sadržani su zasebno u common/* i dinamički se povezuju u drugim scenarijima kao što su daily_database_operation.sh.

Ostavite čist sistem

Ako učitavate bilo koje resurse dok je skripta pokrenuta, preporučuje se da sve takve podatke pohranite u zajednički direktorij s nasumičnim imenom, npr. /tmp/AlRhYbD97/*. Možete koristiti nasumične generatore teksta da odaberete ime direktorija:

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

Nakon završetka rada, čišćenje takvih direktorija može biti omogućeno u rukovaocima zakačivama o kojima smo gore govorili. Ako se ne vodi računa o privremenim direktorijima, oni se akumuliraju i u nekoj fazi uzrokuju neočekivane probleme na hostu, kao što je pun disk.

Korištenje fajlova zaključavanja

Često morate osigurati da se samo jedna instanca skripte izvodi na hostu u bilo kojem trenutku. To se može učiniti pomoću datoteka za zaključavanje.

Obično kreiram fajlove zaključavanja /tmp/project_name/*.lock i provjerite njihovo prisustvo na početku skripte. Ovo pomaže da se skripta elegantno završi i izbjegne neočekivane promjene stanja sistema od strane druge skripte koja se izvodi paralelno. Datoteke zaključavanja nisu potrebne ako trebate da se ista skripta izvršava paralelno na datom hostu.

Izmjerite i poboljšajte

Često moramo da radimo sa skriptama koje se izvode tokom dugog vremenskog perioda, kao što su dnevne operacije baze podataka. Takve operacije obično uključuju niz koraka: učitavanje podataka, provjeru anomalija, uvoz podataka, slanje izvještaja o statusu itd.

U takvim slučajevima, uvijek pokušavam razbiti skriptu u zasebne male skripte i prijaviti njihov status i vrijeme izvršenja koristeći:

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

Kasnije mogu vidjeti vrijeme izvršenja sa:

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

Ovo mi pomaže da identifikujem problematična/spora područja u skriptama kojima je potrebna optimizacija.

Sretno!

Šta još pročitati:

  1. Idi i GPU keš.
  2. Primjer aplikacije vođene događajima zasnovane na webhookovima u skladištu objekata S3 Mail.ru Cloud Solutions.
  3. Naš telegram kanal o digitalnoj transformaciji.

izvor: www.habr.com

Dodajte komentar