Cele mai bune practici pentru scripturi bash: un ghid concis pentru scripturi bash fiabile și eficiente

Cele mai bune practici pentru scripturi bash: un ghid concis pentru scripturi bash fiabile și eficiente
Tapetă Shell de la manapi

Debugarea scripturilor bash este ca și cum ai căuta un ac într-un stack de fân, mai ales când apar noi adăugiri în codul existent fără o revizuire punctuală a structurii, jurnalizării și fiabilității. În astfel de situații, poți fi prins atât din cauza propriilor greșeli, cât și din cauza gestionării unor aglomerări complexe de scripturi.

Comanda Soluții Cloud Mail.ru Am tradus un articol cu recomandări care te vor ajuta să scrii, să debugezi și să menții mai bine scripturile tale. Crede-mă sau nu, dar nimic nu se compară cu satisfacția de a scrie cod bash curat, gata de utilizare, care funcționează de fiecare dată.

În articol, autorul împărtășește ceea ce a învățat în ultimii câțiva ani, precum și câteva greșeli comune care l-au surprins. Acest lucru este important, deoarece fiecare dezvoltator de software, la un moment dat în cariera sa, lucrează cu scripturi pentru automatizarea sarcinilor de rutină.

Handlerii de semnale

Cele mai multe scripturi bash cu care m-am confruntat nu au folosit niciodată un mecanism eficient de curățare atunci când apare ceva neașteptat în timpul execuției scriptului.

Neașteptările pot apărea din exterior, cum ar fi primirea unui semnal de la nucleu. Tratarea acestor cazuri este extrem de importantă pentru a asigura că scripturile sunt suficient de fiabile pentru a rula în sisteme de producție. Folosesc adesea handleri de ieșire pentru a reacționa la astfel de scenarii:

function handle_exit() {
  // Adaugă cod de curățare aici
  // de exemplu. rm -f "/tmp/${lock_file}.lock"
  // ieșire cu un cod de stare corespunzător
}
  
// fură  
trap handle_exit 0 SIGHUP SIGINT SIGQUIT SIGABRT SIGTERM

trap — este o comandă încorporată a shell-ului care te ajută să înregistrezi o funcție de curățare care este apelată în caz de semnale. Totuși, trebuie să fii deosebit de precaut cu astfel de handleri, cum ar fi SIGINT, care provoacă întreruperea scriptului.

În plus, în majoritatea cazurilor, ar trebui să prinzi doar EXIT, dar ideea este că poți personaliza comportamentul scriptului pentru fiecare semnal individual.

Funcțiile încorporate set — finalizare rapidă în caz de eroare

Este foarte important să reacționezi la erori imediat ce apar și să oprești execuția rapid. Nu există nimic mai rău decât să continui executarea unei comenzi precum aceasta:

