Em scripts shell, muitas vezes precisamos garantir que ações de limpeza sejam executadas mesmo se o script for interrompido inesperadamente. O comando trap permite capturar sinais do sistema operacional e executar comandos personalizados quando esses sinais são recebidos. Isso é essencial para criar scripts robustos que lidam com arquivos temporários, conexões de rede ou outros recursos que precisam ser liberados.

Os sinais são notificações enviadas a um processo para indicar eventos como interrupção do usuário (Ctrl+C), término forçado ou finalização normal. Com trap, podemos definir handlers (manipuladores) que respondem a esses sinais, executando funções de limpeza ou ignorando certos sinais quando necessário.

Capturando sinais (EXIT, INT, TERM)

O comando trap tem a sintaxe: trap 'comandos' SIGNAME. Onde 'comandos' são executados quando o sinal SIGNAME é recebido. Os sinais mais comuns em scripts são:

  • EXIT: Não é um sinal real, mas um pseudo-sinal que é acionado quando o script termina, seja por conclusão normal, por um exit ou por um sinal fatal. É ideal para limpeza geral.
  • INT: Interrupção (geralmente Ctrl+C). Permite ao script finalizar graciosamente ao invés de abortar abruptamente.
  • TERM: Sinal de término enviado pelo comando kill ou pelo sistema. Capturá-lo permite limpeza antes de sair.

Exemplo básico de captura de INT e TERM:

#!/bin/bash
cleanup() {
    echo "Executando limpeza..."
    rm -f /tmp/meu_arquivo_temporario
    exit 0
}
trap cleanup INT TERM
echo "Script em execução. Pressione Ctrl+C para interromper."
while true; do
    sleep 1
done

Nesse exemplo, a função cleanup é chamada quando o script recebe SIGINT ou SIGTERM. O exit 0 dentro da função garante que o script termine após a limpeza.

Para capturar a saída normal (EXIT), usamos:

trap cleanup EXIT

Isso garante que cleanup seja executada mesmo se o script chegar ao fim naturalmente.

Limpeza de temporários

Um uso clássico de trap é remover arquivos temporários criados durante a execução do script. Sem a captura de sinais, se o script for interrompido, esses arquivos podem ficar acumulados no sistema.

Exemplo de script que cria um diretório temporário e o remove ao final:

#!/bin/bash
TEMPDIR=$(mktemp -d)
cleanup() {
    echo "Removendo diretório temporário $TEMPDIR"
    rm -rf "$TEMPDIR"
}
trap cleanup EXIT
echo "Diretório temporário criado em: $TEMPDIR"
# Simula trabalho
sleep 10

Mesmo que o script seja interrompido com Ctrl+C, o trap EXIT será executado? Na verdade, o sinal INT não dispara EXIT automaticamente. Para garantir a limpeza também em interrupções, devemos capturar INT explicitamente:

trap cleanup EXIT INT TERM

Dessa forma, qualquer sinal ou término normal chamará a função de limpeza.

Casos de uso

Além da remoção de temporários, trap é útil em várias situações:

  • Ignorar sinais: Para evitar que um script seja interrompido, podemos ignorar certos sinais usando trap '' SIGINT.
  • Reinicializar configurações: Restaurar configurações de terminal (como echo) ao sair.
  • Matar processos filhos: Se o script inicia processos em segundo plano, podemos usar trap para terminá-los antes de sair.
  • Registrar logs: Escrever em um arquivo de log quando o script termina.
  • Executar comandos apenas na saída bem-sucedida: Combinar com trap ... EXIT para ações que devem ocorrer independentemente do motivo da saída.

Exemplo de ignorar Ctrl+C:

#!/bin/bash
trap '' INT
echo "Ctrl+C não funciona agora. Pressione Ctrl+\ para sair."
sleep 30

Exemplos

Vamos a um exemplo mais completo que combina múltiplos sinais e funções:

#!/bin/bash
LOGFILE="/var/log/meu_script.log"
TEMPDIR=$(mktemp -d)

cleanup() {
    local exit_code=$?
    echo "$(date): Script finalizado com código $exit_code" >> "$LOGFILE"
    rm -rf "$TEMPDIR"
    exit $exit_code
}

handle_int() {
    echo "Interrupção recebida. Finalizando..."
    exit 1
}

trap cleanup EXIT
trap handle_int INT TERM

