Bash Scripting Best Practices: En hurtig guide til pålidelige og ydeevne Bash Scripts

Bash Scripting Best Practices: En hurtig guide til pålidelige og ydeevne Bash Scripts
Skal tapet fra manapi

Debugging af 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 rettidig overvejelse af spørgsmål om struktur, logning og pålidelighed. Du kan finde dig selv i sådanne situationer enten på grund af dine egne fejl, eller når du håndterer komplekse bunker af scripts.

Team Mail.ru Cloud-løsninger oversat en artikel med anbefalinger, der vil hjælpe dig med at skrive, fejlrette og vedligeholde dine scripts bedre. Tro det eller ej, intet slår tilfredsstillelsen ved at skrive ren, klar til brug bash-kode, der virker hver gang.

I artiklen deler forfatteren, hvad han har lært i løbet af de sidste par år, samt nogle almindelige fejl, der har fanget ham. Dette er vigtigt, fordi enhver softwareudvikler på et tidspunkt i deres karriere arbejder med scripts for at automatisere rutinemæssige arbejdsopgaver.

Fældebehandlere

De fleste bash-scripts, jeg har stødt på, bruger aldrig en effektiv oprydningsmekanisme, når der sker noget uventet under scriptudførelse.

Overraskelser kan opstå udefra, såsom at modtage et signal fra kernen. Håndtering af sådanne sager er ekstremt vigtigt for at sikre, at scripts er pålidelige nok til at køre på produktionssystemer. Jeg bruger ofte exit-handlere til at reagere på scenarier som dette:

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

trap er en indbygget shell-kommando, der hjælper dig med at registrere en oprydningsfunktion, der kaldes i tilfælde af signaler. Der skal dog udvises særlig forsigtighed med handlere som f.eks SIGINT, hvilket får scriptet til at afbryde.

Derudover skal du i de fleste tilfælde kun fange EXIT, men tanken er, at du faktisk kan tilpasse scriptets adfærd for hvert enkelt signal.

Indbyggede sætfunktioner - hurtig afslutning ved fejl

Det er meget vigtigt at reagere på fejl, så snart de opstår, og stoppe eksekveringen hurtigt. Intet kunne være værre end at fortsætte med at køre en kommando som denne:

