
Debugging bash scripts er som at lede efter en nål i en høstak, især når nye tilføjelser dukker op i den eksisterende kodebase uden at tage højde for struktur, logging og pålidelighed. I sådanne situationer kan man støde på problemer både på grund af egne fejl og ved håndtering af komplekse lag af scripts.
Kommandoen oversatte artiklen med anbefalinger, der kan hjælpe dig med at skrive, debugge og vedligeholde dine scripts bedre. Tro det eller ej, men intet kan sammenlignes med tilfredsstillelsen ved at skrive ren, brugsklar bash-kode, der fungerer hver gang.
I artiklen deler forfatteren, hvad han har lært gennem de seneste par år, samt nogle almindelige fejl, der har taget ham på sengen. Dette er vigtigt, fordi hver softwareudvikler på et tidspunkt i sin karriere arbejder med scripts til at automatisere rutinemæssige arbejdsopgaver.
Signalhåndterere
De fleste bash-scripts, jeg har stødt på, har aldrig brugt en effektiv oprydningsmekanisme, når der sker noget uventet under scriptkørslen.
Uventede situationer kan opstå fra omverdenen, såsom at modtage et signal fra kernen. At håndtere sådanne tilfælde er ekstremt vigtigt for, at scripts er tilstrækkeligt pålidelige til at blive kørt i produktionssystemer. Jeg bruger ofte exit-handlere til at reagere på sådanne scenarier:
function handle_exit() {
// Tilføj oprydningskode her
// for eksempel rm -f "/tmp/${lock_file}.lock"
// afslut med en passende statuskode
}
// trap
trap handle_exit 0 SIGHUP SIGINT SIGQUIT SIGABRT SIGTERM
trap — det er en indbygget shell-kommando, der hjælper dig med at registrere en oprydningsfunktion, som kaldes ved modtagelse af signaler. Men man skal være særlig forsigtig med sådanne handlere som SIGINT, der fremkalder en afbrydelse af scriptet.
Derudover bør man i de fleste tilfælde kun fange EXIT, men ideen er, at du faktisk kan tilpasse scriptets adfærd for hvert enkelt signal.
Indbyggede funktioner sæt — hurtig afslutning ved fejl
Det er meget vigtigt at reagere på fejl, så snart de opstår, og hurtigt stoppe udførelsen. Intet kan være værre end at fortsætte med at køre en kommando som denne:
rm -rf ${directory_name}/*
Bemærk, at variablen directory_name ikke er defineret.
For at håndtere sådanne scenarier er det vigtigt at bruge indbyggede funktioner set, såsom set -o errexit, set -o pipefail eller set -o nounset i begyndelsen af scriptet. Disse funktioner sikrer, at dit script afslutter, så snart det møder nogen ikke-nul exit-kode, brug af udefinerede variabler, forkert kommandoer sendt via pipen osv.:
#!/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
Bemærk: indbyggede funktioner, såsom set -o errexit, vil afslutte scriptet, så snart der opstår en "ubehandlet" exit-kode (undtagen nul). Derfor er det bedre at indføre brugerdefineret fejlhåndtering, såsom:
#!/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
Sådan skripting får dig til at være mere opmærksom på opførselen af alle kommandoer i scriptet og at tage højde for muligheden for fejl, før de overrasker dig.
ShellCheck til at finde fejl under udviklingen
Det er værd at integrere noget som i dine udviklings- og testpipelines for at kontrollere dit bash-kode for at anvende bedste praksis.
Jeg bruger det i mine lokale udviklingsmiljøer for at få rapporter om syntaks, semantik og nogle fejl i koden, som jeg måske har overset under udviklingen. Dette er et statisk analysemiddel til dine bash-scripts, og jeg anbefaler varmt at tage det i brug.
Brug af dine exit-koder
Exit-koder i POSIX er ikke bare nul eller en, men nul eller en ikke-nul værdi. Brug disse muligheder til at returnere brugerdefinerede fejlkoder (mellem 201-254) for forskellige fejlsituationer.
Disse oplysninger kan derefter bruges af andre scripts, der indpakker dit, for at forstå præcist, hvilken type fejl der opstod, og reagere passende:
#!/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
}
Bemærk: vær særligt opmærksom på de variabelnavne, du definerer, så du ikke utilsigtet overskriver miljøvariabler.
Logger-funktioner
En smuk og struktureret logføring er vigtig for nemt at forstå resultaterne af dit skripts udførelse. Ligesom i andre højniveau programmeringssprog bruger jeg altid mine egne loggingsfunktioner i mine bash-scripts, såsom __msg_info, __msg_error og så videre.
Dette hjælper med at sikre en standardiseret logstruktur ved kun at ændre et sted:
#!/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"
Jeg forsøger som regel at have en eller anden form for mekanisme i mine scripts __init, hvor sådanne logvariabler og andre systemvariabler initialiseres eller sættes til standardværdier. Disse variabler kan også sættes fra kommandolinjeargumenter under scriptets kald.
For eksempel noget som:
$ ./run-script.sh --debug
Når et sådant script køres, garanteres det, at de systemindstillinger er sat til standardværdier, hvis de er obligatoriske, eller i det mindste initialiseret med noget passende, hvis det er nødvendigt.
Jeg baserer normalt valget af, hvad der skal initialiseres, og hvad der ikke skal, på et kompromis mellem brugergrænsefladen og detaljerne i konfigurationer, som brugeren kan/skulle finde sig i.
Arkitektur til genbrug og ren systemtilstand
Modulær / genanvendelig kode
├── framework
│ ├── common
│ │ ├── loggers.sh
│ │ ├── mail_reports.sh
│ │ └── slack_reports.sh
│ └── daily_database_operation.sh
Jeg holder et separat repository, som kan bruges til at initialisere et nyt bash-projekt/script, som jeg ønsker at udvikle. Alt, hvad der kan genbruges, kan gemmes i repository'et og hentes i andre projekter, der ønsker at bruge sådanne funktionaliteter. Denne organisering af projekter reducerer betydeligt størrelsen på andre scripts samt sikrer, at kodebasen er lille og nem at teste.
Som i det ovenfor nævnte eksempel indeholder alle loggingsfunktioner, såsom __msg_info, __msg_error og andre, såsom rapporter til Slack, separat i common/* og tilsluttes dynamisk i andre scripts, som daily_database_operation.sh.
Lad efterlade et rent system
Hvis du indlæser nogle ressourcer under scriptets kørsel, anbefales det at gemme alle sådanne data i en fælles katalog med et tilfældigt navn, f.eks. /tmp/AlRhYbD97/*Du kan bruge tilfældige tekstgeneratorer til at vælge et biblioteknavn:
rand_dir_name="$(cat /dev/urandom | tr -dc 'a-zA-Z0-9' | fold -w 16 | head -n 1)"
Når arbejdet er afsluttet, kan oprydning af sådanne mapper sikres i de fangstbehandlere, der er diskuteret tidligere. Hvis man ikke tager sig af at fjerne de midlertidige mapper, vil de hobe sig op og på et tidspunkt forårsage uventede problemer på værten, som for eksempel en fyldt disk.
Brug af lock-filer
Det er ofte nødvendigt at sikre, at kun én instans af et script kører på værten ad gangen. Dette kan gøres ved hjælp af lock-filer.
Jeg opretter normalt lock-filer i /tmp/project_name/*.lock og tjekker for deres tilstedeværelse i starten af scriptet. Dette hjælper med at afslutte scriptet korrekt og undgå uventede ændringer af systemtilstanden fra et andet script, der kører parallelt. Lock-filer er ikke nødvendige, hvis du har brug for, at det samme script kører parallelt på denne vært.
Mål og forbedr
Vi skal ofte arbejde med scripts, der kører i længere perioder, såsom daglige databaseoperationer. Sådanne operationer inkluderer normalt en sekvens af trin: indlæsning af data, kontrol for anomalier, import af data, afsendelse af statusrapporter osv.
I sådanne tilfælde forsøger jeg altid at opdele scriptet i separate små scripts og rapportere deres status og køretid med:
time source "${filepath}" "${args}" >> "${LOG_DIR}/RUN_LOG" 2>&1
Senere kan jeg se køretidet ved hjælp af:
tac "${LOG_DIR}/RUN_LOG.txt" | grep -m1 "real"
Dette hjælper mig med at identificere problematiske/langsomme områder i scripts, der har brug for optimering.
Held og lykke!
Yderligere læsning:
Kilde: habr.com
