Práticas recomendadas de script Bash: um guia rápido para scripts Bash confiáveis ​​e de desempenho

Práticas recomendadas de script Bash: um guia rápido para scripts Bash confiáveis ​​e de desempenho
Papel de parede de concha por manapi

Depurar scripts bash é como procurar uma agulha em um palheiro, especialmente quando novas adições aparecem na base de código existente sem consideração oportuna de questões de estrutura, registro e confiabilidade. Você pode se encontrar nessas situações devido a seus próprios erros ou ao gerenciar pilhas complexas de scripts.

Equipe Soluções em nuvem Mail.ru traduzi um artigo com recomendações que ajudarão você a escrever, depurar e manter melhor seus scripts. Acredite ou não, nada supera a satisfação de escrever um código bash limpo e pronto para uso que funciona sempre.

No artigo, o autor compartilha o que aprendeu nos últimos anos, bem como alguns erros comuns que o pegaram desprevenido. Isso é importante porque todo desenvolvedor de software, em algum momento de sua carreira, trabalha com scripts para automatizar tarefas rotineiras de trabalho.

Manipuladores de armadilhas

A maioria dos scripts bash que encontrei nunca usa um mecanismo de limpeza eficaz quando algo inesperado acontece durante a execução do script.

Surpresas podem surgir de fora, como receber um sinal do núcleo. Lidar com esses casos é extremamente importante para garantir que os scripts sejam confiáveis ​​o suficiente para serem executados em sistemas de produção. Costumo usar manipuladores de saída para responder a cenários 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 é um comando interno do shell que ajuda a registrar uma função de limpeza que é chamada em caso de qualquer sinal. No entanto, cuidados especiais devem ser tomados com manipuladores como SIGINT, o que faz com que o script seja abortado.

Além disso, na maioria dos casos você só deve capturar EXIT, mas a ideia é que você possa personalizar o comportamento do script para cada sinal individual.

Funções de conjunto integradas - encerramento rápido em caso de erro

É muito importante responder aos erros assim que eles ocorrerem e interromper a execução rapidamente. Nada poderia ser pior do que continuar executando um comando como este:

