Tratamento de erros no Bash
LidoNesta aula, você aprenderá a tratar erros em scripts Bash de forma profissional. Abordaremos verificação de comandos, uso do trap para capturar sinais, redirecionamento de mensagens de erro para stderr e técnicas de limpeza de recursos. Ao final, você terá as ferramentas necessárias para criar scripts robustos e confiáveis.
Escrever scripts Bash que apenas funcionam no caminho feliz é fácil, mas o que acontece quando algo dá errado? Comandos falham, arquivos não existem, permissões são negadas. Um script robusto precisa antecipar e tratar esses erros de forma elegante, fornecendo mensagens claras e garantindo que recursos sejam liberados corretamente. Nesta aula, vamos explorar as principais técnicas de tratamento de erros no Bash, desde a verificação de comandos até a captura de sinais e a limpeza de recursos temporários.
O Bash oferece diversas ferramentas que, quando combinadas, permitem criar scripts à prova de falhas. Veremos como verificar a existência de comandos antes de usá-los, como usar o trap para interceptar sinais e executar funções de limpeza, como direcionar mensagens de erro para o fluxo padrão de erro (stderr) e como garantir que arquivos temporários sejam removidos mesmo em caso de falha. Ao dominar essas técnicas, você elevará a qualidade dos seus scripts e evitará dores de cabeça em ambientes de produção.
Verificando comandos
Antes de executar um comando, é prudente verificar se ele está disponível no sistema. Isso é especialmente importante quando o script depende de ferramentas externas, como curl, jq, git etc. Se o comando não estiver instalado, o script pode falhar de forma confusa. Podemos verificar a existência de um comando usando o comando command -v ou which. O command -v é preferível por ser embutido no shell e mais portátil.
Uma boa prática é definir uma função que verifique se um comando existe e, caso contrário, exiba uma mensagem de erro e saia com um código de status adequado. Veja o exemplo:
#!/bin/bash
# Função para verificar se um comando existe
check_command() {
if ! command -v "$1" > /dev/null 2>&1; then
echo "Erro: comando '$1' não encontrado. Instale-o antes de executar este script." >&2
exit 1
fi
}
# Exemplo de uso
check_command curl
check_command jq
# Agora podemos usar os comandos com segurança
curl -s https://api.github.com/repos/octocat/Hello-World | jq '.name'
Nesse exemplo, a função check_command redireciona a saída de command -v para /dev/null (descartando qualquer saída) e os erros para o mesmo lugar, mas se o comando não for encontrado, o if é verdadeiro e a função emite uma mensagem de erro em stderr e sai com código 1.
Outra forma é usar o operador || para executar uma ação alternativa caso o comando falhe. Por exemplo:
command -v ffmpeg || { echo "ffmpeg não está instalado" >&2; exit 1; }
Essas verificações ajudam a diagnosticar problemas rapidamente e a dar feedback claro ao usuário.
trap
O comando trap é uma ferramenta poderosa para capturar sinais e executar comandos específicos quando eles ocorrem. Isso é essencial para garantir que o script faça uma limpeza adequada ao ser interrompido, seja por um Ctrl+C (SIGINT), por um término (SIGTERM) ou por uma saída normal (EXIT). A sintaxe básica é:
trap 'comandos' SINAL
Por exemplo, para capturar Ctrl+C e executar uma função de limpeza, podemos fazer:
#!/bin/bash
cleanup() {
echo "Executando limpeza..."
rm -f /tmp/meu_arquivo_tmp
}
# Armadilha para SIGINT (Ctrl+C) e SIGTERM
trap cleanup SIGINT SIGTERM
# Simula um processo demorado
echo "Iniciando processo..."
sleep 30
Quando o usuário pressionar Ctrl+C, o sinal SIGINT é enviado ao script, e o Bash executa a função cleanup antes de terminar. Também é comum usar trap ... EXIT para executar comandos sempre que o script sair, independentemente do motivo. Isso é útil para limpeza de arquivos temporários, por exemplo:
#!/bin/bash
# Cria um arquivo temporário
TMPFILE=$(mktemp)
# Garante que o arquivo seja removido ao sair
trap "rm -f $TMPFILE" EXIT
# Usa o arquivo temporário
echo "Conteúdo" > "$TMPFILE"
cat "$TMPFILE"
# O script termina e o trap remove o arquivo automaticamente
É importante citar que o trap pode ter múltiplas ações, e a ordem de execução é a ordem em que foram definidas. Também podemos listar as armadilhas ativas com trap -p e remover uma armadilha com trap - SINAL.
Outra aplicação comum é ignorar sinais temporariamente, como em:
trap '' SIGINT # ignora Ctrl+C
Mas use com cuidado, pois pode tornar o script difícil de interromper.
Mensagens de erro (stderr)
No Bash, existem dois fluxos de saída principais: stdout (saída padrão) e stderr (erro padrão). Boas práticas recomendam que mensagens de erro sejam enviadas para stderr, para que possam ser separadas da saída normal do programa. Isso é feito redirecionando a saída com >&2 ou usando o descritor de arquivo 2.
Por exemplo, para exibir uma mensagem de erro, podemos usar:
echo "Erro: arquivo não encontrado" >&2
Também podemos definir uma função de erro que imprime no stderr e sai com um código de status:
#!/bin/bash
# Função para exibir erro e sair
error_exit() {
echo "$1" >&2
exit 1
}
# Uso
if [ ! -f "/etc/passwd" ]; then
error_exit "Arquivo /etc/passwd não existe."
fi
Além disso, é comum redirecionar a saída de erro de comandos para um arquivo de log, usando 2> arquivo.log ou 2>&1 para combinar stdout e stderr. Por exemplo:
./meu_script.sh 2> erro.log
Isso permite que o usuário capture apenas os erros, mantendo a saída normal limpa.
Outra técnica é usar o comando set -x para depuração (mostra cada comando executado) e set -e para sair imediatamente se um comando falhar. No entanto, set -e deve ser usado com cautela, pois pode interromper o script em situações inesperadas. Uma alternativa mais sutil é usar set -o pipefail para capturar falhas em pipelines.
Limpeza
Quando um script cria arquivos temporários, diretórios ou outros recursos, é nossa responsabilidade limpá-los ao final, mesmo se ocorrer um erro. A combinação de trap com EXIT é a maneira mais eficaz de garantir essa limpeza. Vamos ver um exemplo mais completo que inclui a criação de um diretório temporário e a captura de erros:
#!/bin/bash
# Cria um diretório temporário
TMPDIR=$(mktemp -d)
# Função de limpeza
cleanup() {
echo "Removendo diretório temporário $TMPDIR"
rm -rf "$TMPDIR"
}
# Armadilha para EXIT e sinais
trap cleanup EXIT SIGINT SIGTERM
# Simula um trabalho que pode falhar
echo "Trabalhando em $TMPDIR"
# ... algum processamento ...
# Se algo der errado, podemos sair e a limpeza será executada
if [ ! -f "/arquivo/inexistente" ]; then
echo "Erro: arquivo não encontrado" >&2
exit 1
fi
# Continuação do script
Nesse exemplo, se o script for interrompido por um sinal ou se sair por causa de um erro, a função cleanup será executada, removendo o diretório temporário. Isso evita o acúmulo de lixo no sistema.
Outra técnica comum é usar um arquivo de lock para impedir execuções simultâneas. A limpeza deve remover esse lock ao final. Veja:
#!/bin/bash
LOCKFILE="/tmp/meu_script.lock"
# Tenta criar o lock com exclusividade
if ( set -o noclobber; echo "$$" > "$LOCKFILE") 2> /dev/null; then
# Armadilha para remover o lock ao sair
trap 'rm -f "$LOCKFILE"; exit' EXIT SIGINT SIGTERM
echo "Script em execução..."
# ... trabalho ...
else
echo "Outra instância está em execução. Saindo." >&2
exit 1
fi
Aqui, o trap garante que o lock seja removido mesmo se o script for interrompido.
Em scripts mais complexos, a limpeza pode envolver encerrar processos filhos, remover arquivos temporários, restaurar configurações etc. O importante é centralizar essas ações em uma função e usar trap para garantir sua execução.
Boas práticas e observações finais
Ao tratar erros no Bash, lembre-se de sempre fornecer mensagens claras e específicas, direcionadas ao stderr. Use códigos de saída consistentes (0 para sucesso, não-zero para erros) para que outros scripts possam verificar o resultado. Combine set -e com trap para uma abordagem defensiva, mas tenha cuidado com comandos que podem falhar em condições normais (como grep que não encontra correspondência). Nesses casos, use || true ou estruturas condicionais.
Teste seus scripts em diferentes cenários de erro para garantir que o comportamento seja o esperado. Utilize ferramentas como shellcheck para identificar problemas estáticos. A adoção dessas práticas tornará seus scripts mais robustos e confiáveis.
Referências
- Bash Manual: Conditional Constructs
- Bash Manual: Bourne Shell Builtins (trap, command, etc.)
- Bash Manual: Redirections
- Bash Guide for Beginners: Traps
- ShellCheck - find bugs in your shell scripts
- BashFAQ 105: Why doesn't set -e work as expected?
- Stack Overflow: Error handling in Bash
Exercícios
- Exercício 1: Escreva um script que verifique se os comandos
gitevimestão instalados. Se algum não estiver, exiba uma mensagem de erro em stderr e saia com código 1. Se ambos estiverem, imprima "Todos os comandos estão disponíveis".✓ Resposta:#!/bin/bash check_command() { if ! command -v "$1" > /dev/null 2>&1; then echo "Erro: comando '$1' não encontrado." >&2 exit 1 fi } check_command git check_command vim echo "Todos os comandos estão disponíveis" - Exercício 2: Crie um script que crie um arquivo temporário usando
mktempe, em seguida, usetrappara removê-lo quando o script sair. O script deve imprimir o nome do arquivo temporário e aguardar uma entrada do usuário antes de sair.✓ Resposta:#!/bin/bash TMPFILE=$(mktemp) trap "rm -f $TMPFILE" EXIT echo "Arquivo temporário criado: $TMPFILE" read -p "Pressione Enter para sair..." - Exercício 3: Escreva um script que tente ler um arquivo passado como argumento. Se o arquivo não existir, envie uma mensagem de erro para stderr e saia com código 2. Use uma função de erro para centralizar a mensagem.✓ Resposta:
#!/bin/bash if [ $# -ne 1 ]; then echo "Uso: $0 <arquivo>" >&2 exit 2 fi FILE="$1" if [ ! -f "$FILE" ]; then echo "Erro: arquivo '$FILE' não encontrado." >&2 exit 2 fi cat "$FILE" - Exercício 4: Crie um script que execute um comando longo (por exemplo,
sleep 30) e, se o usuário pressionar Ctrl+C, exiba uma mensagem de limpeza e saia. Usetrappara capturar SIGINT.✓ Resposta:#!/bin/bash cleanup() { echo "Interrompido. Executando limpeza..." exit 0 } trap cleanup SIGINT echo "Iniciando tarefa demorada. Pressione Ctrl+C para interromper." sleep 30 - Exercício 5: Escreva um script que crie um diretório temporário com
mktemp -d, entre nele e execute um comando que pode falhar (por exemplo,false). Usetrappara remover o diretório ao sair, mesmo que ocorra erro. O script deve terminar com o código de erro do comando que falhou.✓ Resposta:#!/bin/bash TMPDIR=$(mktemp -d) trap 'rm -rf "$TMPDIR"' EXIT cd "$TMPDIR" || exit 1 echo "Executando comando que pode falhar..." false # comando falha echo "Este comando não será alcançado se 'false' falhar?"Para capturar o código de erro, precisamos usar
set -eou verificar explicitamente. Exemplo comset -e:#!/bin/bash set -e TMPDIR=$(mktemp -d) trap 'rm -rf "$TMPDIR"' EXIT cd "$TMPDIR" || exit 1 echo "Executando comando que pode falhar..." false # comando falha e o script sai imediatamente