Em scripts shell, é comum precisarmos executar ações específicas quando o script termina, seja de forma normal ou devido a um sinal externo (como Ctrl+C). O comando trap permite capturar sinais do sistema e executar comandos ou funções personalizadas. Isso é essencial para garantir a limpeza de recursos temporários, evitar arquivos órfãos e assegurar um comportamento previsível mesmo em interrupções.

Nesta aula, você aprenderá a usar trap para lidar com os sinais mais comuns (EXIT, INT, TERM), criar funções de limpeza e aplicar boas práticas em scripts do mundo real.

Capturando sinais (EXIT, INT, TERM)

O trap associa um comando ou função a um ou mais sinais. A sintaxe básica é: trap 'comando' SINAL. O sinal EXIT é especial: é gerado sempre que o script termina, independentemente da causa (sucesso, erro, saída por exit ou fim natural). Já INT (SIGINT) é enviado ao pressionar Ctrl+C, e TERM (SIGTERM) é o sinal padrão de término enviado pelo comando kill.

É importante notar que o comando dentro de trap pode ser uma string com múltiplos comandos separados por ponto e vírgula, ou uma função. Para maior clareza, recomenda-se o uso de funções. Exemplo:

#!/bin/bash

cleanup() {
    echo "Limpando recursos..."
    rm -f /tmp/meu_arquivo_tmp
}

trap cleanup EXIT INT TERM

echo "Script em execução..."
sleep 10
echo "Fim normal."

No código acima, a função cleanup é chamada automaticamente se o script receber qualquer um dos sinais EXIT, INT ou TERM. Isso garante que o arquivo temporário seja removido mesmo se o script for interrompido.

Limpeza de temporários

Arquivos temporários são comuns em scripts que processam dados, fazem downloads ou realizam operações intermediárias. Se o script falhar ou for interrompido, esses arquivos podem acumular-se e consumir espaço em disco. Usando trap, podemos removê-los de forma confiável.

Uma prática recomendada é criar um diretório temporário com mktemp -d e registrar sua remoção no trap. Exemplo:

#!/bin/bash

# Cria diretório temporário
tmp_dir=$(mktemp -d)

# Função de limpeza
cleanup() {
    echo "Removendo $tmp_dir"
    rm -rf "$tmp_dir"
}

trap cleanup EXIT

# Usa o diretório temporário
cd "$tmp_dir" || exit 1
echo "Trabalhando em $tmp_dir"
touch arquivo1.txt arquivo2.txt
# ... processamento ...

echo "Script concluído."

Note que o trap foi configurado apenas para EXIT. Se o script receber um sinal INT ou TERM, o EXIT será gerado automaticamente após o tratamento padrão do sinal (a menos que o trap também capture esses sinais). Portanto, capturar EXIT já cobre a maioria dos casos de término.

Casos de uso

Além da limpeza de temporários, o trap é útil para:

  • Restaurar configurações: Se o script altera variáveis de ambiente, diretórios ou estados, pode restaurá-los ao final.
  • Encerrar processos filhos: Em scripts que disparam subprocessos, o trap pode matá-los antes de sair.
  • Log de finalização: Registrar em um arquivo de log que o script terminou, com timestamp e status.
  • Ignorar sinais temporariamente: Para evitar interrupções durante operações críticas (ex.: escrita em arquivo).
  • Tratamento personalizado de Ctrl+C: Em vez de simplesmente sair, pode-se perguntar ao usuário se deseja realmente cancelar.

Cada sinal pode ter um tratamento diferente. Por exemplo, pode-se ignorar SIGINT durante uma operação atômica e restaurá-lo depois:

#!/bin/bash

# Ignora Ctrl+C durante operação crítica
trap '' INT
echo "Operação crítica em andamento..."
sleep 5
echo "Operação concluída."

# Restaura tratamento padrão
trap - INT
echo "Agora Ctrl+C funciona novamente."
sleep 5

Exemplos

Exemplo 1: Script de backup que remove arquivos temporários e registra log.

#!/bin/bash

