Pràctiques recomanades de Bash Scripting: una guia ràpida per a scripts Bash fiables i de rendiment

Pràctiques recomanades de Bash Scripting: una guia ràpida per a scripts Bash fiables i de rendiment
Fons de pantalla de Shell de manapi

Depurar scripts bash és com buscar una agulla en un paller, especialment quan apareixen noves addicions a la base de codi existent sense tenir en compte oportunament els problemes d'estructura, registre i fiabilitat. Podeu trobar-vos en aquestes situacions, ja sigui a causa dels vostres propis errors o quan gestioneu piles complexes d'scripts.

Equip Mail.ru Solucions al núvol va traduir un article amb recomanacions que us ajudaran a escriure, depurar i mantenir millor els vostres scripts. Ho creieu o no, res millor que la satisfacció d'escriure codi bash net i llest per utilitzar que funcioni cada vegada.

En l'article, l'autor comparteix el que ha après durant els darrers anys, així com alguns errors comuns que l'han agafat desprevingut. Això és important perquè cada desenvolupador de programari, en algun moment de la seva carrera, treballa amb scripts per automatitzar les tasques de treball rutinàries.

Manipuladors de trampes

La majoria dels scripts bash que he trobat mai utilitzen un mecanisme de neteja eficaç quan passa alguna cosa inesperada durant l'execució de l'script.

Des de l'exterior poden sorgir sorpreses, com per exemple rebre un senyal del nucli. La gestió d'aquests casos és extremadament important per garantir que els scripts siguin prou fiables per executar-se en sistemes de producció. Sovint faig servir controladors de sortida per respondre a escenaris com aquest:

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 és una ordre integrada de l'intèrpret d'ordres que us ajuda a registrar una funció de neteja que s'anomena en cas de qualsevol senyal. Tanmateix, s'ha de tenir especial cura amb els manipuladors com ara SIGINT, que fa que l'script s'avorri.

A més, en la majoria dels casos només s'ha d'agafar EXIT, però la idea és que realment podeu personalitzar el comportament de l'script per a cada senyal individual.

Funcions de conjunt integrades: terminació ràpida en cas d'error

És molt important respondre als errors tan aviat com es produeixin i aturar l'execució ràpidament. Res pot ser pitjor que continuar executant una ordre com aquesta:

