Pesquisa de site

Corrija bugs em scripts Bash imprimindo um rastreamento de pilha


Imprimir automaticamente um rastreamento de pilha em erros não tratados em seus scripts pode tornar muito mais fácil encontrar e corrigir bugs em seu código.

Ninguém quer escrever código ruim, mas inevitavelmente serão criados bugs. A maioria das linguagens modernas, como Java, JavaScript, Python, etc., imprimem automaticamente um rastreamento de pilha quando encontram uma exceção não tratada, mas não scripts de shell. Seria muito mais fácil encontrar e corrigir bugs em scripts de shell se você pudesse imprimir um rastreamento de pilha e, com um pouco de trabalho, você pode.

Os scripts de shell podem abranger vários arquivos e o código bem escrito é subdividido em funções. Rastrear problemas quando algo dá errado em um script de shell pode ser difícil quando esses scripts ficam grandes o suficiente. Um rastreamento de pilha que percorre o código de trás para frente, desde o erro até o início, pode mostrar onde seu código falhou e fornecer uma melhor compreensão do motivo, para que você possa corrigi-lo adequadamente.

Para implementar o rastreamento de pilha, uso o trap da seguinte maneira no início do meu script:

set -E

trap 'ERRO_LINENO=$LINENO' ERR
trap '_failure' EXIT

Este exemplo realiza algumas coisas, mas abordarei a segunda, trap 'ERRO_LINENO=$LINENO' ERR, primeiro. Esta linha garante que o script capture todos os comandos que saem com um código de saída diferente de zero (ou seja, um erro) e salva o número da linha do comando no arquivo onde o erro foi sinalizado. Isso não é capturado na saída.

A primeira linha acima (set -E) garante que a armadilha de erro seja herdada em todo o script. Sem isso, sempre que você entrar em um bloco if ou until, por exemplo, você perderá o controle do número da linha correto.

A segunda armadilha captura o sinal de saída do script e o envia para a função _failure, que definirei em instantes. Mas por que na saída e não no erro se você está tentando depurar o script? Em scripts bash, as falhas de comando são frequentemente usadas na lógica de controle ou podem ser ignoradas completamente como sem importância por design. Por exemplo, digamos que no início do seu script você queira ver se um determinado programa já está instalado antes de perguntar ao usuário se ele gostaria que você o instalasse:

if [[ ! -z $(command -v some_command) ]]
then
   # CAPTURE LOCATION OF some_command
   SOME_COMMAND_EXEC=$(which some_command)
else
   # echo $? would give us a non-zero value here; i.e. an error code
   # IGNORE ERR: ASK USER IF THEY WANT TO INSTALL some_command
fi

Se você parasse de processar cada erro e some_command não estivesse instalado, isso encerraria prematuramente o script, o que obviamente não é o que você deseja fazer aqui, portanto, em geral, você deseja apenas registrar um erro e rastrear a pilha quando o script saiu involuntariamente devido a um erro.

Para forçar a saída do seu script sempre que houver um erro inesperado, use a opção set -e:

set -e
# SCRIPT WILL EXIT IF ANY COMMAND RETURNS A NON-ZERO CODE
# WHILE set -e IS IN FORCE
set +e
# COMMANDS WITH ERRORS WILL NOT CAUSE THE SCRIPT TO EXIT HERE

A próxima pergunta é: quais são alguns exemplos em que você provavelmente gostaria que seu script fosse encerrado e destacasse uma falha? Exemplos comuns incluem o seguinte:

  1. Um sistema remoto inacessível
  2. Falha na autenticação em um sistema remoto
  3. Erros de sintaxe em arquivos de configuração ou script originados
  4. Compilações de imagens Docker
  5. Erros do compilador

Examinar muitas páginas de logs após a conclusão de um script em busca de possíveis erros que possam ser difíceis de detectar pode ser extremamente frustrante. É ainda mais frustrante quando você descobre que algo está errado muito depois de executar o script e agora precisa vasculhar vários conjuntos de registros para descobrir o que pode ter dado errado e onde. O pior é quando o erro já existe há algum tempo e você só o descobre no pior momento possível. Em qualquer caso, identificar o problema o mais rápido possível e corrigi-lo é sempre a prioridade.

Veja o exemplo de código de rastreamento de pilha (disponível para download aqui):

# Sample code for generating a stack trace on catastrophic failure

set -E

trap 'ERRO_LINENO=$LINENO' ERR
trap '_failure' EXIT

