trap - Manipulação de Sinais e Limpeza no Bash

trap - Manipulação de Sinais e Limpeza no Bash

O Que Você Vai Aprender

  • Como usar trap para capturar sinais e garantir que a limpeza seja executada
  • Como prevenir o bug clássico onde Ctrl-C deixa arquivos temporários para trás
  • Um template pronto para produção combinando o pseudo-sinal EXIT, uma função cleanup e set -e

Resumo Rápido

  • Canalize toda limpeza em um único trap cleanup EXIT (ele roda não importa como o script termine)
  • Crie arquivos temporários com mktemp, depois defina o trap imediatamente
  • SIGKILL (9) e SIGSTOP não podem ser capturados. Trate tudo o mais.

Pré-requisitos

  • Shell: bash (EXIT / INT / TERM também funcionam em POSIX sh)
  • Público: usuários intermediários escrevendo scripts shell

O Que É o trap?

Conclusão: trap é um builtin do bash que registra um comando para executar quando um sinal ou evento especial ocorre. É como você agenda limpeza.

trap registra um manipulador de interrupção: "quando este sinal chegar, execute este comando." A sintaxe básica é:

trap 'comando' SINAL...

Por exemplo, capturando Ctrl-C (SIGINT) para imprimir uma mensagem:

trap 'echo "Interrupted"' INT

Um sinal pode ser nomeado SIGINT, INT, ou dado por número 2. O prefixo SIG é opcional, e para portabilidade o nome simples (INT / TERM / EXIT) é preferido.

O primeiro argumento é uma string que o shell avalia. Aspas simples adiam a expansão até o sinal realmente chegar. Aspas duplas expandem variáveis quando o trap é definido, então seja deliberado sobre aspas ao embutir variáveis de limpeza.

Por Que Usar o Pseudo-sinal EXIT?

Conclusão: EXIT é um pseudo-sinal que dispara no instante em que um script termina, seja em sucesso, erro ou Ctrl-C. Canalizar a limpeza aqui é a abordagem mais robusta.

Capturar sinais reais (INT / TERM) um por um arrisca perder um e pular a limpeza. EXIT reage ao evento "script está terminando" em vez de um sinal, então ele roda exatamente uma vez independentemente de como o script termine.

#!/usr/bin/env bash
tmpfile=$(mktemp)
trap 'rm -f "$tmpfile"' EXIT

# O que quer que aconteca depois (saida normal, falha com set -e, Ctrl-C),
# tmpfile e sempre removido quando o script termina.
echo "working..." > "$tmpfile"
cat "$tmpfile"

O trap EXIT não roda com SIGKILL (kill -9). SIGKILL encerra o processo imediatamente, sem dar chance de executar um manipulador. Entenda isso como um limite rigido do trap.

Como Você Escreve uma Função de Cleanup?

Conclusão: Colete a limpeza em uma função cleanup e registre-a com trap cleanup EXIT. Mesmo com muitos recursos temporários, você gerencia a desmontagem em um único lugar.

Uma string de comando inline é suficiente para trabalho curto, mas a legibilidade sofre conforme a lista de coisas para remover cresce. Extrair uma função é a prática padrão.

#!/usr/bin/env bash
set -euo pipefail

workdir=$(mktemp -d)
lockfile="/tmp/myjob.lock"

cleanup() {
  local rc=$?          # salvar o codigo de saida anterior
  rm -rf "$workdir"
  rm -f "$lockfile"
  echo "cleanup done (exit=$rc)"
  exit "$rc"           # sair com o codigo original
}
trap cleanup EXIT

# --- trabalho principal ---
touch "$lockfile"
echo "data" > "$workdir/output.txt"

A linha chave é local rc=$? no topo da função. Executar outros comandos dentro de cleanup sobrescreve $?, então você salva o código de saída primeiro e o restaura com exit "$rc". Isso significa "limpe, mas ainda reporte o erro real para quem chamou."

Criar um diretório de trabalho com mktemp -d e removê-lo com rm -rf "$workdir" e mais seguro do que rastrear arquivos temporários individuais. Um workdir vazio tornaria rm -rf perigoso, então set -u (erro em variáveis indefinidas) previne esse problema.

Como Você Trata Múltiplos Sinais?

Conclusão: Canalize a limpeza em EXIT, e só capture INT / TERM individualmente quando precisar de comportamento de interrupção personalizado. Alguns sinais não podem ser capturados.