rm -rf ${directory_name}/*

Tingueu en compte que la variable directory_name no determinat.

És important utilitzar funcions integrades per gestionar aquests escenaris set, tal com set -o errexit, set -o pipefail o set -o nounset al principi del guió. Aquestes funcions asseguren que el vostre script sortirà tan bon punt es trobi amb un codi de sortida diferent de zero, l'ús de variables no definides, les ordres no vàlides passades per una canonada, etc.

#!/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: funcions integrades com ara set -o errexit, sortirà de l'script tan bon punt hi hagi un codi de retorn "brut" (que no sigui zero). Per tant, és millor introduir un tractament d'errors personalitzat, per exemple:

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

Escriure scripts d'aquesta manera obliga a tenir més cura amb el comportament de totes les ordres de l'script i preveure la possibilitat d'un error abans que et sorprengui.

ShellCheck per detectar errors durant el desenvolupament

Val la pena integrar alguna cosa així ShellCheck a les vostres canalitzacions de desenvolupament i proves per comprovar el vostre codi bash amb les millors pràctiques.

L'utilitzo als meus entorns de desenvolupament locals per obtenir informes sobre la sintaxi, la semàntica i alguns errors del codi que podria haver perdut durant el desenvolupament. Aquesta és una eina d'anàlisi estàtica per als vostres scripts bash i us recomano molt utilitzar-la.

Utilitzant els teus propis codis de sortida

Els codis de retorn a POSIX no són només zero o un, sinó zero o un valor diferent de zero. Utilitzeu aquestes funcions per retornar codis d'error personalitzats (entre 201 i 254) per a diversos casos d'error.

Aquesta informació la poden utilitzar altres scripts que embolcallen el vostre per entendre exactament quin tipus d'error s'ha produït i reaccionar en conseqüència:

#!/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 us plau, tingueu especial cura amb els noms de variables que definiu per evitar anul·lar accidentalment les variables d'entorn.

Funcions de registre

El registre bonic i estructurat és important per entendre fàcilment els resultats del vostre script. Com passa amb altres llenguatges de programació d'alt nivell, sempre faig servir funcions de registre natives als meus scripts bash, com ara __msg_info, __msg_error i així successivament.

Això ajuda a proporcionar una estructura de registre estandarditzada fent canvis en un sol lloc:

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

Normalment intento tenir algun tipus de mecanisme als meus guions __init, on aquestes variables de registre i altres variables del sistema s'inicialitzen o s'estableixen en valors predeterminats. Aquestes variables també es poden establir des d'opcions de línia d'ordres durant la invocació de l'script.

Per exemple, alguna cosa com:

$ ./run-script.sh --debug

Quan s'executa un script d'aquest tipus, assegura que la configuració de tot el sistema s'estableix amb els valors predeterminats si són necessaris, o almenys que s'inicialitzi amb alguna cosa adequada si cal.

Normalment base l'elecció de què inicialitzar i què no fer en un compromís entre la interfície d'usuari i els detalls de les configuracions en què l'usuari pot/ha d'aprofundir.

Arquitectura per a la reutilització i l'estat net del sistema

Codi modular/reutilitzable

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

Conservo un repositori separat que puc utilitzar per inicialitzar un nou projecte/script bash que vull desenvolupar. Qualsevol cosa que es pugui reutilitzar es pot emmagatzemar en un dipòsit i recuperar-la per altres projectes que vulguin utilitzar aquesta funcionalitat. Organitzar els projectes d'aquesta manera redueix significativament la mida d'altres scripts i també garanteix que la base de codi sigui petita i fàcil de provar.

Com a l'exemple anterior, totes les funcions de registre com ara __msg_info, __msg_error i d'altres, com els informes de Slack, s'inclouen per separat a common/* i connectar-se dinàmicament en altres escenaris com daily_database_operation.sh.

Deixa enrere un sistema net

Si carregueu recursos mentre s'executa l'script, es recomana emmagatzemar totes aquestes dades en un directori compartit amb un nom aleatori, p. /tmp/AlRhYbD97/*. Podeu utilitzar generadors de text aleatori per seleccionar el nom del directori:

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

Un cop finalitzat el treball, la neteja d'aquests directoris es pot proporcionar als gestors de ganxos comentats anteriorment. Si no es cuiden els directoris temporals, s'acumulen i en algun moment causen problemes inesperats a l'amfitrió, com ara un disc ple.

Ús de fitxers de bloqueig

Sovint cal assegurar-se que només s'executa una instància d'un script en un amfitrió en un moment donat. Això es pot fer mitjançant fitxers de bloqueig.

Acostumo a crear fitxers de bloqueig /tmp/project_name/*.lock i comproveu la seva presència al principi del guió. Això ajuda l'script a finalitzar amb gràcia i evitar canvis inesperats a l'estat del sistema per un altre script que s'executa en paral·lel. Els fitxers de bloqueig no són necessaris si necessiteu que el mateix script s'executi en paral·lel en un host determinat.

Mesura i millora

Sovint hem de treballar amb scripts que s'executen durant llargs períodes de temps, com ara operacions diàries de bases de dades. Aquestes operacions solen implicar una seqüència de passos: càrrega de dades, comprovació d'anomalies, importació de dades, enviament d'informes d'estat, etc.

En aquests casos, sempre intento dividir l'script en petits scripts separats i informar-ne l'estat i el temps d'execució mitjançant:

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

Més tard puc veure el temps d'execució amb:

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

Això m'ajuda a identificar les àrees de problemes/lentes en els scripts que necessiten optimització.

Bona sort!

Què més cal llegir:

  1. Go i memòria cau GPU.
  2. Un exemple d'una aplicació basada en esdeveniments basada en webhooks a l'emmagatzematge d'objectes S3 de Mail.ru Cloud Solutions.
  3. El nostre canal de telegram sobre transformació digital.

Font: www.habr.com

Afegeix comentari