Mellores prácticas de scripting de Bash: unha guía rápida para scripts de Bash fiables e de rendemento

Mellores prácticas de scripting de Bash: unha guía rápida para scripts de Bash fiables e de rendemento
Fondo de pantalla de Shell de manapi

Depurar scripts bash é como buscar unha agulla nun palleiro, especialmente cando aparecen novas incorporacións na base de código existente sen a consideración oportuna dos problemas de estrutura, rexistro e fiabilidade. Podes atoparte en tales situacións debido aos teus propios erros ou ao xestionar pilas complexas de scripts.

Equipo Solucións na nube Mail.ru traduciu un artigo con recomendacións que che axudarán a escribir, depurar e manter mellor os teus scripts. Créalo ou non, nada supera a satisfacción de escribir un código bash limpo e listo para usar que funciona cada vez.

No artigo, o autor comparte o que aprendeu durante os últimos anos, así como algúns erros comúns que o colleron desprevido. Isto é importante porque todos os desenvolvedores de software, nalgún momento da súa carreira, traballan con scripts para automatizar tarefas de traballo rutineiras.

Manexadores de trampas

A maioría dos scripts bash que atopei nunca usan un mecanismo de limpeza eficaz cando ocorre algo inesperado durante a execución do script.

Desde fóra poden xurdir sorpresas, como recibir un sinal do núcleo. O manexo destes casos é moi importante para garantir que os scripts sexan o suficientemente fiables como para executarse en sistemas de produción. Moitas veces uso controladores de saída para responder a escenarios como este:

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 é un comando integrado no intérprete de comandos que che axuda a rexistrar unha función de limpeza que se chama en caso de producirse algún sinal. Non obstante, débese ter especial coidado con manipuladores como SIGINT, o que fai que o script se aborte.

Ademais, na maioría dos casos só debes atrapar EXIT, pero a idea é que realmente pode personalizar o comportamento do script para cada sinal individual.

Funcións de conxunto integradas: terminación rápida por erro

É moi importante responder aos erros en canto se produzan e deter a execución rapidamente. Nada pode ser peor que seguir executando un comando coma este:

rm -rf ${directory_name}/*

Teña en conta que a variable directory_name non determinado.

É importante utilizar funcións integradas para xestionar tales escenarios setcomo set -o errexit, set -o pipefail ou set -o nounset ao comezo do guión. Estas funcións garanten que o seu script sairá en canto atope algún código de saída distinto de cero, uso de variables indefinidas, comandos non válidos pasados ​​por un tubo, 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: funcións integradas como set -o errexit, sairá do script en canto haxa un código de retorno "en bruto" (distinto de cero). Polo tanto, é mellor introducir un tratamento personalizado de erros, por exemplo:

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

Escribir scripts deste xeito obrígache a ter máis coidado co comportamento de todos os comandos do script e prever a posibilidade de que se produza un erro antes de que te tome por sorpresa.

ShellCheck para detectar erros durante o desenvolvemento

Paga a pena integrar algo así ShellCheck nas túas canalizacións de desenvolvemento e probas para comprobar o teu código bash coas mellores prácticas.

Eu úsoo nos meus contornos de desenvolvemento local para obter informes sobre sintaxe, semántica e algúns erros no código que puiden perder durante o desenvolvemento. Esta é unha ferramenta de análise estática para os teus scripts bash e recomendo encarecidamente usala.

Usando os teus propios códigos de saída

Os códigos de retorno en POSIX non son só cero ou un, senón cero ou un valor distinto de cero. Use estas funcións para devolver códigos de erro personalizados (entre 201 e 254) para varios casos de erro.

Esta información pode ser usada por outros scripts que envolven o teu para comprender exactamente que tipo de erro ocorreu e reaccionar en consecuencia:

#!/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: por favor teña especial coidado cos nomes das variables que defina para evitar que se anulen accidentalmente as variables de ambiente.

Funcións de rexistro

O rexistro bonito e estruturado é importante para comprender facilmente os resultados do seu script. Como ocorre con outras linguaxes de programación de alto nivel, sempre uso funcións de rexistro nativas nos meus scripts bash, como __msg_info, __msg_error e así por diante.

Isto axuda a proporcionar unha estrutura de rexistro estandarizada facendo cambios nun só lugar:

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

Normalmente intento ter algún tipo de mecanismo nos meus guións __init, onde tales variables do rexistrador e outras variables do sistema se inicializan ou configuran os valores predeterminados. Estas variables tamén se poden establecer desde as opcións da liña de comandos durante a invocación do script.

Por exemplo, algo así como:

$ ./run-script.sh --debug

Cando se executa un script deste tipo, garante que a configuración de todo o sistema estea configurada con valores predeterminados se son necesarios, ou polo menos que se inicialicen en algo apropiado se é necesario.

Normalmente baseo a elección de que inicializar e que non facer nunha compensación entre a interface de usuario e os detalles das configuracións nas que o usuario pode/debe afondar.

Arquitectura para a reutilización e o estado limpo do sistema

Código modular/reutilizable

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

Conservo un repositorio separado que podo usar para inicializar un novo proxecto/script bash que quero desenvolver. Calquera cousa que se poida reutilizar pode ser almacenada nun repositorio e recuperada por outros proxectos que queiran utilizar esa funcionalidade. Organizar proxectos deste xeito reduce significativamente o tamaño doutros scripts e tamén garante que a base de código sexa pequena e fácil de probar.

Como no exemplo anterior, todas as funcións de rexistro como __msg_info, __msg_error e outros, como os informes de Slack, están contidos por separado en common/* e conectarse dinámicamente noutros escenarios como daily_database_operation.sh.

Deixa atrás un sistema limpo

Se está a cargar algún recurso mentres se está a executar o script, recoméndase almacenar todos eses datos nun directorio compartido cun nome aleatorio, p. /tmp/AlRhYbD97/*. Podes usar xeradores de texto aleatorios para seleccionar o nome do directorio:

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

Despois de completar o traballo, pódese proporcionar a limpeza destes directorios nos manejadores de ganchos comentados anteriormente. Se non se coidan os directorios temporais, acumúlanse e nalgún momento provocan problemas inesperados no host, como un disco cheo.

Usando ficheiros de bloqueo

Moitas veces cómpre asegurarse de que só se está a executar unha instancia dun script nun servidor en cada momento. Isto pódese facer usando ficheiros de bloqueo.

Normalmente creo ficheiros de bloqueo /tmp/project_name/*.lock e comprobar a súa presenza ao comezo do guión. Isto axuda a que o script remate con gracia e evite cambios inesperados no estado do sistema por outro script que se execute en paralelo. Os ficheiros de bloqueo non son necesarios se precisa que o mesmo script se execute en paralelo nun host determinado.

Mide e mellora

Moitas veces necesitamos traballar con scripts que se executan durante longos períodos de tempo, como operacións diarias de bases de datos. Este tipo de operacións implica normalmente unha secuencia de pasos: carga de datos, comprobación de anomalías, importación de datos, envío de informes de estado, etc.

Nestes casos, sempre intento dividir o script en pequenos scripts separados e informar do seu estado e tempo de execución usando:

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

Máis tarde podo ver o tempo de execución con:

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

Isto axúdame a identificar áreas problemáticas/lentas nos scripts que precisan optimización.

Boa sorte!

Que máis ler:

  1. Go e cachés GPU.
  2. Un exemplo dunha aplicación dirixida por eventos baseada en webhooks no almacenamento de obxectos S3 de Mail.ru Cloud Solutions.
  3. A nosa canle de telegram sobre transformación dixital.

Fonte: www.habr.com

Engadir un comentario