Manejo de errores: exit codes, set -euo pipefail, trap, depuración

Introducción

Un script robusto detecta fallos en cuanto ocurren, limpia los recursos al salir y ofrece información de depuración útil. Las opciones set -euo pipefail, el comando trap y las herramientas de depuración de bash hacen posible todo esto con pocas líneas de código.

Exit codes — Códigos de salida

Cada comando devuelve un número entero de 0 a 255 al terminar. 0 significa éxito; cualquier otro valor indica error. Este valor se guarda en $?.


# $? contiene el código de salida del último comando
ls /etc/passwd
echo "Código: $?"     # 0

ls /fichero_inexistente 2>/dev/null
echo "Código: $?"     # 2

# Guardar $? antes de que otro comando lo sobreescriba
grep "root" /etc/passwd
RESULTADO=$?
echo "grep salió con: $RESULTADO"
          

Códigos convencionales:

  • 0 → éxito
  • 1 → error genérico
  • 2 → uso incorrecto del comando (argumento inválido)
  • 126 → el fichero existe pero no es ejecutable
  • 127 → comando no encontrado
  • 128+N → terminado por señal N (p.ej. 130 = Ctrl+C = SIGINT)

#!/usr/bin/env bash

# exit N termina el script con el código N
comprobar_root() {
    if [ "$(id -u)" -ne 0 ]; then
        echo "Error: se requieren permisos de root" >&2
        exit 1
    fi
}

