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→ éxito1→ error genérico2→ uso incorrecto del comando (argumento inválido)126→ el fichero existe pero no es ejecutable127→ comando no encontrado128+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 (conset -e)INT— Ctrl+C (SIGINT)TERM—killpor 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"