Le migliori pratiche per gli script bash: guida rapida per script bash affidabili e performanti

Le migliori pratiche per gli script bash: guida rapida per script bash affidabili e performanti
Sfondo shell di manapi

Il debugging degli script bash è come cercare un ago in un pagliaio, soprattutto quando nuove aggiunte compaiono in un codice esistente senza un'appropriata revisione delle questioni di struttura, logging e affidabilità. In queste situazioni, si può incorrere in errori sia propri che nella gestione di complesse aderenze di script.

Team Mail.ru Cloud Solutions Ho tradotto un articolo con consigli che ti aiuteranno a scrivere, fare il debug e mantenere meglio i tuoi script. Vuoi crederci o no, ma nulla può eguagliare la soddisfazione di scrivere codice bash pulito e pronto all'uso che funziona ogni volta.

Nell'articolo, l'autore condivide ciò che ha appreso negli ultimi anni, insieme ad alcuni errori comuni che lo hanno colto di sorpresa. Questo è importante, perchÊ ogni sviluppatore di software, a un certo punto della propria carriera, lavora con script per automatizzare attività lavorative routinarie.

Gestori di eccezione

La maggior parte degli script bash che ho incontrato non ha mai utilizzato un meccanismo di pulizia efficiente quando qualcosa di imprevisto accade durante l'esecuzione dello script.

Gli imprevisti possono sorgere dall'esterno, ad esempio ricevendo un segnale dal kernel. Gestire tali situazioni è estremamente importante per rendere gli script abbastanza affidabili da essere eseguiti in sistemi di produzione. Spesso utilizzo gestori di uscita per rispondere a tali scenari:

function handle_exit() {
  	// Aggiungi qui il codice di pulizia
  	// ad esempio: rm -f "/tmp/${lock_file}.lock"
  	// esci con un codice di stato appropriato
}
  
// trap  
trap handle_exit 0 SIGHUP SIGINT SIGQUIT SIGABRT SIGTERM

trap è un comando incorporato della shell che ti aiuta a registrare una funzione di pulizia che viene chiamata in caso di segnali. Tuttavia, è necessario prestare particolare attenzione a gestori come SIGINT, che interrompe l'esecuzione dello script.

Inoltre, nella maggior parte dei casi, dovresti intercettare solo EXIT, ma l'idea è che puoi davvero personalizzare il comportamento dello script per ogni singolo segnale.

Le funzioni incorporate set — esci rapidamente in caso di errore

È fondamentale reagire agli errori non appena si verificano e fermare rapidamente l'esecuzione. Non c'è niente di peggio che continuare a eseguire un comando come questo:

