bash trap: scripts que no mueren dejando basura
El script que me hizo aprender trap
Tenía un script que procesaba archivos grandes. Algo así:
#!/usr/bin/env bash
tempdir=$(mktemp -d)
cd "$tempdir"
wget https://datos.example.com/grande.csv
procesar.sh grande.csv > resultado.csv
scp resultado.csv user@server:/datos/
cd /
rm -rf "$tempdir"
Funcionaba. Hasta que un día wget falló por timeout. El script terminó sin limpiar $tempdir. Otro día lo cancelé con Ctrl+C. Mismo problema.
Dos semanas después, /tmp tenía 14 GB en directorios huérfanos. trap me habría ahorrado el susto.
Qué hace trap
trap captura señales y ejecuta código antes de que el script muera:
trap 'limpiar_y_salir' EXIT
Cuando el script termina — normal, con error, o con Ctrl+C — ejecuta limpiar_y_salir. No importa cómo muera.
El script corregido
#!/usr/bin/env bash
set -euo pipefail
# ── Limpieza ──────────────────────────────
cleanup() {
local exit_code=$?
echo "Limpiando..."
if [[ -n "${tempdir:-}" ]] && [[ -d "$tempdir" ]]; then
rm -rf "$tempdir"
echo " Directorio temporal eliminado: $tempdir"
fi
exit $exit_code
}
trap cleanup EXIT
# ── Señales comunes ───────────────────────
trap 'echo "Interrumpido (SIGINT)"; exit 130' INT
trap 'echo "Terminado (SIGTERM)"; exit 143' TERM
# ── Trabajo real ──────────────────────────
tempdir=$(mktemp -d)
echo "Trabajando en: $tempdir"
cd "$tempdir"
wget https://datos.example.com/grande.csv
procesar.sh grande.csv > resultado.csv
scp resultado.csv user@server:/datos/
echo "Completado exitosamente."
¿Qué pasa ahora en cada escenario?
| Evento | Qué hace |
|---|---|
| Script termina bien | cleanup borra el tempdir y sale con 0 |
wget falla | set -e detiene el script, cleanup se dispara, borra tempdir |
| Ctrl+C | trap INT captura SIGINT, sale con 130, cleanup se dispara |
kill externo | trap TERM captura SIGTERM, sale con 143, cleanup se dispara |
En todos los casos, el directorio temporal se borra. Sin trap, solo se borraba si el script terminaba sin errores (el caso feliz, que no es el frecuente).
Señales que importan
trap cleanup EXIT # Siempre. Error, éxito, Ctrl+C, kill.
trap cleanup INT # Ctrl+C
trap cleanup TERM # kill (sin -9)
trap cleanup HUP # Terminal cerrada
IMPORTANTE: SIGKILL (kill -9) no se puede capturar. Si alguien te tira un kill -9, el script muere sin limpiar. No hay nada que hacer. Pero fuera de eso, todo lo demás es capturable.
Patrón que repito en todos mis scripts
#!/usr/bin/env bash
set -euo pipefail
cleanup() {
local exit_code=$?
# Cerrar conexiones, borrar temporales, desmontar, notificar...
echo "[$(date)] Script terminado con código $exit_code"
exit $exit_code
}
trap cleanup EXIT INT TERM
# El resto del script...
Tres líneas. Las pego al inicio de cualquier script que toque el sistema de archivos o la red. No cuesta nada y evita dolores de cabeza.
Ejemplo real: script que monta una unidad cifrada
#!/usr/bin/env bash
set -euo pipefail
MOUNT_POINT="/mnt/cifrado"
cleanup() {
local exit_code=$?
if mountpoint -q "$MOUNT_POINT" 2>/dev/null; then
echo "Desmontando $MOUNT_POINT..."
umount "$MOUNT_POINT"
fi
if [[ -n "${mapper:-}" ]] && [[ -e "/dev/mapper/$mapper" ]]; then
echo "Cerrando volumen cifrado..."
cryptsetup close "$mapper"
fi
exit $exit_code
}
trap cleanup EXIT INT TERM
mapper="backup_$(date +%Y%m%d)"
cryptsetup open /dev/sdb1 "$mapper" --key-file /etc/backup.key
mount "/dev/mapper/$mapper" "$MOUNT_POINT"
# ... hacer el backup ...
echo "Backup completado. Limpiando..."
Si falla a la mitad, si cancelás, si se cae la terminal: el volumen se desmonta y se cierra. No queda información cifrada colgando en /dev/mapper.
trap son 3 líneas. Pegalas al inicio de cualquier script que cree archivos temporales, monte unidades o abra conexiones. Después me lo agradecés.