rm -rf ${directory_name}/*

Observe que a variável directory_name não determinado.

É importante usar funções integradas para lidar com tais cenários set, Tal como set -o errexit, set -o pipefail ou set -o nounset no início do roteiro. Essas funções garantem que seu script será encerrado assim que encontrar qualquer código de saída diferente de zero, uso de variáveis ​​indefinidas, comandos inválidos passados ​​por um canal e assim por diante:

#!/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: funções integradas, como set -o errexit, sairá do script assim que houver um código de retorno "bruto" (diferente de zero). Portanto, é melhor introduzir o tratamento de erros personalizado, 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

Escrever scripts dessa forma obriga você a ter mais cuidado com o comportamento de todos os comandos do script e a antecipar a possibilidade de um erro antes que ele o pegue de surpresa.

ShellCheck para detectar erros durante o desenvolvimento

Vale a pena integrar algo como Verificação do casco em seus pipelines de desenvolvimento e teste para verificar seu código bash em relação às práticas recomendadas.

Eu o uso em meus ambientes de desenvolvimento local para obter relatórios sobre sintaxe, semântica e alguns erros no código que posso ter perdido durante o desenvolvimento. Esta é uma ferramenta de análise estática para seus scripts bash e eu recomendo fortemente usá-la.

Usando seus próprios códigos de saída

Os códigos de retorno em POSIX não são apenas zero ou um, mas zero ou um valor diferente de zero. Use esses recursos para retornar códigos de erro personalizados (entre 201 e 254) para vários casos de erro.

Essas informações podem então ser usadas por outros scripts que envolvem o seu para entender exatamente que tipo de erro ocorreu e reagir de acordo:

#!/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: tenha especial cuidado com os nomes das variáveis ​​que você define para evitar substituir acidentalmente as variáveis ​​de ambiente.

Funções de registro

O registro bonito e estruturado é importante para compreender facilmente os resultados do seu script. Tal como acontece com outras linguagens de programação de alto nível, sempre uso funções de registro nativas em meus scripts bash, como __msg_info, __msg_error e assim por diante.

Isso ajuda a fornecer uma estrutura de registro padronizada, fazendo alterações em apenas um local:

#!/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 tento ter algum tipo de mecanismo em meus scripts __init, onde tais variáveis ​​do criador de logs e outras variáveis ​​do sistema são inicializadas ou configuradas com valores padrão. Essas variáveis ​​também podem ser definidas nas opções da linha de comando durante a chamada do script.

Por exemplo, algo como:

$ ./run-script.sh --debug

Quando esse script é executado, ele garante que as configurações de todo o sistema sejam definidas com os valores padrão, se necessário, ou pelo menos inicializadas com algo apropriado, se necessário.

Normalmente baseio a escolha do que inicializar e do que não fazer em uma compensação entre a interface do usuário e os detalhes das configurações que o usuário pode/deve se aprofundar.

Arquitetura para reutilização e estado limpo do sistema

Código modular/reutilizável

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

Eu mantenho um repositório separado que posso usar para inicializar um novo projeto/script bash que desejo desenvolver. Qualquer coisa que possa ser reutilizada pode ser armazenada em um repositório e recuperada por outros projetos que queiram utilizar essa funcionalidade. Organizar projetos dessa forma reduz significativamente o tamanho de outros scripts e também garante que a base de código seja pequena e fácil de testar.

Como no exemplo acima, todas as funções de registro, como __msg_info, __msg_error e outros, como relatórios do Slack, estão contidos separadamente em common/* e conectar-se dinamicamente em outros cenários como daily_database_operation.sh.

Deixe para trás um sistema limpo

Se você estiver carregando algum recurso enquanto o script estiver em execução, é recomendável armazenar todos esses dados em um diretório compartilhado com um nome aleatório, por exemplo. /tmp/AlRhYbD97/*. Você pode usar geradores de texto aleatórios para selecionar o nome do diretório:

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

Após a conclusão do trabalho, a limpeza de tais diretórios pode ser fornecida nos manipuladores de ganchos discutidos acima. Se os diretórios temporários não forem cuidados, eles se acumulam e, em algum momento, causam problemas inesperados no host, como um disco cheio.

Usando arquivos de bloqueio

Freqüentemente, você precisa garantir que apenas uma instância de um script esteja sendo executada em um host por vez. Isso pode ser feito usando arquivos de bloqueio.

Eu costumo criar arquivos de bloqueio em /tmp/project_name/*.lock e verifique a presença deles no início do roteiro. Isso ajuda o script a terminar normalmente e a evitar alterações inesperadas no estado do sistema por outro script em execução em paralelo. Os arquivos de bloqueio não serão necessários se você precisar que o mesmo script seja executado em paralelo em um determinado host.

Medir e melhorar

Muitas vezes precisamos trabalhar com scripts que são executados durante longos períodos de tempo, como operações diárias de banco de dados. Tais operações normalmente envolvem uma sequência de etapas: carregamento de dados, verificação de anomalias, importação de dados, envio de relatórios de status e assim por diante.

Nesses casos, sempre tento dividir o script em pequenos scripts separados e relatar seu status e tempo de execução usando:

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

Mais tarde posso ver o tempo de execução com:

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

Isso me ajuda a identificar áreas problemáticas/lentas em scripts que precisam de otimização.

Boa sorte!

O que mais ler:

  1. Go e caches de GPU.
  2. Um exemplo de aplicativo orientado a eventos baseado em webhooks no armazenamento de objetos S3 da Mail.ru Cloud Solutions.
  3. Nosso canal de telegram sobre transformação digital.

Fonte: habr.com

Adicionar um comentário