Cele mai bune practici de scriptare Bash: un ghid rapid pentru scripturi Bash de încredere și de performanță

Cele mai bune practici de scriptare Bash: un ghid rapid pentru scripturi Bash de încredere și de performanță
Tapet de Shell de manapi

Depanarea scripturilor bash este ca și cum ați căuta un ac într-un car de fân, mai ales când apar noi adăugiri în baza de cod existentă fără a lua în considerare în timp util problemele de structură, logare și fiabilitate. Vă puteți găsi în astfel de situații fie din cauza propriilor greșeli, fie atunci când gestionați grămezi complexe de scripturi.

Echipă Mail.ru Cloud Solutions a tradus un articol cu ​​recomandări care vă vor ajuta să scrieți, să depanați și să vă întrețineți mai bine scripturile. Credeți sau nu, nimic nu depășește satisfacția de a scrie cod bash curat, gata de utilizat, care funcționează de fiecare dată.

În articol, autorul împărtășește ceea ce a învățat în ultimii ani, precum și câteva greșeli obișnuite care l-au luat pe nepregătite. Acest lucru este important deoarece fiecare dezvoltator de software, la un moment dat în cariera lor, lucrează cu scripturi pentru a automatiza sarcinile de rutină.

Manipulatori de capcane

Majoritatea scripturilor bash pe care le-am întâlnit nu folosesc niciodată un mecanism de curățare eficient când se întâmplă ceva neașteptat în timpul execuției scriptului.

Surprizele pot apărea din exterior, cum ar fi primirea unui semnal de la miez. Gestionarea unor astfel de cazuri este extrem de importantă pentru a vă asigura că scripturile sunt suficient de fiabile pentru a rula pe sistemele de producție. Folosesc adesea handlere de ieșire pentru a răspunde la scenarii ca acesta:

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 este o comandă încorporată în shell care vă ajută să înregistrați o funcție de curățare care este apelată în cazul oricăror semnale. Cu toate acestea, trebuie avută o grijă deosebită cu manipulatorii precum SIGINT, ceea ce face ca scriptul să se anuleze.

În plus, în cele mai multe cazuri ar trebui doar să prinzi EXIT, dar ideea este că puteți personaliza efectiv comportamentul scriptului pentru fiecare semnal individual.

Funcții de set încorporate - terminare rapidă în caz de eroare

Este foarte important să răspundeți la erori imediat ce apar și să opriți rapid execuția. Nimic nu poate fi mai rău decât să continuați să rulați o comandă ca aceasta:

