Scripting Avançado em Bash: Melhores Práticas para Tratamento de Erros

Melhore o tratamento de erros em Bash com modo estrito, verificações explícitas, armadilhas de limpeza, códigos de saída claros e registro em stderr.

Scripting Avançado em Bash: Melhores Práticas para Tratamento de Erros

O tratamento de erros em scripts Bash é o que impede que um pequeno erro de automação se transforme em um problema de produção desastroso. Se um backup falhar, uma chamada de API retornar um erro ou um arquivo temporário for deixado para trás, seu script deve parar claramente e deixar o sistema em um estado conhecido.

Use esses padrões quando seu script modificar arquivos, implantar código, comunicar-se com serviços remotos ou executar sem alguém monitorando o terminal.

O Fundamento: Entendendo os Códigos de Saída

Todo comando executado no Bash, seja bem-sucedido ou falho, retorna um status de saída (ou código de saída). Este é o mecanismo fundamental para sinalizar resultados de comandos.

  • Código de Saída 0: Indica execução bem-sucedida. Por convenção, zero significa sucesso.
  • Código de Saída 1-255 (Não zero): Indica um erro, falha ou aviso. Códigos não zero específicos geralmente denotam tipos de erro específicos (por exemplo, 1 geralmente significa erro genérico, 2 frequentemente significa uso incorreto de comando do shell).

O status de saída mais recente é armazenado na variável especial $?.

# Comando bem-sucedido
ls /tmp
echo "Status: $?"
# Status: 0

# Comando falho (arquivo inexistente)
cat /arquivo_inexistente
echo "Status: $?"
# Status: 1 (ou maior, dependendo do erro)

Melhor Prática Obrigatória: Implementando o Modo Estrito

Para qualquer script Bash sério, três diretivas devem ser colocadas imediatamente após a linha shebang. Coletivamente, elas são frequentemente chamadas de "modo estrito". Elas forçam o script a falhar cedo, em vez de continuar após um pré-requisito quebrado.

1. Sair Imediatamente em Caso de Erro (set -e)

O comando set -e ou set -o errexit instrui o Bash a sair imediatamente do script se qualquer comando sair com um status não zero. Isso evita falhas em cascata.

Aviso: set -e é ignorado em testes condicionais (if, while) ou se um comando fizer parte de uma lista && ou ||. O status de falha deve ser explicitamente usado pela estrutura circundante.

2. Tratar Variáveis Não Definidas como Erros (set -u)

O comando set -u ou set -o nounset faz com que o script saia imediatamente se tentar usar uma variável que não foi definida (por exemplo, digitar $NOMEDOARQUIVO em vez de $NOME_DO_ARQUIVO). Isso evita bugs difíceis de depurar resultantes de variáveis vazias ou não intencionais.

3. Lidar com Erros em Pipelines (set -o pipefail)

Por padrão, se uma série de comandos for encadeada (por exemplo, cmd1 | cmd2 | cmd3), o Bash relata apenas o status de saída do último comando (cmd3). Se cmd1 falhar, o script pode continuar executando com sucesso.

set -o pipefail garante que o status de saída do pipeline seja o status de saída do último comando que falhou, ou zero se todos os comandos forem bem-sucedidos. Isso é crítico para processamento de dados confiável.

Cabeçalho Padrão do Modo Estrito

Sempre inicie scripts avançados com este cabeçalho robusto:

#!/bin/bash

set -euo pipefail

Alguns modelos mais antigos também definem IFS=$'\n\t'. Use isso apenas quando você entender como isso afeta a divisão de palavras no restante do script. Citar variáveis e ler entrada com while IFS= read -r line geralmente é mais claro.

Verificação Condicional de Erros

Enquanto set -e lida com erros inesperados, muitas vezes você precisa verificar condições específicas ou fornecer mensagens de erro personalizadas.

Usando Blocos if e Funções Personalizadas

Em vez de depender apenas de set -e, use blocos if para lidar com falhas potenciais conhecidas de forma graciosa e fornecer saída descritiva.

# Defina uma função de erro personalizada para consistência
error_exit() {
    printf '[FATAL] %s\n' "$1" >&2
    exit 1
}

DIR_TEMP="/tmp/processamento_dados_$(date +%s)"

# Verifique se a criação do diretório foi bem-sucedida
if ! mkdir -p "$DIR_TEMP"; then
    error_exit "Falha ao criar diretório temporário: $DIR_TEMP"