rm -rf ${directory_name}/*

Rețineți că variabila directory_name nu este definită.

Pentru a gestiona astfel de scenarii, este important să folosești funcții încorporate set, cum ar fi set -o errexit, set -o pipefail sau set -o nounset la începutul scriptului. Aceste funcții garantează că scriptul tău se va încheia imediat ce întâlnește orice cod de ieșire diferit de zero, utilizarea variabilelor nedefinite, comenzi incorecte transmise prin canal și așa mai departe:

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

Notă: funcții încorporate, cum ar fi set -o errexit, vor ieși din script imediat ce apare un cod de returnare „neprelucrat” (cu excepția zero). De aceea, este mai bine să introduci un management personalizat al erorilor, de exemplu:

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

Un astfel de scris de scripturi te face să te gândești mai atent la comportamentul tuturor comenzilor din script și să previzualizezi posibilitatea apariției unei erori înainte ca aceasta să te surprindă.

ShellCheck pentru identificarea erorilor în timpul dezvoltării

Merită să integrezi ceva precum ShellCheck în conductele tale de dezvoltare și testare pentru a verifica codul tău bash conform celor mai bune practici.

Îl folosesc în mediile mele locale de dezvoltare pentru a obține rapoarte despre sintaxă, semantică și unele erori în cod pe care le-aș fi putut rata în timpul dezvoltării. Este un instrument de analiză statică pentru scripturile tale bash și recomand cu căldură aplicarea acestuia.

Utilizarea propriilor coduri de ieșire

Codurile de returnare în POSIX nu sunt doar zero sau unu, ci zero sau o valoare diferită de zero. Folosește aceste capabilități pentru a returna coduri de eroare personalizate (între 201-254) pentru diferite situații de eroare.

Aceste informații pot fi apoi folosite de alte scripturi care îți învăluiesc scriptul pentru a înțelege exact ce tip de eroare a apărut și a reacționa corespunzător:

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

Notă: te rog să acorzi o atenție deosebită numelui variabilelor pe care le definești pentru a evita redefinirea accidentală a variabilelor de mediu.

Funcții de logare

Un jurnal frumos și structurat este important pentru a înțelege ușor rezultatele execuției scriptului tău. Așa cum fac în alte limbaje de programare de înalt nivel, folosesc întotdeauna funcții proprii de logare în scripturile mele bash, cum ar fi __msg_info, __msg_error și așa mai departe.

Acest lucru ajută la asigurarea unei structuri standardizate de logare, făcând modificări doar într-un singur loc:

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

De obicei, încerc să am în scripturile mele un anumit mecanism __init, unde variabilele de logare și alte variabile de sistem sunt inițializate sau stabilite pe valori prestabilite. Aceste variabile pot fi, de asemenea, stabilite din parametrii liniei de comandă în timpul apelului scriptului.

De exemplu, ceva de genul:

$ ./run-script.sh --debug

Atunci când un astfel de script este executat, este garantat că setările sistemului sunt stabilite pe valorile prestabilite, dacă sunt obligatorii, sau, cel puțin, inițializate cu ceva corespunzător, dacă este necesar.

De obicei, îmi bazează alegerea asupra a ce să inițializez și ce nu, pe un compromis între interfața utilizatorului și detaliile configurațiilor în care utilizatorul poate/trebuie să se implice.

Arhitectura pentru reutilizarea și o stare curată a sistemului

Cod modular / reutilizabil

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

Îmi păstrez un repository separat, care poate fi folosit pentru inițializarea unui nou proiect/script bash pe care vreau să-l dezvolt. Tot ce poate fi reutilizat poate fi păstrat în repository și accesat în alte proiecte care doresc să folosească funcționalitățile respective. O astfel de organizare a proiectelor reduce semnificativ dimensiunea altor scripturi și asigură că baza de cod este mică și ușor de testat.

Ca în exemplul de mai sus, toate funcțiile de logare, cum ar fi __msg_info, __msg_error și alte, cum ar fi rapoartele Slack, sunt conținute separat în common/* și sunt conectate dinamic în alte scenarii, cum ar fi daily_database_operation.sh.

Lăsați în urma o sistem curat

Dacă încarcăți anumite resurse în timpul executării scriptului, este recomandat să păstrați toate aceste date într-un director comun cu un nume aleatoriu, de exemplu /tmp/AlRhYbD97/*Puteți utiliza generatoare de text aleator pentru a alege un nume de director:

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

După finalizarea operațiunii, curățarea acestor directoare poate fi asigurată în handler-ele de semnal discutate mai sus. Dacă nu se are grijă de ștergerea directoarelor temporare, acestea se acumulează și, la un moment dat, pot provoca probleme neașteptate pe gazdă, cum ar fi umplerea discului.

Utilizarea fișierelor de blockare

Adesea, este necesar să asigurăm executarea unui singur exemplu de script pe gazdă în orice moment. Acest lucru se poate face cu ajutorul fișierelor de blockare.

De obicei, creez fișiere de blockare în /tmp/project_name/*.lock și verific existența lor la începutul scriptului. Acest lucru ajută la finalizarea corectă a scriptului și la evitarea modificărilor neașteptate ale stării sistemului de către alt script care rulează în paralel. Fișierele de blockare nu sunt necesare dacă trebuie să aveți același script executat în paralel pe această gazdă.

Măsurați și îmbunătățiți

Adesea, trebuie să lucrăm cu scripturi care rulează pe o perioadă lungă de timp, cum ar fi operațiuni zilnice cu baze de date. Aceste operațiuni includ, de obicei, o secvență de pași: încărcarea datelor, verificarea anomaliilor, importul datelor, trimiterea rapoartelor de stare și așa mai departe.

În astfel de cazuri, încerc întotdeauna să împart scriptul în scripturi mai mici și să raportez despre starea și timpul de execuție folosind:

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

Ulterior, pot verifica timpul de execuție folosind:

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

Acest lucru mă ajută să identific zonele problematice/lente din scripturi care necesită optimizare.

Mult noroc!

Ce altceva să citești:

  1. Go și cache-urile GPU.
  2. Exemplu de aplicație bazată pe evenimente cu webhook-uri în obiectul de stocare S3 Mail.ru Cloud Solutions.
  3. Canalul nostru de telegram despre transformarea digitală.

Sursa: habr.com

Cumpără un hosting fiabil pentru site-uri cu protecție DDoS, servere VPS VDS 🔥 Cumpără un hosting fiabil pentru site-uri cu protecție DDoS, servere VPS VDS | ProHoster