rm -rf ${directory_name}/*

Bemærk venligst, at variablen directory_name ikke bestemt.

Det er vigtigt at bruge indbyggede funktioner til at håndtere sådanne scenarier set, Såsom set -o errexit, set -o pipefail eller set -o nounset i begyndelsen af ​​manuskriptet. Disse funktioner sikrer, at dit script afsluttes, så snart det støder på en exitkode, der ikke er nul, brug af udefinerede variabler, ugyldige kommandoer, der sendes over et rør, og så videre:

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

Note: indbyggede funktioner som f.eks set -o errexit, afslutter scriptet, så snart der er en "rå" returkode (andre end nul). Derfor er det bedre at indføre tilpasset fejlhåndtering, for eksempel:

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

At skrive scripts på denne måde tvinger dig til at være mere forsigtig med adfærden af ​​alle kommandoerne i scriptet og forudse muligheden for en fejl, før den overrasker dig.

ShellCheck for at opdage fejl under udvikling

Det er værd at integrere sådan noget Skalcheck ind i dine udviklings- og testpipelines for at kontrollere din bash-kode i forhold til bedste praksis.

Jeg bruger det i mine lokale udviklingsmiljøer til at få rapporter om syntaks, semantik og nogle fejl i koden, som jeg måske er gået glip af under udviklingen. Dette er et statisk analyseværktøj til dine bash-scripts, og jeg anbefaler stærkt at bruge det.

Brug dine egne udgangskoder

Returkoder i POSIX er ikke kun nul eller én, men nul eller en værdi, der ikke er nul. Brug disse funktioner til at returnere brugerdefinerede fejlkoder (mellem 201-254) for forskellige fejltilfælde.

Disse oplysninger kan derefter bruges af andre scripts, der omslutter dine til at forstå præcis, hvilken type fejl der opstod, og reagere i overensstemmelse hermed:

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

Note: vær særlig forsigtig med de variabelnavne, du definerer, for at undgå utilsigtet tilsidesættelse af miljøvariabler.

Logningsfunktioner

Smuk og struktureret logning er vigtig for nemt at forstå resultaterne af dit script. Som med andre programmeringssprog på højt niveau, bruger jeg altid native log-funktioner i mine bash-scripts, som f.eks. __msg_info, __msg_error og så videre.

Dette hjælper med at give en standardiseret logningsstruktur ved kun at foretage ændringer ét 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 prøver normalt at have en eller anden form for mekanisme i mine scripts __init, hvor sådanne loggervariabler og andre systemvariable initialiseres eller indstilles til standardværdier. Disse variabler kan også indstilles fra kommandolinjeindstillinger under scriptankaldelse.

For eksempel noget som:

$ ./run-script.sh --debug

Når et sådant script udføres, sikrer det, at indstillinger for hele systemet er sat til standardværdier, hvis de er nødvendige, eller i det mindste initialiseret til noget passende, hvis det er nødvendigt.

Jeg plejer at basere valget af, hvad der skal initialiseres, og hvad der ikke skal gøres, på en afvejning mellem brugergrænsefladen og detaljerne i de konfigurationer, som brugeren kan/bør dykke ned 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 har et separat lager, som jeg kan bruge til at initialisere et nyt projekt/bash-script, som jeg vil udvikle. Alt, der kan genbruges, kan gemmes i et lager og hentes af andre projekter, der ønsker at bruge denne funktionalitet. At organisere projekter på denne måde reducerer størrelsen af ​​andre scripts betydeligt og sikrer også, at kodebasen er lille og nem at teste.

Som i eksemplet ovenfor er alle logningsfunktioner som f.eks __msg_info, __msg_error og andre, såsom Slack-rapporter, er indeholdt separat i common/* og tilslut dynamisk i andre scenarier som f.eks daily_database_operation.sh.

Efterlad et rent system

Hvis du indlæser nogen ressourcer, mens scriptet kører, anbefales det at gemme alle sådanne data i en delt mappe med et tilfældigt navn, f.eks. /tmp/AlRhYbD97/*. Du kan bruge tilfældige tekstgeneratorer til at vælge mappenavnet:

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

Efter afslutning af arbejdet kan oprydning af sådanne mapper tilvejebringes i de ovenfor omtalte krogbehandlere. Hvis der ikke tages hånd om midlertidige mapper, akkumuleres de og forårsager på et tidspunkt uventede problemer på værten, såsom en fuld disk.

Brug af låsefiler

Ofte skal du sikre dig, at kun én forekomst af et script kører på en vært på et givet tidspunkt. Dette kan gøres ved hjælp af låsefiler.

Jeg plejer at oprette låsefiler /tmp/project_name/*.lock og tjek deres tilstedeværelse i begyndelsen af ​​scriptet. Dette hjælper scriptet med at afslutte elegant og undgå uventede ændringer i systemtilstanden af ​​et andet script, der kører parallelt. Låsefiler er ikke nødvendige, hvis du har brug for, at det samme script skal køres parallelt på en given vært.

Mål og forbedre

Vi har ofte brug for at arbejde med scripts, der kører over lange perioder, såsom daglige databaseoperationer. Sådanne operationer involverer typisk en række trin: indlæsning af data, kontrol for uregelmæssigheder, import af data, afsendelse af statusrapporter og så videre.

I sådanne tilfælde forsøger jeg altid at opdele scriptet i separate små scripts og rapportere deres status og eksekveringstid ved hjælp af:

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

Senere kan jeg se udførelsestiden med:

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

Dette hjælper mig med at identificere problem/langsomme områder i scripts, der har brug for optimering.

Held og lykke!

Hvad skal man ellers læse:

  1. Go og GPU-cache.
  2. Et eksempel på en begivenhedsdrevet applikation baseret på webhooks i S3-objektlageret i Mail.ru Cloud Solutions.
  3. Vores telegramkanal om digital transformation.

Kilde: www.habr.com

Tilføj en kommentar