
Att debugga bash-skript Àr som att leta efter en nÄl i en höstack, sÀrskilt nÀr nya tillÀgg dyker upp i en befintlig kodbas utan att strukturen, loggningen och tillförlitligheten beaktas i tid. I sÄdana situationer kan man drabbas av bÄde egna misstag och av att hantera komplicerade skriptkaos.
Team Jag har översatt en artikel med rekommendationer som hjÀlper dig att skriva, debugga och upprÀtthÄlla dina skript pÄ ett bÀttre sÀtt. Tro det eller ej, men ingenting kan jÀmföras med tillfredsstÀllelsen av att skriva ren, anvÀndbar bash-kod som fungerar varje gÄng.
I artikeln delar författaren med sig av vad han har lÀrt sig under de senaste Ären, samt nÄgra vanliga misstag som har överrumplat honom. Detta Àr viktigt eftersom varje mjukvaruutvecklare vid nÄgot tillfÀlle i sin karriÀr arbetar med skript för att automatisera rutinuppgifter.
FÄngsthÄllare
De flesta bash-skript jag har stött pÄ har aldrig anvÀnt en effektiv rengöringsmekanism nÀr nÄgot ovÀntat intrÀffar under körning.
Uppgifter kan uppstÄ utifrÄn, som att fÄ ett signal frÄn kÀrnan. Att hantera sÄdana situationer Àr oerhört viktigt för att skripten ska vara tillrÀckligt tillförlitliga för att köras i produktionssystem. Jag anvÀnder ofta exit-hanterare för att reagera pÄ sÄdana scenarier:
function handle_exit() {
// LÀgg till stÀdkod hÀr
// till exempel rm -f "/tmp/${lock_file}.lock"
// avsluta med en lÀmplig statuskod
}
// trap <LISTA ĂVER SIGNALER ATT FĂ
NGSTI
trap handle_exit 0 SIGHUP SIGINT SIGQUIT SIGABRT SIGTERM
trap Àr en inbyggd kommandorad i terminalen som hjÀlper dig att registrera en rengöringsfunktion som kallas vid vissa signaler. Men sÀrskild försiktighet bör iakttas med sÄdana hanterare som SIGINT, vilket orsakar avbrott i skriptet.
Dessutom bör endast fÄnga EXIT, men idén Àr att du faktiskt kan anpassa skriptets beteende för varje specifik signal.
Inbyggda funktioner set â snabb avslutning vid fel
Det Àr mycket viktigt att reagera pÄ fel sÄ snart de uppstÄr och snabbt avbryta exekveringen. Det kan inte bli vÀrre Àn att fortsÀtta köra ett kommando som detta:
rm -rf ${directory_name}
*
Observera att variabeln directory_name inte Àr definierad.
För att hantera sÄdana scenarier Àr det viktigt att anvÀnda inbyggda funktioner set, sÄsom set -o errexit, set -o pipefail eller set -o nounset i början av skriptet. Dessa funktioner sÀkerstÀller att ditt skript avslutas sÄ snart det stöter pÄ vilken icke-noll exitkod som helst, anvÀndning av odefinierade variabler, felaktiga kommandon som passerar genom en pipeline och sÄ vidare:
#!/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
AnmÀrkning: inbyggda funktioner, som set -o errexit, avslutar skriptet sÄ snart det finns en "obehandlad" returkod (utom noll). DÀrför Àr det bÀttre att införa anvÀndardefinierad felhantering, till exempel:
#!/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
Att skriva sÄdana skript fÄr dig att ta en noggrannare hÀnsyn till beteendet för alla kommandon i skriptet och förutse möjligheten av fel innan de överraskar dig.
ShellCheck för att identifiera fel under utvecklingen
Det Àr vÀrt att integrera nÄgot som i dina utvecklings- och testpipeline för att kontrollera din bash-kod för bÀsta praxis.
Jag anvÀnder det i mina lokala utvecklingsmiljöer för att fÄ rapporter om syntax, semantik och vissa fel i koden som jag kan ha missat under utvecklingen. Det Àr ett statiskt analysverktyg för dina bash-skript och jag rekommenderar starkt att anvÀnda det.
AnvÀndning av egna exit-koder
Returkod i POSIX Àr inte bara noll eller ett, utan noll eller ett icke-nollvÀrde. AnvÀnd dessa möjligheter för att returnera anvÀndardefinierade felkoder (mellan 201-254) för olika fel.
Denna information kan sedan anvÀndas av andra skript som omsluter ditt, för att exakt förstÄ vilken typ av fel som intrÀffade och reagera pÄ lÀmpligt sÀtt:
#!/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
}
AnmÀrkning: VÀnligen var sÀrskilt försiktig med namnen pÄ variabler som du definierar för att undvika oavsiktlig ÄsidosÀttning av miljövariabler.
Logger-funktioner
En vacker och strukturerad loggning Àr viktig för att enkelt förstÄ resultaten av ditt skripts körning. Precis som i andra högprogrammeringssprÄk anvÀnder jag alltid egna loggningsfunktioner i mina bash-skript, sÄsom __msg_info, __msg_error och hÀmta bilder frÄn dab.semrush.com
Detta hjÀlper till att sÀkerstÀlla en standardiserad loggningsstruktur genom att göra Àndringar pÄ ett enda stÀlle:
#!/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"
Jag försöker vanligtvis ha nÄgon typ av mekanism i mina skript __init, dÀr sÄdana loggvariabler och andra systemvariabler initieras eller sÀtts till standardvÀrden. Dessa variabler kan ocksÄ sÀttas av kommandoradens parametrar vid anrop av skriptet.
Till exempel nÄgot i stil med:
$ ./run-script.sh --debug
NÀr ett sÄdant skript körs, garanteras det att globala instÀllningar Àr instÀllda pÄ standardvÀrden om de Àr obligatoriska, eller Ätminstone initialiserade med nÄgot motsvarande om det Àr nödvÀndigt.
Jag baserar vanligtvis mitt val av vad som ska initieras och vad som inte ska initieras pÄ en kompromiss mellan anvÀndargrÀnssnittet och konfigurationsdetaljer som anvÀndaren kan/ska ta till sig.
Arkitektur för ÄteranvÀndning och ren systemstatus
ModulÀr / ÄteranvÀndbar kod
âââ framework
â âââ common
â â âââ loggers.sh
â â âââ mail_reports.sh
â â âââ slack_reports.sh
â âââ daily_database_operation.sh
Jag har ett separat arkiv som kan anvÀndas för att initiera ett nytt projekt/bash-skript som jag vill utveckla. Allt som kan ÄteranvÀndas kan sparas i arkivet och hÀmtas i andra projekt som vill anvÀnda sÄdana funktioner. SÄdan projektorganisation minskar avsevÀrt storleken pÄ andra skript och sÀkerstÀller ocksÄ att kodbasen Àr liten och lÀtt att testa.
Som i det ovanstÄende exemplet finns alla loggningsfunktioner, sÄsom __msg_info, __msg_error och andra, som Slack-rapporter, lagrade separat i common/* och ansluts dynamiskt i andra scenarier, som daily_database_operation.sh.
LĂ€mna efter dig ett rent system
Om du laddar ner nÄgra resurser under skriptets körning, rekommenderas det att lagra alla sÄdana data i en gemensam katalog med ett slumpmÀssigt namn, till exempel /tmp/AlRhYbD97/*Du kan anvÀnda slumpgeneratorer för att vÀlja ett katalognamn:
rand_dir_name="$(cat /dev/urandom | tr -dc 'a-zA-Z0-9' | fold -w 16 | head -n 1)"
Efter slutförandet kan rensning av sÄdana kataloger tillhandahÄllas i de hanterare som diskuterats ovan. Om temporÀra kataloger inte tas om hand, ackumuleras de och orsakar ovÀntade problem pÄ vÀrden, som till exempel en fullstÀndig disk.
AnvÀndning av lÄs-filer
Ofta behöver vi sÀkerstÀlla att endast en instans av skriptet körs pÄ vÀrden vid varje given tidpunkt. Detta kan göras med hjÀlp av lÄs-filer.
Jag brukar skapa lÄs-filer i /tmp/project_name/*.lock och kontrollerar deras existens i början av skriptet. Detta hjÀlper till att avsluta skriptet korrekt och undvika ovÀntade tillstÄnd i systemet frÄn andra skript som körs parallellt. LÄs-filer behövs inte om du behöver att samma skript kan köras parallellt pÄ denna vÀrd.
MÀta och förbÀttra
Vi arbetar ofta med skript som körs under lÄng tid, till exempel dagliga databashanteringsoperationer. SÄdana operationer inkluderar vanligtvis en sekvens av steg: laddning av data, kontroll för anomalier, import av data, skickande av statusrapporter och sÄ vidare.
I sÄdana fall försöker jag alltid dela upp skriptet i separata smÄ skript och rapportera deras status och genomförd tid med hjÀlp av:
time source "${filepath}" "${args}" >> "${LOG_DIR}/RUN_LOG" 2>&1
Senare kan jag se genomförd tid med hjÀlp av:
tac "${LOG_DIR}/RUN_LOG.txt" | grep -m1 "real"
Det hjÀlper mig att identifiera problematiska/lÄngsamma omrÄden i skripten som behöver optimeras.
Lycka till!
Vad mer att lÀsa:
KĂ€lla: habr.com