log_file="/var/log/backup.log"
temp_dir=$(mktemp -d)

cleanup() {
    echo "$(date): Limpeza do backup" >> "$log_file"
    rm -rf "$temp_dir"
}

trap cleanup EXIT

# Simula backup
cp -r /dados/importantes "$temp_dir"
echo "Backup copiado."
# ... compactação ...
echo "$(date): Backup concluído" >> "$log_file"

Exemplo 2: Script interativo que pergunta antes de sair com Ctrl+C.

#!/bin/bash

handle_int() {
    echo
    read -p "Deseja realmente sair? (s/N) " resposta
    if [[ "$resposta" =~ ^[Ss]$ ]]; then
        echo "Saindo..."
        exit 0
    else
        echo "Continuando..."
    fi
}

trap handle_int INT

echo "Pressione Ctrl+C para testar."
while true; do
    sleep 1
done

Exemplo 3: Garantir que processos filhos sejam mortos ao sair.

#!/bin/bash

# Inicia um processo em segundo plano
sleep 100 &
pid=$!

cleanup() {
    echo "Matando processo $pid"
    kill $pid 2>/dev/null
    wait $pid 2>/dev/null
}

trap cleanup EXIT

echo "Processo $pid em execução. Pressione Ctrl+C para encerrar."
sleep 10
echo "Script terminou normalmente."

Boas práticas

  • Use funções para maior clareza e reutilização.
  • Sempre use aspas duplas nas variáveis para evitar problemas com espaços.
  • Em traps que capturam INT, considere redefinir o trap para o padrão após a operação crítica.
  • Teste o comportamento do script com diferentes sinais usando kill -SINAL PID.
  • Documente no script quais sinais estão sendo capturados e por quê.

Referências

Exercícios

  1. Crie um script que crie um arquivo temporário em /tmp e, ao receber EXIT, o remova. Teste com exit 0 e exit 1.

    ✓ Resposta:
    #!/bin/bash
    tmpfile="/tmp/meu_temp_$$"
    touch "$tmpfile"
    cleanup() { rm -f "$tmpfile"; echo "Arquivo removido."; }
    trap cleanup EXIT
    echo "Arquivo criado: $tmpfile"
    exit 0
  2. Escreva um script que ignore Ctrl+C durante 5 segundos (usando trap com string vazia) e depois restaure o comportamento padrão.

    ✓ Resposta:
    #!/bin/bash
    echo "Ignorando Ctrl+C por 5s..."
    trap '' INT
    sleep 5
    echo "Agora Ctrl+C funciona."
    trap - INT
    sleep 5
    echo "Fim."
  3. Crie um script que, ao receber SIGTERM, imprima "Recebi SIGTERM, limpando..." e saia com código 0.

    ✓ Resposta:
    #!/bin/bash
    cleanup() { echo "Recebi SIGTERM, limpando..."; exit 0; }
    trap cleanup TERM
    echo "PID: $$. Envie 'kill $$' para testar."
    while true; do sleep 1; done
  4. Faça um script que crie um diretório temporário com mktemp -d, execute um loop infinito e, ao ser interrompido (qualquer sinal), remova o diretório e saia.

    ✓ Resposta:
    #!/bin/bash
    tmpdir=$(mktemp -d)
    cleanup() { rm -rf "$tmpdir"; echo "Diretório $tmpdir removido."; exit; }
    trap cleanup EXIT INT TERM
    echo "Diretório criado: $tmpdir"
    while true; do sleep 1; done
  5. Escreva um script que, ao receber SIGINT, pergunte "Deseja sair? (s/N)" e só saia se o usuário responder 's' ou 'S'. Use trap.

    ✓ Resposta:
    #!/bin/bash
    handle_int() {
        echo
        read -p "Deseja sair? (s/N) " resp
        if [[ "$resp" =~ ^[Ss]$ ]]; then
            echo "Saindo..."
            exit 0
        fi
    }
    trap handle_int INT
    echo "Pressione Ctrl+C para testar."
    while true; do sleep 1; done