Najboljše prakse bash skriptov: kratek priročnik za zanesljive in učinkovite bash skripte

Najboljše prakse bash skriptov: kratek priročnik za zanesljive in učinkovite bash skripte
Shell wallpaper by manapi

Debugging bash scripts is akin to searching for a needle in a haystack, especially when new additions appear in an existing codebase without timely consideration of structure, logging, and reliability concerns. In such situations, one can face issues both from personal mistakes and when managing complex layers of scripts.

Komanda Mail.ru Cloud Solutions I translated an article with recommendations that will help you write, debug, and maintain your scripts better. Whether you believe it or not, nothing compares to the satisfaction of writing clean, ready-to-use bash code that works every time.

In this article, the author shares what they have learned over the past few years, along with some common mistakes that have caught them off guard. This is important because every software developer works with scripts at some point in their career to automate routine tasks.

Trap handlers

Most bash scripts I have encountered never used an effective cleanup mechanism when something unexpected happens during execution.

Unexpected events can originate externally, such as receiving a signal from the kernel. Handling such cases is extremely important for ensuring scripts are reliable enough to run in production systems. I often use exit handlers to respond to such scenarios:

function handle_exit() {
  // Add cleanup code here
  // for eg. rm -f "/tmp/${lock_file}.lock"
  // exit with an appropriate status code
}
  
// trap  
trap handle_exit 0 SIGHUP SIGINT SIGQUIT SIGABRT SIGTERM

trap is a built-in shell command that helps you register a cleanup function that is called in response to any signals. However, one must exercise particular caution with handlers like SIGINT, which triggers script termination.

Moreover, in most cases, one should only catch EXIT, but the idea is that you can customize the behavior of the script for each individual signal.

Built-in set functions — fast exit on error

Zelo pomembno je, da se na napake reagira takoj, ko se te pojavijo, in da se hitro ustavi izvajanje. Nič ni hujšega, kot nadaljevati z izvajanjem ukaza, kot je ta:

rm -rf ${directory_name}/*

Upoštevajte, da spremenljivka directory_name ni definirana.

Za obravnavo takšnih scenarijev je pomembno uporabiti vgrajene funkcije set, kot so set -o errexit, set -o pipefail ili set -o nounset na začetku skripta. Te funkcije zagotavljajo, da se vaš skript ustavi takoj, ko naleti na katero koli nenulčno kodo zaključka, uporabo nedefiniranih spremenljivk, napačne ukaze, poslane po cevi itd.:

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

Opomba: vgrajene funkcije, kot so set -o errexit, se bodo ustavile iz skripta, takoj ko se pojavi "neobdelana" koda vrnitve (razen nič). Zato je bolje uvesti uporabniško obravnavo napak, na primer:

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

Tako pisanje skriptov vas sili, da bolj pozorno gledate na vedenje vseh ukazov v skriptu in predvidite možnost napake, preden vas ta preseneti.

ShellCheck za odkrivanje napak med razvojem

Vredno je integrirati nekaj takega, kot je ShellCheck , v vaše razvojne in testne cevi, da preverite vaš bash kodo glede na najboljše prakse.

Uporabljam ga v svojih lokalnih razvojnjih okoljih, da prejemam poročila o sintaktičnih, semantičnih in nekaterih napakah v kodi, ki sem jih morda spregledal med razvojem. To je orodje za statično analizo vaših bash skriptov in ga močno priporočam.

Uporaba svojih exit-kodov

Vrnitvene kode v POSIX niso samo nič ali ena, temveč nič ali nenulčna vrednost. Izkoristite te možnosti za vračanje uporabniških kod napak (med 201-254) za različne primere napak.

Te informacije se lahko nato uporabljajo v drugih skriptih, ki ovijajo vašega, da natančno razumejo, kakšna napaka se je zgodila, in se ustrezno odzovejo:

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

Opomba: prosim, bodite še posebej previdni z imeni spremenljivk, ki jih definirate, da ne pride do nenamernega prepisovanja okoljskih spremenljivk.

Funkcije-loggerji

Lepe in strukturirano beleženje dnevnikov je pomembno za enostavno razumevanje rezultatov izvajanja vašega skripta. Kot v drugih visokoletei programskih jezikih, vedno uporabljam v svojih bash skriptih lastne funkcije beleženja, kot so __msg_info, __msg_error in tako naprej.

To pomaga zagotoviti standardizirano strukturo beleženja dnevnikov, pri čemer se spremembe izvajajo le na enem mestu:

#!/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čajno se trudim imeti v svojih skriptih nek mekanizem __init, kjer so takšne spremenljivke dnevnika in druge sistemske spremenljivke inicializirane ali nastavljene na privzete vrednosti. Te spremenljivke se lahko tudi nastavijo iz parametrov ukazne vrstice med klicanjem skripta.

Na primer, nekaj takega:

$ ./run-script.sh --debug

Ko se tak skript izvaja, je zagotovljeno, da so splošne sistemske nastavitve nastavljene na privzete vrednosti, če so obvezne, ali vsaj inicializirane z nečim ustreznim, če je to potrebno.

Običajno se odločam, kaj inicializirati in kaj ne, na podlagi kompromisa med uporabniškim vmesnikom in podrobnostmi konfiguracij, v katere se uporabnik lahko/mora poglobiti.

Arhitektura za ponovno uporabo in čisto stanje sistema

Modularna/kontinuirana koda

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

Imam ločeno skladišče, ki ga lahko uporabim za inicializacijo novega projekta/bash skripta, ki ga želim razviti. Vse, kar se lahko ponovno uporabi, je shranjeno v skladišču in dosegljivo v drugih projektih, ki želijo uporabiti te funkcionalnosti. Takšna organizacija projektov znatno zmanjša velikost drugih skriptov ter zagotovi, da je koda majhna in enostavno testirana.

Kot v zgornjem primeru, so vse funkcije beleženja dnevnikov, kot so __msg_info, __msg_error in druge, kot so poročila preko Slacka, shranjene ločeno v common/* in se dinamično povezujejo v drugih scenarijih, kot je daily_database_operation.sh.

Pustite za seboj čist sistem

Če med izvajanjem skripta nalagate kakšne vire, je priporočljivo, da vse te podatke shranite v skupno mapo z naključnim imenom, na primer /tmp/AlRhYbD97/*. Lahko uporabite generatorje naključnega besedila za izbiro imena za direktorij:

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

Po končanem delu je možno čiščenje takšnih direktorijev zagotoviti v obvladovalcih napak, obravnavanih zgoraj. Če čiščenja začasnih direktorijev ne poskrbiš, se ti kopičijo in na neki točki povzročajo nepričakovane težave na strežniku, kot je zasičen disk.

Uporaba lock-datotek

Pogosto je potrebno zagotoviti, da na strežniku v vsakem trenutku teče le ena instanca skripte. To lahko dosežemo z lock-datotekami.

Običajno ustvarjam lock-datoteke v /tmp/project_name/*.lock in preverjam njihovo prisotnost na začetku skripte. To pomaga pravilno zaključiti delo skripte in se izogniti nepričakovanim spremembam stanja sistema z drugim skriptom, ki teče vzporedno. Lock-datoteke niso potrebne, če potrebujete, da se ista skripta izvaja vzporedno na tem strežniku.

Merjenje in izboljšanje

Pogosto moramo delati s skripti, ki se izvajajo daljše časovno obdobje, na primer vsakodnevnimi operacijami z bazami podatkov. Takšne operacije običajno vključujejo zaporedje korakov: nalaganje podatkov, preverjanje za anomalije, uvoz podatkov, pošiljanje poročil o stanju itd.

V takih situacijah vedno poskušam razdeliti skripto na manjše skripte in poročati o njihovem stanju in času izvajanja z:

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

Kasneje lahko pogledam čas izvajanja z:

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

To mi pomaga določiti težavne/počasi delujoče dele v skriptah, ki potrebujejo optimizacijo.

Srečno!

Šta još pročitati:

  1. Go in GPU keširanje.
  2. Primer aplikacije, ki temelji na dogodkih, z uporabo spletnih prizorov v S3 objektni shrambi Mail.ru Cloud Solutions.
  3. Naš Telegram kanal o digitalni transformaciji.

Izvor: habr.com

Kupite zanesljivo gostovanje za spletne strani z zaščito pred DDoS, VPS VDS strežniki 🔥 Kupite zanesljivo gostovanje za spletne strani z zaščito pred DDoS, VPS VDS strežniki | ProHoster