rm -rf ${directory_name}/*

Si prega di notare che la variabile directory_name non è definita.

Per gestire tali scenari è importante utilizzare funzioni incorporate set, come set -o errexit, set -o pipefail o set -o nounset all'inizio dello script. Queste funzioni garantiscono che il tuo script termini non appena incontra un codice di uscita diverso da zero, utilizza variabili non definite, comandi errati passati attraverso una pipe e cosÏ via:

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

Nota: funzioni incorporate, come set -o errexit, usciranno dallo script non appena si verifica un codice di ritorno "non gestito" (escluso zero). Pertanto, è meglio introdurre una gestione degli errori personalizzata, come:

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

Uno script scritto in questo modo ti costringe a prestare particolare attenzione al comportamento di tutti i comandi nello script e a prevedere la possibilitĂ  di un errore prima che ti colga di sorpresa.

ShellCheck per rilevare errori durante lo sviluppo

Vale la pena integrare qualcosa come ShellCheck nei vostri flussi di sviluppo e test, per verificare il vostro codice bash in merito all'applicazione delle migliori pratiche.

Lo uso nei miei ambienti di sviluppo locali per ottenere report sulla sintassi, semantica e alcuni errori di codice che potrei aver trascurato durante lo sviluppo. È uno strumento di analisi statica per i vostri script bash e lo consiglio vivamente.

Uso dei codici di uscita

I codici di ritorno in POSIX non sono solo zero o uno, ma zero o un valore diverso da zero. Utilizzate queste opzioni per restituire codici di errore personalizzati (tra 201-254) per diversi aspetti di errore.

Queste informazioni possono essere utilizzate da altri script che avvolgono il vostro, per comprendere esattamente quale tipo di errore è avvenuto e reagire di conseguenza:

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

Nota: si prega di prestare particolare attenzione ai nomi delle variabili che definite per evitare di sovrascrivere accidentalmente le variabili di ambiente.

Funzioni di logging

Una gestione dei log ben strutturata è cruciale per comprendere facilmente i risultati dell'esecuzione del tuo script. Come in altri linguaggi di programmazione di alto livello, uso sempre nelle mie script bash funzioni di logging personalizzate, come __msg_info, __msg_error e cosÏ via.

Questo aiuta a garantire una struttura di logging standardizzata, apportando modifiche solo in un unico punto:

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

Di solito cerco di avere nei miei script qualche meccanismo __init, dove queste variabili di logging e altre variabili di sistema vengono inizializzate o impostate su valori predefiniti. Queste variabili possono anche essere impostate tramite parametri della riga di comando durante la chiamata dello script.

Ad esempio, qualcosa del tipo:

$ ./run-script.sh --debug

Quando uno script del genere viene eseguito, è garantito che le impostazioni globali siano impostate sui valori predefiniti, se sono obbligatorie, o almeno inizializzate con qualcosa di appropriato, se necessario.

Di solito baso la mia scelta su cosa inizializzare e cosa no su un compromesso tra l'interfaccia utente e i dettagli delle configurazioni in cui l'utente può/deve immergersi.

Architettura per il riutilizzo e stato pulito del sistema

Codice modulare / riutilizzabile

├── framework
│   ├── common
│   │   ├── loggers.sh
│   │   ├── mail_reports.sh
│   │   └── slack_reports.sh
│   └── daily_database_operation.sh

Tengo un repository separato che può essere utilizzato per inizializzare un nuovo progetto/script bash che voglio sviluppare. Qualsiasi cosa riutilizzabile può essere salvata nel repository e utilizzata in altri progetti che desiderano avere tali funzionalità. Questa organizzazione dei progetti riduce significativamente la dimensione di altri script e garantisce che la base di codice sia piccola e facilmente testabile.

Come nell'esempio sopra, tutte le funzioni di registrazione, come __msg_info, __msg_error e altre, come i report su Slack, sono contenute separatamente in common/* e vengono collegate dinamicamente in altri script, come daily_database_operation.sh.

Lascia un sistema pulito

Se stai caricando delle risorse durante l'esecuzione dello script, è consigliabile conservare tutti questi dati in una cartella comune con un nome casuale, ad esempio /tmp/AlRhYbD97/*. Puoi utilizzare generatori di testo casuale per selezionare il nome della directory:

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

Dopo il completamento, la pulizia di tali directory può essere gestita nei gestori delle eccezioni, come discusso in precedenza. Se non ci si occupa della rimozione delle directory temporanee, esse si accumulano e a un certo punto provocano problemi imprevisti sull'host, come l'esaurimento dello spazio su disco.

Utilizzo di file di lock

Spesso è necessario garantire l'esecuzione di un solo esempio di script sull'host in qualsiasi momento. Questo può essere fatto utilizzando file di lock.

Di solito creo file di lock in /tmp/project_name/*.lock e ne verifico l'esistenza all'inizio dello script. Questo aiuta a terminare correttamente lo script ed evita cambiamenti di stato imprevisti da parte di un altro script che esegue parallelamente. I file di lock non sono necessari se devi eseguire lo stesso script in parallelo su questo host.

Misurare e migliorare

Spesso ci troviamo a lavorare con scenari che vengono eseguiti per un lungo periodo di tempo, come le operazioni giornaliere sui database. Queste operazioni di solito includono una sequenza di passaggi: caricamento dei dati, verifica di anomalie, importazione dei dati, invio di report di stato e cosĂŹ via.

In questi casi, cerco sempre di suddividere lo scenario in piccoli script distinti e di comunicare il loro stato e il tempo di esecuzione attraverso:

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

In seguito, posso controllare il tempo di esecuzione con:

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

Questo mi aiuta a identificare le aree problematiche/lente negli script che necessitano di ottimizzazione.

Buona fortuna!

Cosa altro leggere:

  1. Go e cache GPU.
  2. Esempio di applicazione event-driven basata su webhook in oggetti di storage S3 di Mail.ru Cloud Solutions.
  3. Il nostro canale Telegram sulla trasformazione digitale.

Fonte: habr.com

Acquista hosting affidabile per siti web con protezione DDoS, VPS VDS server 🔥 Acquista hosting affidabile per siti web con protezione DDoS, VPS VDS server | ProHoster