rm -rf ${directory_name}/*

Vă rugăm să rețineți că variabila directory_name nedeterminat.

Este important să folosiți funcții încorporate pentru a gestiona astfel de scenarii set, Ca set -o errexit, set -o pipefail sau set -o nounset la începutul scenariului. Aceste funcții asigură că scriptul tău se va închide de îndată ce întâlnește orice cod de ieșire diferit de zero, utilizarea de variabile nedefinite, comenzi nevalide trecute peste o conductă ș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

Nota: funcții încorporate precum set -o errexit, va ieși din script de îndată ce există un cod de returnare „brut” (altul decât zero). Prin urmare, este mai bine să introduceți o gestionare personalizată a 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

Scrierea scripturilor în acest fel vă obligă să fiți mai atent la comportamentul tuturor comenzilor din script și să anticipați posibilitatea unei erori înainte de a vă lua prin surprindere.

ShellCheck pentru a detecta erorile în timpul dezvoltării

Merită să integrezi ceva de genul ShellCheck în conductele dvs. de dezvoltare și testare pentru a vă verifica codul bash cu cele mai bune practici.

Îl folosesc în mediile mele locale de dezvoltare pentru a obține rapoarte despre sintaxă, semantică și unele erori din cod pe care s-ar putea să le fi ratat în timpul dezvoltării. Acesta este un instrument de analiză statică pentru scripturile tale bash și recomand cu căldură să-l folosești.

Folosind propriile coduri de ieșire

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

Aceste informații pot fi apoi folosite de alte scripturi care le încap pe ale dvs. pentru a înțelege exact ce tip de eroare a apărut și pentru a reacționa în consecință:

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

Nota: Vă rugăm să fiți deosebit de atenți la numele variabilelor pe care le definiți pentru a evita suprascrierea accidentală a variabilelor de mediu.

Funcții de înregistrare

Jurnalul frumos și structurat este important pentru a înțelege cu ușurință rezultatele scriptului dvs. Ca și în cazul altor limbaje de programare de nivel înalt, folosesc întotdeauna funcții de înregistrare native în scripturile mele bash, cum ar fi __msg_info, __msg_error și așa mai departe.

Acest lucru ajută la furnizarea unei structuri de jurnalizare standardizate, făcând modificări î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 un fel de mecanism în scripturile mele __init, unde astfel de variabile de înregistrare și alte variabile de sistem sunt inițializate sau setate la valorile implicite. Aceste variabile pot fi setate și din opțiunile liniei de comandă în timpul invocării scriptului.

De exemplu, ceva de genul:

$ ./run-script.sh --debug

Când se execută un astfel de script, se asigură că setările la nivel de sistem sunt setate la valorile implicite dacă sunt necesare, sau cel puțin inițializate la ceva adecvat dacă este necesar.

De obicei, alegerea a ceea ce să inițializez și ce să nu fac pe un compromis între interfața cu utilizatorul și detaliile configurațiilor în care utilizatorul poate/ar trebui să se aprofundeze.

Arhitectură pentru reutilizare și starea sistemului curată

Cod modular/reutilizabil

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

Păstrez un depozit separat pe care îl pot folosi pentru a inițializa un nou proiect/script bash pe care vreau să-l dezvolt. Orice lucru care poate fi reutilizat poate fi stocat într-un depozit și preluat de alte proiecte care doresc să folosească această funcționalitate. Organizarea proiectelor în acest fel reduce semnificativ dimensiunea altor scripturi și asigură, de asemenea, că baza de cod este mică și ușor de testat.

Ca în exemplul de mai sus, toate funcțiile de înregistrare, cum ar fi __msg_info, __msg_error iar altele, cum ar fi rapoartele Slack, sunt incluse separat în common/* și conectați dinamic în alte scenarii, cum ar fi daily_database_operation.sh.

Lăsați în urmă un sistem curat

Dacă încărcați orice resurse în timp ce scriptul rulează, este recomandat să stocați toate aceste date într-un director partajat cu un nume aleatoriu, de ex. /tmp/AlRhYbD97/*. Puteți utiliza generatoare aleatorii de text pentru a selecta numele directorului:

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

După finalizarea lucrărilor, curățarea unor astfel de directoare poate fi furnizată în manevrele de cârlig discutate mai sus. Dacă directoarele temporare nu sunt îngrijite, ele se acumulează și la un moment dat provoacă probleme neașteptate pe gazdă, cum ar fi un disc plin.

Utilizarea fișierelor de blocare

Adesea trebuie să vă asigurați că o singură instanță a unui script rulează pe o gazdă la un moment dat. Acest lucru se poate face folosind fișiere de blocare.

De obicei creez fișiere de blocare /tmp/project_name/*.lock și verificați prezența lor la începutul scenariului. Acest lucru ajută scriptul să se termine cu grație și să evite modificările neașteptate ale stării sistemului de către un alt script care rulează în paralel. Fișierele de blocare nu sunt necesare dacă aveți nevoie de același script pentru a fi executat în paralel pe o anumită gazdă.

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

De multe ori trebuie să lucrăm cu scripturi care rulează pe perioade lungi de timp, cum ar fi operațiunile zilnice cu bazele de date. Astfel de operațiuni implică de obicei o secvență de pași: încărcarea datelor, verificarea anomaliilor, importarea datelor, trimiterea rapoartelor de stare și așa mai departe.

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

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

Mai târziu pot vedea timpul de execuție cu:

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

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

Noroc!

Ce altceva de citit:

  1. Go și cache-urile GPU.
  2. Un exemplu de aplicație bazată pe evenimente bazate pe webhook-uri în stocarea obiectelor S3 a Mail.ru Cloud Solutions.
  3. Canalul nostru de telegrame despre transformarea digitală.

Sursa: www.habr.com

Adauga un comentariu