_failure() {
  ERR_CODE=$? # capture last command exit code
  set +xv # turns off debug logging, just in case
  if [[  $- =~ e && ${ERR_CODE} != 0 ]]
  then
      # only log stack trace if requested (set -e)
      # and last command failed
      echo
      echo "========= CATASTROPHIC COMMAND FAIL ========="
      echo
      echo "SCRIPT EXITED ON ERROR CODE: ${ERR_CODE}"
      echo
      LEN=${#BASH_LINENO[@]}
      for (( INDEX=0; INDEX<$LEN-1; INDEX++ ))
      do
          echo '---'
          echo "FILE: $(basename ${BASH_SOURCE[${INDEX}+1]})"
          echo "  FUNCTION: ${FUNCNAME[${INDEX}+1]}"
          if [[ ${INDEX} > 0 ]]
          then
           # commands in stack trace
              echo "  COMMAND: ${FUNCNAME[${INDEX}]}"
              echo "  LINE: ${BASH_LINENO[${INDEX}]}"
          else
              # command that failed
              echo "  COMMAND: ${BASH_COMMAND}"
              echo "  LINE: ${ERRO_LINENO}"
          fi
      done
      echo
      echo "======= END CATASTROPHIC COMMAND FAIL ======="
      echo
  fi
}

# set working directory to this directory for duration of this test
cd "$(dirname ${0})"

echo 'Beginning stacktrace test'

set -e
source ./testfile1.sh
source ./testfile2.sh
set +e

_file1_function1

No stacktrace.sh acima, a primeira coisa que a função _failure faz é capturar o código de saída do último comando usando o valor interno do shell $?. Em seguida, ele verifica se a saída foi inesperada, verificando a saída de $-, um valor interno do shell que contém as configurações atuais do shell bash, para ver se set -e está em vigor. Se o script foi encerrado devido a um erro e o erro foi inesperado, o rastreamento de pilha será enviado para o console.

Os seguintes valores de shell integrados são usados para criar o rastreamento de pilha:

  1. BASH_SOURCE: Array de nomes de arquivos onde cada comando foi chamado de volta ao script principal.
  2. FUNCNAME: matriz de números de linha correspondentes a cada arquivo em BASH_SOURCE.
  3. BASH_LINENO: matriz de números de linha por arquivo correspondente a BASH_SOURCE.
  4. BASH_COMMAND: Último comando executado com flags e argumentos.

Se o script sair com um erro de maneira inesperada, ele fará um loop sobre as variáveis acima e gerará cada uma delas em ordem para que um rastreamento de pilha possa ser criado. O número da linha do comando com falha não é mantido nas matrizes acima, mas é por isso que você capturou o número da linha cada vez que um comando falhou com a primeira instrução trap acima.

Juntando tudo

Crie os dois arquivos a seguir para dar suporte ao teste, para que você possa ver como as informações são coletadas em vários arquivos. Primeiro, testfile1.sh:

_file1_function1() {
   echo
   echo "executing in _file1_function1"
   echo

   _file2_function1
}

# adsfadfaf

_file1_function2() {
   echo
   echo "executing in _file1_function2"
   echo
  
   set -e
   curl this_curl_will_fail_and_CAUSE_A_STACK_TRACE

   # function never called
   _file2_does_not_exist
}

E a seguir, testfile2.sh:

_file2_function1() {
   echo
   echo "executing in _file2_function1"
   echo

   curl this_curl_will_simply_fail

   _file1_function2
}

NOTA: Se você mesmo criar esses arquivos, certifique-se de tornar o arquivo stacktrace.sh executável.

A execução de stacktrace.sh resultará no seguinte:

~/shell-stack-trace-example$./stracktrace.sh
Beginning stacktrace test

executing in _file1_function1

executing in _file2_function1
curl: (6) Could not resolve host: this_curl_will_simply_fail

executing in _file1_function2
curl: (6) Could not resolve host: this_curl_will_fail_and_CAUSE_A_STACK_TRACE

========= CATASTROPHIC COMMAND FAIL =========

SCRIPT EXITED ON ERROR CODE: 6

---
FILE: testfile1.sh
  FUNCTION: _file1_function2
  COMMAND: curl this_curl_will_fail_and_CAUSE_A_STACK_TRACE
  LINE: 15
---
FILE: testfile2.sh
  FUNCTION: _file2_function1
  COMMAND: _file1_function2
  LINE: 7
---
FILE: testfile1.sh
  FUNCTION: _file1_function1
  COMMAND: _file2_function1
  LINE: 5
---
FILE: stracktrace.sh
  FUNCTION: main
  COMMAND: _file1_function1
  LINE: 53

======= END CATASTROPHIC COMMAND FAIL =======

Para obter crédito extra, tente descomentar a linha em testfile1.sh e executar stacktrace.sh novamente:

# adsfadfaf

Em seguida, comente novamente a linha e, em vez disso, comente a seguinte linha em testfile1.sh que causou um rastreamento de pilha e execute stacktrace.sh uma última vez:

curl this_curl_will_fail_and_CAUSE_A_STACK_TRACE

Este exercício deve lhe dar uma ideia do resultado e de quando ele ocorre se você tiver erros de digitação em seus scripts.

Artigos relacionados: