BÀsta metoder för bash-skript: en kort guide till pÄlitliga och effektiva bash-skript

BÀsta metoder för bash-skript: en kort guide till pÄlitliga och effektiva bash-skript
Shell-tapet av manapi

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 Mail.ru Cloud Solutions 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 ShellCheck 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:

  1. Go och GPU-cache.
  2. Exempel pÄ en event-driven applikation baserad pÄ webhooks i S3-objekter i Mail.ru Cloud Solutions.
  3. VÄr Telegram-kanal om digital transformation.

KĂ€lla: habr.com

Köp pĂ„litlig webbhotell med DDoS-skydd, VPS VDS-servrar đŸ”„ Köp pĂ„litlig webbhotell med DDoS-skydd, VPS VDS-servrar | ProHoster