Aqui estão os principais sinais e seus usos.

Sinal Número Disparado por Capturável
INT 2 Ctrl-C Sim
TERM 15 kill padrão Sim
HUP 1 Desconexão do terminal Sim
QUIT 3 Ctrl-\ Sim
EXIT 0 Script termina (pseudo) Sim
KILL 9 kill -9 Não
STOP 19 Suspensão de processo Não

Imprima uma mensagem personalizada em INT enquanto deixa a desmontagem para EXIT:

#!/usr/bin/env bash
tmpfile=$(mktemp)
trap 'rm -f "$tmpfile"' EXIT
trap 'echo "Interrupted by user"; exit 130' INT

echo "Running... press Ctrl-C to interrupt"
sleep 30

Quando o manipulador INT encerra o script com exit 130, o trap EXIT então dispara e remove tmpfile. Por convenção, o código de saída para INT é 128 + 2 = 130.

SIGKILL (9) e SIGSTOP (19) não podem ser capturados, ignorados ou redefinidos por design do SO. O requisito "limpar mesmo com kill -9" é impossível com trap. Trate isso de fora, ex. um processo supervisor ou ExecStopPost= do systemd.

Como Você Combina com set -e de Forma Segura?

Conclusão: set -e (sair em erro) combina bem com trap. Mesmo se um comando falhar no meio, o trap executa a limpeza, sem deixar estado incompleto.

Com set -e, o script sai no momento em que um comando falha. O trap EXIT ainda roda, então a limpeza é garantida. Adicionar o pseudo-sinal ERR (uma extensão do bash) permite registrar qual comando falhou.

#!/usr/bin/env bash
set -euo pipefail

workdir=$(mktemp -d)
trap 'rm -rf "$workdir"' EXIT
trap 'echo "error: failed at line $LINENO (exit=$?)" >&2' ERR

cp /etc/hostname "$workdir/"
grep "nonexistent string" "$workdir/hostname"   # falha aqui -> ERR e EXIT disparam
echo "never reached"

O trap ERR reporta a falha e o trap EXIT limpa - um padrão de dois estágios que é a base de um script defensivo.

Adicionar set -o pipefail também detecta falhas no meio de um pipeline. set -euo pipefail + trap cleanup EXIT vale memorizar como o template de fortalecimento para scripts shell.

Como Você Inspeciona ou Remove um Trap?

Conclusão: Liste traps ativos com trap -p, e remova um com trap - SINAL. Ambos são úteis ao debugar.

Liste os traps atualmente definidos com trap -p.

$ trap 'rm -f /tmp/x' EXIT
$ trap -p
trap -- 'rm -f /tmp/x' EXIT

Para remover um trap para um sinal específico, defina seu comando como -.

trap - EXIT       # remover o trap EXIT (restaurar comportamento padrao)

Para ignorar um sinal (não fazer nada quando ele chegar), defina o comando como uma string vazia.

trap '' INT       # desabilitar Ctrl-C

Definir trap '' INT para ignorar é herdado pelos processos filhos. Se você só quer desabilitar Ctrl-C em parte de um script, restaure com trap - INT ao sair da seção crítica.

Resumo: o Template Seguro de trap

Conclusão: Crie recursos temporários com mktemp, defina trap cleanup EXIT imediatamente, e faça o cleanup salvar o código de saída antes de desmontar. Este é o padrão que não falha.

Copiar e colar: esqueleto de um script robusto

#!/usr/bin/env bash
set -euo pipefail

workdir=$(mktemp -d)

cleanup() {
  local rc=$?
  rm -rf "$workdir"
  exit "$rc"
}
trap cleanup EXIT
trap 'echo "interrupted" >&2; exit 130' INT

# --- trabalho principal vai aqui ---
echo "scratch" > "$workdir/tmp.txt"

Três pontos para lembrar:

  • Canalize a limpeza em EXIT (ele roda em sucesso, falha e interrupção)
  • Salve o código de saída no topo de cleanup com local rc=$?, depois exit "$rc"
  • SIGKILL / SIGSTOP não podem ser capturados. Projete com isso em mente.

Como próximo passo, tente escrever um script prático baseado em trap (gerenciamento de lock-file, um job agendado) junto com Básico do getopts ou Básico do Cron para aprofundar seu entendimento.

Próximas Leituras