Trap: executando código ao sair do script

1. Introdução ao comando trap

O comando trap é uma das ferramentas mais poderosas do Bash para controle de fluxo e tratamento de eventos. Ele permite que você execute código específico quando um sinal é recebido pelo shell, seja durante a execução normal do script ou quando algo inesperado acontece.

A sintaxe básica é simples:

trap 'comando' SINAL

Onde 'comando' é o código a ser executado e SINAL é o nome do sinal a ser capturado.

Os sinais mais comuns no contexto de scripts Bash são:

  • EXIT — executado quando o script termina (por qualquer motivo)
  • INT (SIGINT) — enviado quando o usuário pressiona Ctrl+C
  • TERM (SIGTERM) — sinal de término enviado por processos externos
  • ERR — executado quando um comando retorna código de erro diferente de zero

2. Capturando a saída normal do script com trap EXIT

O sinal EXIT é especial no Bash: ele é acionado sempre que o script termina, independentemente de ter sido bem-sucedido ou não. Isso o torna ideal para tarefas de limpeza.

#!/bin/bash

tmpfile=$(mktemp)

trap 'echo "Limpando arquivo temporário..."; rm -f "$tmpfile"' EXIT

echo "Escrevendo dados no arquivo temporário..."
echo "dados importantes" > "$tmpfile"

# Simula algum processamento
sleep 2

echo "Script finalizado com sucesso"

A diferença crucial entre trap ... EXIT e código colocado após o corpo principal do script é que o trap será executado mesmo se o script for interrompido abruptamente ou se houver um exit explícito em qualquer ponto.

#!/bin/bash

cleanup() {
    echo "Executando limpeza..."
}

trap cleanup EXIT

if [ "$1" = "erro" ]; then
    echo "Erro detectado, saindo..."
    exit 1  # O trap será executado aqui
fi

echo "Script executado normalmente"

3. Tratando interrupções do usuário: trap INT e trap TERM

Interrupções do usuário são comuns em scripts longos. O trap INT captura Ctrl+C, permitindo um desligamento gracioso.

#!/bin/bash

salvar_estado() {
    echo "Salvando estado antes de sair..."
    # Salvar progresso em arquivo de checkpoint
    echo "$contador" > /tmp/checkpoint.txt
}

trap 'salvar_estado; exit 1' INT

contador=0
while true; do
    ((contador++))
    echo "Processando item $contador..."
    sleep 1
done

Para ignorar completamente interrupções, use uma string vazia como comando:

#!/bin/bash

trap '' INT  # Ignora Ctrl+C

echo "Este script não pode ser interrompido com Ctrl+C"
sleep 10
echo "Concluído"

O sinal TERM é útil para scripts que precisam responder a sinais de término enviados por outros processos (como o comando kill):

#!/bin/bash

trap 'echo "Recebido SIGTERM, salvando dados..."; exit 0' TERM

echo "PID do processo: $$"
echo "Envie 'kill $$' para testar"

while true; do
    echo "Trabalhando..."
    sleep 2
done

4. Usando trap ERR para depuração e rollback

O sinal ERR é acionado sempre que um comando retorna um código de erro. Combinado com set -e, ele cria um mecanismo poderoso para tratamento de erros.

#!/bin/bash

set -e  # Sai do script ao primeiro erro

rollback() {
    echo "Erro detectado! Realizando rollback..."
    # Reverter alterações no banco de dados
    echo "DELETE FROM transacoes WHERE status='pendente';" | mysql
}

trap 'rollback' ERR

echo "Iniciando transação..."
mysql -e "INSERT INTO transacoes (status) VALUES ('pendente')"

# Comando que pode falhar
processar_dados() {
    return 1  # Simula uma falha
}

processar_dados
echo "Transação concluída com sucesso"  # Não será executado

5. Múltiplos traps e ordem de execução

Quando você define múltiplos traps para o mesmo sinal, o último definido substitui o anterior. Para acumular comportamentos, use funções:

#!/bin/bash

trap1() { echo "Primeira tarefa de limpeza"; }
trap2() { echo "Segunda tarefa de limpeza"; }

cleanup() {
    trap1
    trap2
}

trap cleanup EXIT

echo "Script em execução..."

A ordem de execução quando múltiplos sinais são recebidos segue a prioridade dos sinais. Geralmente, EXIT é executado por último, após os handlers de INT ou TERM.

6. Limpando traps e restaurando comportamento padrão

Para remover um trap e restaurar o comportamento padrão de um sinal, use trap - SINAL:

#!/bin/bash

trap 'echo "Interrompido!"' INT
echo "Trap ativo para Ctrl+C"
sleep 3

trap - INT  # Restaura comportamento padrão
echo "Trap removido - Ctrl+C agora funciona normalmente"
sleep 3

echo "Script concluído"

7. Casos de uso avançados

Trap em subshells

Traps definidos em subshells não afetam o shell pai:

#!/bin/bash

trap 'echo "Trap do shell pai"' EXIT

(
    trap 'echo "Trap do subshell"' EXIT
    echo "Dentro do subshell"
)
echo "De volta ao shell pai"

Usando trap DEBUG para rastreamento

O sinal DEBUG é executado antes de cada comando, útil para debugging:

#!/bin/bash

trap 'echo "Executando: $BASH_COMMAND"' DEBUG

echo "Primeiro comando"
ls /tmp
echo "Último comando"

Watchdog com trap ALRM

#!/bin/bash

TIMEOUT=5

watchdog() {
    echo "Timeout! Processo excedeu $TIMEOUT segundos"
    exit 1
}

trap watchdog ALRM

echo "Iniciando processo com timeout de $TIMEOUT segundos..."
sleep 10 &

pid=$!
(sleep $TIMEOUT; kill -ALRM $$) &
sleep_pid=$!

wait $pid
kill $sleep_pid 2>/dev/null
echo "Processo concluído dentro do tempo"

8. Boas práticas e armadilhas comuns

Aspas corretas: Use aspas simples para evitar expansão de variáveis no momento da definição do trap:

# Correto - variável será expandida quando o trap for executado
trap 'echo "Arquivo: $file"' EXIT

# Incorreto - variável é expandida imediatamente
trap "echo \"Arquivo: $file\"" EXIT

Evite traps dentro de loops: Cada iteração pode redefinir o trap, causando comportamentos inesperados:

# Evite isso
for i in {1..5}; do
    trap "echo 'Loop $i interrompido'" INT
    sleep 1
done

Testando traps: Use kill -SINAL $$ para testar traps durante o desenvolvimento:

#!/bin/bash

trap 'echo "Teste de sinal bem-sucedido"' USR1

echo "Testando com kill -USR1 $$"
kill -USR1 $$

Sempre limpe recursos: Um trap EXIT bem definido deve garantir que arquivos temporários, descritores de arquivo e outros recursos sejam liberados, independentemente de como o script termina.

Referências