comprobar_args() {
    if [ $# -lt 2 ]; then
        echo "Uso: $0 origen destino" >&2
        exit 2
    fi
}
          

set — Opciones de seguridad

La combinación canónica para scripts robustos es poner al principio del script:


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

set -e (errexit) — Salir ante cualquier error:


set -e

mkdir /tmp/test_dir
cp fichero_inexistente /tmp/test_dir/   # falla → el script se detiene aquí
echo "Esto nunca se ejecuta"

# Excepciones: comandos en if, while, until, || y && no abortan
if ! grep -q "patron" fichero.txt; then
    echo "No encontrado"
fi

comando_que_puede_fallar || true    # || true evita la salida
          

set -u (nounset) — Error al usar variables no definidas:


set -u

echo "$VARIABLE_NO_DEFINIDA"   # error: unbound variable → el script termina

# Usar valor por defecto para evitar el error con -u
echo "{$VARIABLE_NO_DEFINIDA:-valor_por_defecto}"

# Comprobar si está definida sin arriesgar
if [ -n "{$VAR+x}" ]; then
    echo "VAR está definida: $VAR"
fi
          

set -o pipefail — Propagar errores en tuberías:


# Sin pipefail: el código de salida de la tubería es el del último comando
# grep falla (no hay fichero) pero cat tiene éxito → $? = 0 (MALO)
grep "patron" fichero_inexistente | cat

# Con pipefail: si cualquier comando de la tubería falla, la tubería falla
set -o pipefail
grep "patron" fichero_inexistente | cat
echo "Código: $?"   # distinto de 0
          

set -x (xtrace) — Mostrar cada comando antes de ejecutarlo:


set -x   # activa el trazado
FICHERO="/etc/passwd"
grep "root" "$FICHERO" | wc -l
# Salida de trazado (prefijada con PS4, por defecto "+"):
# + FICHERO=/etc/passwd
# + grep root /etc/passwd
# + wc -l

set +x   # desactiva el trazado

# PS4 personalizado para mostrar línea y función
export PS4='+ {$BASH_SOURCE}:{$LINENO}: {$FUNCNAME[0]:+{$FUNCNAME[0]}(): }'
          

trap — Capturar señales y eventos

trap 'comandos' SEÑAL registra comandos que se ejecutarán cuando el script reciba una señal o llegue a ciertos eventos. Esencial para limpieza de recursos.

Señales más usadas:

  • EXIT — al terminar el script (por cualquier motivo)
  • ERR — cuando un comando falla (con set -e)
  • INT — Ctrl+C (SIGINT)
  • TERMkill por defecto (SIGTERM)
  • HUP — cierre de terminal (SIGHUP)

Trampa EXIT — limpieza garantizada:


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

TMPDIR=$(mktemp -d)

# La limpieza se ejecuta siempre: éxito, error o Ctrl+C
cleanup() {
    echo "Limpiando directorio temporal: $TMPDIR"
    rm -rf "$TMPDIR"
}
trap cleanup EXIT

echo "Trabajando en $TMPDIR..."
cp /etc/passwd "$TMPDIR/"
# ... más trabajo ...
echo "Terminado"
# cleanup() se llamará automáticamente al salir
          

Trampa ERR — registrar fallos:


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

on_error() {
    local EXIT_CODE=$?
    local LINE=$1
    echo "[ERROR] El script falló en la línea $LINE con código $EXIT_CODE" >&2
}
trap 'on_error $LINENO' ERR

echo "Paso 1"
cp fichero_inexistente /tmp/   # falla aquí
echo "Paso 2 (no se ejecuta)"
          

Trampa INT/TERM — salida limpia ante señales:


#!/usr/bin/env bash

EJECUTANDO=true

on_signal() {
    echo ""
    echo "Señal recibida. Finalizando..."
    EJECUTANDO=false
}
trap on_signal INT TERM

while $EJECUTANDO; do
    echo "Trabajando... (Ctrl+C para parar)"
    sleep 2
done

echo "Script terminado limpiamente"
          

Cancelar un trap:


trap - EXIT    # elimina el trap EXIT (vuelve al comportamiento por defecto)
trap '' INT    # ignora la señal INT (Ctrl+C no hace nada)
          

Depuración de scripts

Opciones de bash al invocar:


# -n: solo verifica la sintaxis, no ejecuta
bash -n script.sh

# -x: traza cada comando antes de ejecutarlo
bash -x script.sh

# -v: imprime cada línea del script antes de procesarla
bash -v script.sh

# Combinar: traza y solo sintaxis
bash -xn script.sh

# Pasar variables desde fuera para probar comportamientos
VAR=valor bash -x script.sh
          

Activar/desactivar traza dentro del script:


#!/usr/bin/env bash

echo "Parte normal sin traza"

set -x   # activa traza
RESULTADO=$(grep "root" /etc/passwd | wc -l)
echo "Líneas con root: $RESULTADO"
set +x   # desactiva traza

echo "Continúa sin traza"
          

Mensajes de depuración condicionales:


#!/usr/bin/env bash

# Activar con: DEBUG=1 ./script.sh
DEBUG="{$DEBUG:-0}"

debug() {
    [ "$DEBUG" = "1" ] && echo "[DEBUG] $*" >&2 || true
}

debug "Script iniciado con $# argumentos"
debug "Procesando fichero: $1"

# Uso:
# ./script.sh arg           # sin mensajes debug
# DEBUG=1 ./script.sh arg   # con mensajes debug
          

PS4 enriquecido para trazas detalladas:


# Mostrar fichero, línea y función en cada línea de traza
export PS4='+ [{$BASH_SOURCE##*/}:{$LINENO}]{$FUNCNAME[0]:+ {$FUNCNAME[0]}()}: '

set -x
mi_funcion() {
    echo "dentro"
}
mi_funcion
# Salida:
# + [script.sh:7] mi_funcion(): echo dentro
# + [script.sh:7] mi_funcion(): dentro
          

Plantilla de script robusto


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

# ── Constantes ──────────────────────────────────────────
readonly SCRIPT_NAME="{$0##*/}"
readonly SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"

# ── Funciones de log ─────────────────────────────────────
log()   { echo "[INFO]  $(date '+%H:%M:%S') $*"; }
error() { echo "[ERROR] $(date '+%H:%M:%S') $*" >&2; }

# ── Limpieza ─────────────────────────────────────────────
TMPDIR=""
cleanup() {
    [ -n "$TMPDIR" ] && rm -rf "$TMPDIR"
    log "Limpieza completada"
}
trap cleanup EXIT
trap 'error "Línea $LINENO: comando fallido"; exit 1' ERR

# ── Validación de argumentos ─────────────────────────────
usage() {
    echo "Uso: $SCRIPT_NAME [opciones] "
    echo "  -v  Modo verbose"
    echo "  -h  Mostrar esta ayuda"
    exit 0
}

VERBOSE=false
while getopts "vh" opt; do
    case $opt in
        v) VERBOSE=true ;;
        h) usage ;;
        ?) error "Opción desconocida: -$OPTARG"; exit 2 ;;
    esac
done
shift $((OPTIND - 1))

[ $# -lt 1 ] && { error "Falta argumento obligatorio"; usage; }

# ── Lógica principal ─────────────────────────────────────
TMPDIR=$(mktemp -d)
log "Iniciando $SCRIPT_NAME"

# ... código del script ...

log "Completado con éxito"
          
        

Usar siempre set -euo pipefail al inicio de todo script. Registrar un trap EXIT para limpiar ficheros temporales. Usar bash -n para verificar sintaxis antes de ejecutar.

Scripts prácticos: backup automático, monitor del sistema, limpieza de ficheros

Índice de la sección

Índice del curso