Automatización Linux, scripting Bash y herramientas para sysadmins

bash trap: scripts que no mueren dejando basura

#bash#linux#scripting

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?

EventoQué hace
Script termina biencleanup borra el tempdir y sale con 0
wget fallaset -e detiene el script, cleanup se dispara, borra tempdir
Ctrl+Ctrap INT captura SIGINT, sale con 130, cleanup se dispara
kill externotrap 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.