fi

echo "Diretório temporário criado com sucesso: $DIR_TEMP"

# Exemplo de verificação se um arquivo existe antes de processar
ARQUIVO_PARA_PROCESSAR="entrada.csv"

if [[ ! -f "$ARQUIVO_PARA_PROCESSAR" ]]; then
    error_exit "Arquivo de entrada não encontrado: $ARQUIVO_PARA_PROCESSAR"
fi

Lógica de Curto-Circuito (&& e ||)

Para operações sequenciais simples, use operadores de curto-circuito. Isso é altamente legível e conciso.

  • Cadeia de Sucesso (&&): O segundo comando é executado apenas se o primeiro for bem-sucedido.
  • Captura de Falha (||): O segundo comando é executado apenas se o primeiro falhar.
# Execute a configuração e depois processe, falhando se a configuração falhar
configurar_ambiente && processar_dados

# Tente conectar, caso contrário, saia graciosamente com uma mensagem
ssh usuario@servidor || { echo "Falha na conexão, verifique as configurações de rede." >&2; exit 2; }

Terminação Graciosa e Limpeza com trap

O comando trap permite que o script capture sinais (como Ctrl+C, terminação do sistema ou saída do script) e execute um comando ou função especificada antes de terminar. Isso é essencial para tarefas de limpeza.

A Função cleanup

Defina uma função dedicada para reverter quaisquer alterações (por exemplo, excluir arquivos temporários, redefinir configurações) e use trap para garantir que ela seja executada independentemente de como o script termina.

# Variável global para a função de limpeza verificar
ARQUIVO_TEMP=""

cleanup() {
    printf '%s\n' "--- Executando Procedimentos de Limpeza ---"
    if [[ -f "$ARQUIVO_TEMP" ]]; then
        rm -f "$ARQUIVO_TEMP"
        echo "Arquivo temporário excluído: $ARQUIVO_TEMP"
    fi
    # Opcionalmente, forneça relatório de status de saída final
}

# 1. Trap EXIT: Executa a limpeza independentemente de sucesso, falha ou sinal.
trap cleanup EXIT

# 2. Trap de sinais (INT=Ctrl+C, TERM=Sinal Kill)
trap 'printf "%s\n" "Script interrompido por usuário ou sinal do sistema." >&2; exit 130' INT
trap 'printf "%s\n" "Script terminado." >&2; exit 143' TERM

# --- Lógica Principal do Script ---
ARQUIVO_TEMP=$(mktemp)
echo "Conteúdo temporário" > "$ARQUIVO_TEMP"
# Se o script falhar ou for interrompido aqui, cleanup() é garantido de executar

Por que Usar trap cleanup EXIT?

Definir um trap em EXIT garante que a função de limpeza será executada quer o script termine normalmente (exit 0), saia explicitamente com um erro (exit 1) ou seja forçado a terminar devido a set -e.

Relatório de Erros Avançado

Mensagens de erro padrão (comando não encontrado) muitas vezes carecem de contexto. Scripts avançados devem relatar o que falhou, onde falhou e por que.

Registrando Números de Linha

Quando uma função como error_exit é chamada, você pode determinar o número da linha dentro do script onde o erro ocorreu usando o array BASH_LINENO ou o comando caller (embora caller seja frequentemente restrito a funções).

Para relatórios simples fora de funções, use a variável LINENO:

# Exemplo de relatório de falha imediata
(comando_arriscado) || {
    echo "[ERRO $LINENO] comando_arriscado falhou com status $?" >&2
    exit 3
}

Distinguindo Saída

Sempre envie mensagens informativas para a saída padrão (stdout) e mensagens de erro/aviso para o erro padrão (stderr). Isso é crucial se a saída do seu script estiver sendo canalizada para outro programa ou registrada externamente.

  • echo "Mensagem informativa" (vai para stdout)
  • echo "[AVISO] Substituição de configuração" >&2 (vai para stderr)

Monte o Padrão

Para a maioria dos scripts de produção, o padrão prático é simples: comece com set -euo pipefail, valide as entradas antes de fazer o trabalho, envolva falhas esperadas em if ! comando; then ...; fi, e adicione trap cleanup EXIT antes de criar estado temporário.

Isso fornece falhas úteis em vez de falhas misteriosas. Da próxima vez que um trabalho falhar às 2 da manhã, o log deve mostrar o que falhou, onde procurar e se a limpeza foi executada.