echo "Iniciando script. PID: $$"
# Simula trabalho
for i in {1..10}; do
    echo "Trabalhando... $i"
    sleep 1
done

Nesse script, a função cleanup é chamada sempre que o script termina (EXIT) e registra o código de saída em um log. Já handle_int lida com interrupções e termina com código 1. A variável $? captura o código de saída do último comando executado.

Outro exemplo útil é garantir que um processo em segundo plano seja morto ao sair:

#!/bin/bash
# Inicia um servidor em background
./meu_servidor &
SERVER_PID=$!

cleanup() {
    echo "Parando servidor (PID $SERVER_PID)..."
    kill $SERVER_PID 2>/dev/null
    wait $SERVER_PID 2>/dev/null
    echo "Servidor parado."
}

trap cleanup EXIT INT TERM

echo "Servidor iniciado. Pressione Ctrl+C para parar."
wait $SERVER_PID

O wait aguarda o processo em segundo plano terminar. Se o script receber um sinal, o trap executa a limpeza e o wait é interrompido.

Boas práticas e observações finais

Ao usar trap, lembre-se de:

  • Escapar corretamente aspas e variáveis dentro do comando do trap. Use aspas simples para evitar expansão prematura ou aspas duplas com cuidado.
  • Definir traps no início do script, antes de qualquer operação que possa gerar arquivos temporários.
  • Usar trap - SIGNAME para resetar o trap para o comportamento padrão.
  • Testar o script com diferentes sinais para garantir que a limpeza funciona.

O comando trap é uma ferramenta poderosa para escrever scripts confiáveis e profissionais. Dominá-lo é essencial para qualquer programador shell.

Referências

Exercícios

  1. Crie um script que cria um arquivo temporário em /tmp, escreve algo nele e, ao sair (qualquer motivo), o remove. Use trap para capturar EXIT.

    ✓ Resposta:
    #!/bin/bash
    TMPFILE=$(mktemp /tmp/meu_script.XXXXXX)
    echo "Conteúdo temporário" > "$TMPFILE"
    cleanup() {
        rm -f "$TMPFILE"
        echo "Arquivo $TMPFILE removido."
    }
    trap cleanup EXIT
    echo "Arquivo criado: $TMPFILE"
    # Resto do script
    sleep 5
  2. Escreva um script que ignora o sinal INT (Ctrl+C) por 10 segundos e depois volta a tratá-lo normalmente.

    ✓ Resposta:
    #!/bin/bash
    echo "Ignorando Ctrl+C por 10 segundos..."
    trap '' INT
    sleep 10
    echo "Agora Ctrl+C funcionará."
    trap - INT
    sleep 10
    echo "Fim."
  3. Faça um script que inicia um processo em segundo plano (ex.: sleep 100) e, ao receber INT ou TERM, mata esse processo antes de sair.

    ✓ Resposta:
    #!/bin/bash
    sleep 100 &
    PID=$!
    cleanup() {
        echo "Matando processo $PID..."
        kill $PID 2>/dev/null
        wait $PID 2>/dev/null
        echo "Processo finalizado."
        exit 0
    }
    trap cleanup INT TERM
    echo "Processo em segundo plano iniciado (PID $PID)."
    wait $PID
    echo "Processo terminou sozinho."
  4. Crie um script que registra em um arquivo de log (/tmp/script.log) a data e hora de início e de término, usando trap EXIT. Dica: capture a data de início em uma variável.

    ✓ Resposta:
    #!/bin/bash
    LOGFILE="/tmp/script.log"
    START_TIME=$(date)
    echo "Início: $START_TIME" >> "$LOGFILE"
    cleanup() {
        echo "Término: $(date)" >> "$LOGFILE"
    }
    trap cleanup EXIT
    echo "Script executando..."
    sleep 5
  5. Escreva um script que cria dois arquivos temporários, e na limpeza (EXIT, INT, TERM) remove ambos. Use uma função de limpeza que itera sobre uma lista de arquivos.

    ✓ Resposta:
    #!/bin/bash
    TMP1=$(mktemp /tmp/tmp1.XXXXXX)
    TMP2=$(mktemp /tmp/tmp2.XXXXXX)
    FILES=("$TMP1" "$TMP2")
    cleanup() {
        for f in "${FILES[@]}"; do
            rm -f "$f"
            echo "Removido: $f"
        done
    }
    trap cleanup EXIT INT TERM
    echo "Arquivos criados: ${FILES[*]}"
    sleep 10