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:
- Um sistema remoto inacessível
- Falha na autenticação em um sistema remoto
- Erros de sintaxe em arquivos de configuração ou script originados
- Compilações de imagens Docker
- 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:
- BASH_SOURCE: Array de nomes de arquivos onde cada comando foi chamado de volta ao script principal.
- FUNCNAME: matriz de números de linha correspondentes a cada arquivo em BASH_SOURCE.
- BASH_LINENO: matriz de números de linha por arquivo correspondente a BASH_SOURCE.
- 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.