
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 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 , 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:
Izvor: habr.com
