Bash: Checagem de erros

By | April 11, 2020

Comentei no post anterior que checagem de erros em bash pode ser meio complicada. Mas não necessariamente impossível. Só chata de implementar. Vamos dar uma olhada melhor como fazer isso.

Shell script tem um problema inerente à sua natureza: É, genericamente falando, uma lista de comandos. Ele vai executar todas as linhas de comando, uma após a outra, independente se o anterior funcionou ou não.
Pense nas consequências disso olhando esse script:

#!/bin/bash
OLD_DIRECTORY=$(grep old_name file.txt|cut -d "=" -f2)
rm -rvf /$OLD_DIRECTORY

Se por algum motivo ele não conseguir popular a variável OLD_DIRECTORY alguém vai ter um dia muito ruim:

# ./clean.sh
grep: file.txt: No such file or directory
removed '/bin/chmod'
removed '/bin/ionice'
removed '/bin/gzip'
removed '/bin/setserial'
removed '/bin/fsync'
removed '/bin/false'
removed '/bin/dmesg'
removed '/bin/mount'
removed '/bin/setpriv'
removed '/bin/login'
removed '/bin/hostname'
removed '/bin/echo'
removed '/bin/mpstat'
removed '/bin/su'
removed '/bin/mktemp'
removed '/bin/pipe_progress'
removed '/bin/touch'
removed '/bin/watch'
removed '/bin/mv'
removed '/bin/date'
removed '/bin/gunzip'
removed '/bin/chgrp'
removed '/bin/egrep'
removed '/bin/iostat'
...

Vejam que a primeira linha fala que o arquivo “file.txt” não foi encontrado pelo grep. Mas o rapazinho bash continua na sua missão de executar todas as linhas e parte para a próxima.

Como a variável OLD_DIRECTORY não foi populada o comando que o bash executa é:

rm -rfv /

Tradicionalmente eu costumava endereçar isso da seguinte forma:

#!/bin/bash

OLD_DIRECTORY=$(grep old_name file.txt|cut -d "=" -f2 )
[ -z "$OLD_DIRECTORY" ] && echo "Deu ruim" && exit 1
rm -rvf $OLD_DIRECTORY/
# ./clean.sh 
grep: file.txt: No such file or directory
Deu ruim

Isso sem dúvida está correto e nem deu trabalho de escrever nesse script de 3 linhas. Mas ai começamos a ter um script de 100, 200, 300 linhas. Começa a ter outros colaboradores adicionando código, que podem não ser tão cuidadosos quanto você. Ou coisas podem falhar de uma forma inesperada que seus testes não estavam esperando.

Uma forma simples de prevenir problemas é setar algumas opções no bash. Vamos colocar essas no script original:

-e : Aborta o script imediatamente se um comando falha, exceto se faz parte de uma pipeline.

-o pipefail : Também aborta de for parte de uma pipeline

#!/bin/bash
set -e -o pipefail

OLD_DIRECTORY=$(grep old_name file.txt|cut -d "=" -f2)
rm -rvf $OLD_DIRECTORY/
# ./clean.sh 
grep: file.txt: No such file or directory

Pronto. Agora o nosso script não vai ter consequências desastrosas caso o grep falhe e também não precisamos nos preocupar em endereçar todas as possibilidades de error ao decorrer do script. Qualquer coisa que der errada em qualquer ponto ele vai abortar a execução naquele momento.

Mantenha em mente que você talvez precise adaptar algumas coisas no seu shell script. É muito comum comandos falharem intencionalmente e fazermos o tratamento disso.

Um exemplo:

#!/bin/bash
set -e -o pipefail

TOOLS=(aws eksctl helm kubectl curl jq)
for tool in ${TOOLS[@]}; do
    command -v $tool > /dev/null 2>&1 
    (( $? > 0 )) && echo -e "$ERROR $tool is not installed" && exit 1
done
# ./clean.sh

Nada aconteceu? Que raios?? Na verdade o script rodou e, logo de cara não achou o “aws”. Mas como estamos redirecionando mensagens de erro pra /dev/null e abortando o script, nunca ficamos sabendo o que deu errado.

A solução é adaptar o script da seguinte forma:

#!/bin/bash
set -e -o pipefail

TOOLS=(aws eksctl helm kubectl curl jq)
for tool in ${TOOLS[@]}; do
    command -v $tool > /dev/null 2>&1 && :
    (( $? > 0 )) && echo -e "$ERROR $tool is not installed" && exit 1
done
# ./clean.sh 
 aws is not installed

Bem melhor! o && : garantiu que o a próxima linha fosse executada, apesar da atual ter falhado. Ai podemos fazer uma avaliação e dar pro usuário uma mensagem mais útil do que simplesmente sair em silêncio.

Aqui um artigo – em inglês – bem bacana: https://vaneyckt.io/posts/safer_bash_scripts_with_set_euxo_pipefail/