
Debugging bash scripts is like searching for a needle in a haystack, especially when new additions appear in the existing codebase without timely consideration of structure, logging, and reliability. In such situations, one can find themselves facing issues due to their own mistakes or while managing complex script entanglements.
Ekipa I translated an article with recommendations that will help you write, debug, and maintain your scripts better. Believe it or not, nothing compares to the satisfaction of writing clean, ready-to-use bash code that works every time.
In the article, the author shares what he has learned over the past few years, as well as some common mistakes that caught him off guard. This is important because every software developer, at some point in their career, works with scripts to automate routine tasks.
Trap Handlers
Most bash scripts I've encountered have never used an effective cleanup mechanism when something unexpected happens during script execution.
Surprises can arise from external sources, such as receiving a signal from the kernel. Handling such cases is extremely important to ensure that 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 invoked in case of any signals. However, special caution should be exercised with such handlers like SIGINT, which interrupts the script.
Additionally, in most cases, you should only catch EXIT, but the idea is that you can actually tailor the script's behavior for each individual signal.
Built-in set functions â quick exit on error
ĂshtĂ« shumĂ« e rĂ«ndĂ«sishme tĂ« reagohet ndaj gabimeve sapo ndodhin dhe tĂ« ndĂ«rpritet menjĂ«herĂ« ekzekutimi. Nuk ka gjĂ« mĂ« tĂ« keqe se tĂ« vazhdosh ekzekutimin e njĂ« komande si kjo:
rm -rf ${directory_name}/*
Vini re se variabli directory_name nuk është i përcaktuar.
Për të trajtuar skenarë të tillë, është e rëndësishme të përdoren funksionet e ndërtuara set, të tilla si set -o errexit, set -o pipefail ose set -o nounset në fillim të skriptit. Këto funksione garantojnë që skripti juaj të përfundojë sapo gjendet ndonjë kod përfundimi jo zero, përdorim të variablave të pa përcaktuar, komanda të gabuara të dhëna për kanal etj:
#!/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
Vërejtje: funksionet e ndërtuara, të tilla si set -o errexit, do të dalin nga skripti sa herë që shfaqet një kod kthimi 'i pa trajtuar' (përveç zero). Prandaj, është më mirë të përfshihen trajtimi i personalizuar i gabimeve, për shembull:
#!/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
Shkrimi i tillë i skripteve ju detyron të jeni më të kujdesshëm ndaj sjelljes së të gjitha komandueseve në skript dhe të parashikoni mundësinë e shfaqjes së gabimeve përpara se ato të ju zënë në befasi.
ShellCheck për zbërthimin e gabimeve gjatë zhvillimit
Wert Integruar diçka si në tubacionet tuaja të zhvillimit dhe testimit për të kontrolluar kodin tuaj bash për përmirësimin e praktikave më të mira.
Unë e përdor atë në ambientet e mia lokale të zhvillimit për të marrë raporte mbi sintaksën, semantikën dhe disa gabime në kod që mund të kem humbur gjatë zhvillimit. Ky është një mjet analize statike për skriptet tuaja bash, dhe e rekomandoj fuqishëm të aplikohet.
Përdorimi i kodave tuaj të daljes
Kodet e kthimit në POSIX nuk janë vetëm zero ose një, por zero ose një vlerë tjetër. Shfrytëzoni këto mundësi për të kthyer kodat e gabimit të personalizuara (ndërmjet 201-254) për raste të ndryshme gabimesh.
Këto informacion mund të përdoren nga skenarë të tjerë që mbështesin tuajin, për të kuptuar saktësisht se çfarë lloj gabimi ka ndodhur dhe për të reaguar përkatësisht:
#!/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
}
Vërejtje: Ju lutem, jini veçanërisht të kujdesshëm me emrat e variablave që përcaktoni, për të mos lejuar mbivendosjen e rastësishme të variablave të ambientit.
Funksionet e regjistrit
Një journaling i bukur dhe i strukturuar është i rëndësishëm për të kuptuar lehtësisht rezultatet e ekzekutimit të skriptit tuaj. Ashtu si në gjuhët e tjera të programimit të nivelit të lartë, unë gjithmonë përdor në skriptet e mia të bash funksione të mia të journaling, siç janë __msg_info, __msg_error dhe kështu me radhë.
Kjo ndihmon në sigurimin e një strukture të standardizuar për journaling, duke bërë ndryshime vetëm në një vend:
#!/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"
Unë zakonisht përpiqem të kem në skriptet e mia një farë mekanizmi __init, ku variablat e journaling dhe variablat e tjera sistemore inicializohen ose vendosen në vlera të paracaktuara. Këto variabla gjithashtu mund të vendosen nga parametrat e vijës së komandës gjatë thirrjes së skriptit.
Për shembull, diçka si:
$ ./run-script.sh --debug
Kur një skript i tillë ekzekutohet, sigurohet që parametrat me sistemin të vendosen në vlerat e paracaktuara, nëse ato janë të detyrueshme, ose të paktën të inicializohen me diçka përkatëse, nëse është e nevojshme.
Unë zakonisht e bazoj zgjedhjen e asaj që të inicializohet dhe asaj që jo, në një kompromis midis ndërfaqes së përdoruesit dhe detajeve të konfigurimeve, në të cilat përdoruesi mund/dëshiron të merret.
Arkitektura për ripërdorim dhe një gjendje të pastër të sistemit
Kode modular / të ripërdorshëm
âââ framework
â âââ common
â â âââ loggers.sh
â â âââ mail_reports.sh
â â âââ slack_reports.sh
â âââ daily_database_operation.sh
UnĂ« mbaj njĂ« depo tĂ« veçantĂ«, e cila mund tĂ« pĂ«rdoret pĂ«r tĂ« inicializuar njĂ« projekt tĂ« ri/skript bash, qĂ« dĂ«shiroj tĂ« zhvilloj. Ădo gjĂ« qĂ« mund tĂ« pĂ«rdoret pĂ«rsĂ«ri, mund tĂ« ruhet nĂ« depo dhe tĂ« merret nĂ« projekte tĂ« tjera qĂ« duan tĂ« pĂ«rdorin kĂ«to funksionalitete. Kjo organizim projektesh zvogĂ«lon ndjeshĂ«m madhĂ«sinĂ« e skripteve tĂ« tjera si dhe garanton qĂ« baza e kodit Ă«shtĂ« e vogĂ«l dhe e lehtĂ« pĂ«r t'u testuar.
Ashtu si në shembujt e sipërm, të gjitha funksionet e journaling, siç janë __msg_info, __msg_error dhe të tjera, si raportet në Slack, ruhen veçmas në common/* dhe lidhen dinamikisht në skripte të tjera, siç është daily_database_operation.sh.
Lini një sistem të pastër pas vetes
Nëse po ngarkoni ndonjë burim gjatë ekzekutimit të skriptit, rekomandohet të ruani të gjitha këto të dhëna në një katalog të përbashkët me emër të rastësishëm, për shembull /tmp/AlRhYbD97/*Ju mund të përdorni gjenerues të teksteve të rastësishme për të zgjedhur emrin e direktorisë:
rand_dir_name="$(cat /dev/urandom | tr -dc 'a-zA-Z0-9' | fold -w 16 | head -n 1)"
Pasi të përfundojë puna, pastrimi i këtyre katalogëve mund të sigurohet në menaxherët e kapjes, të diskutuar më lart. Nëse nuk kujdeseni për eliminimin e direktorive temporare, ato grumbullohen dhe në një moment shkaktojnë probleme të papritura në host, siç është mbushja e disku.
Përdorimi i skedarëve lock
Shpesh është e nevojshme të sigurohet ekzekutimi i vetëm një instance të skenarit në host në çdo moment. Kjo mund të realizohet me anë të skedarëve lock.
Unë zakonisht krijoj skedarë lock në /tmp/project_name/*.lock dhe kontrolloj praninë e tyre në fillim të skenarit. Kjo ndihmon në përfundimin e saktë të skenarit dhe parandalon ndryshimet e papritura të gjendjes së sistemit nga ndonjë skenar tjetër që funksionon paralelisht. Skedarët lock nuk janë të nevojshëm nëse ju nevojitet që i njëjti skenar të ekzekutohet paralelisht në këtë host.
Masa dhe përmirësimi
Shpesh na është dashur të punojmë me skenarë që ekzekutohen për një periudhë të gjatë kohe, si operacione të përditshme me baza të dhënash. Këto operacione zakonisht përfshijnë një rend të hapave: shkarkimi i të dhënave, kontrolli për anomalitë, importimi i të dhënave, dërgimi i raporteve të gjendjes etj.
Në raste të tilla, unë gjithmonë përpiqem të ndaj skenarin në skenare të vegjël dhe të raportoj për gjendjen dhe kohën e ekzekutimit me anë të:
time source "${filepath}" "${args}" >> "${LOG_DIR}/RUN_LOG" 2>&1
Më vonë, unë mund të shoh kohën e ekzekutimit me anë të:
tac "${LOG_DIR}/RUN_LOG.txt" | grep -m1 "real"
Kjo më ndihmon të identifikoj zonat problematike/të ngadalta në skenarët që kërkojnë optimizim.
UPD.
ĂfarĂ« tjetĂ«r mund tĂ« lexoni:
Burimi: habr.com
