Comprendiendo los Códigos de Salida: Manejo Efectivo de Errores con $? y exit

Usa los códigos de salida de Bash, $?, exit, set -e y pipefail para que los fallos de los scripts sean claros y controlados.

Comprendiendo los Códigos de Salida: Manejo Efectivo de Errores con $? y exit

Cuando un script de Bash falla, el código de salida le dice al llamante qué hacer a continuación: continuar, reintentar, alertar o detenerse. Entender los códigos de salida, $? y exit es la diferencia entre una automatización que oculta fallos y una que los reporta claramente.

Esta guía muestra cómo Bash rastrea el estado de los comandos y cómo puedes usar ese estado para un manejo de errores simple y confiable.

El Concepto de los Estados de Salida

Cada comando o programa ejecutado en un entorno de shell tipo Unix—ya sea un comando integrado como cd, una utilidad externa como grep, u otro script de shell—devuelve un valor entero al finalizar. Este entero es el código de salida, que señala el resultado de la operación al proceso llamante.

La Convención Estándar

La convención para los códigos de salida es universalmente reconocida:

  • 0 (Cero): Significa éxito. El comando se ejecutó exactamente como se esperaba y no ocurrieron errores.
  • 1 a 255: Significan fallo o condiciones de error específicas. Estos valores distintos de cero indican que algo salió mal. Los números más altos a menudo corresponden a tipos específicos de errores (por ejemplo, archivo no encontrado, permiso denegado, error de sintaxis), aunque el significado exacto depende del programa específico.

Nota sobre el Rango: Aunque los códigos de salida son técnicamente un valor de 8 bits (0-255), los scripts de shell generalmente solo se preocupan por 0 para éxito y distinto de cero para fallo. Los códigos de salida mayores de 255 suelen ser truncados o interpretados módulo 256 por el shell.

Inspeccionando el Último Código de Salida: La Variable $?

La variable especial del shell $? (signo de dólar y signo de interrogación) es central para monitorear el estado de los comandos. Inmediatamente después de que cualquier comando se ejecuta, el shell almacena su código de salida en $?.

Cómo Usar $?

Debes verificar $? inmediatamente después del comando que te interesa, ya que cualquier comando subsiguiente (incluso mostrar la variable) sobrescribirá su valor.

Ejemplo 1: Verificando Éxito y Fallo

# 1. Un comando exitoso
echo "Prueba de éxito" > /dev/null
echo "Código de salida para éxito: $?"

# 2. Un comando fallido (por ejemplo, intentar listar un archivo inexistente)
ls /ruta/no/existente
echo "Código de salida para fallo: $?"

Salida Esperada:

Código de salida para éxito: 0
ls: no se puede acceder a '/ruta/no/existente': No such file or directory
Código de salida para fallo: 2

Implementando Verificación Condicional de Errores

Simplemente conocer el código de salida no es suficiente; el poder viene de usar esta información para controlar el flujo del script. Esto se hace típicamente con declaraciones if u operadores de cortocircuito (&& y ||).

Usando Declaraciones if

Esta es la forma más explícita de manejar errores:

if grep -q "datos importantes" archivo_log.txt;
then
    echo "Datos encontrados exitosamente."
else
    ULTIMO_ESTADO=$?
    echo "Error: Grep falló con estado $ULTIMO_ESTADO. Datos no encontrados."
    # Considera salir aquí si el script no puede continuar
fi

En el ejemplo anterior, grep -q suprime la salida (-q) y devuelve 0 solo si se encuentra una coincidencia. La estructura if verifica el estado de salida automáticamente, pero capturar explícitamente $? dentro del bloque else es útil para un registro detallado.

Usando Lógica de Cortocircuito (&& y ||)

Para verificaciones secuenciales simples, los operadores de cortocircuito proporcionan un manejo de errores conciso:

  • && (Y): El comando después de && solo se ejecuta si el comando anterior tuvo éxito (devolvió 0).
  • || (O): El comando después de || solo se ejecuta si el comando anterior falló (devolvió distinto de cero).

Ejemplo 2: Manejo de Errores Conciso

# 1. Solo ejecuta 'procesar_datos' SI 'obtener_datos' tiene éxito
obtener_datos.sh && ./procesar_datos.sh

# 2. Ejecuta 'enviar_alerta' SOLO SI la operación principal falla
rsync -a origen/ destino/ || echo "RSync falló en $(date)" >> /var/log/errores_rsync.log

Controlando la Terminación del Script con exit

El comando exit se usa para terminar inmediatamente el script de shell actual o función y devolver un estado de salida especificado al llamante (que podría ser otro script o la terminal del usuario).

Sintaxis y Uso

La sintaxis es simplemente exit [código_de_estado].

Si no se proporciona un estado, exit toma por defecto el estado del comando en primer plano ejecutado más recientemente. Si llamas explícitamente a exit 0 sin ejecutar ningún comando primero, devuelve 0.

Ejemplo 3: Saliendo por Fallo en Precondición

Este script asegura que exista un archivo de configuración requerido antes de continuar.

ARCHIVO_CONFIG="/etc/app/config.conf"

if [[ ! -f "$ARCHIVO_CONFIG" ]]; then
    echo "Error: Archivo de configuración no encontrado en $ARCHIVO_CONFIG."
    # Termina el script inmediatamente con un código de error específico (por ejemplo, 20)
    exit 20 
fi

echo "Configuración cargada. Continuando script..."
# ... resto del script
exit 0

Mejor Práctica: Usando Códigos de Salida Significativos

Aunque 0 y 1 cubren la mayoría de los casos básicos, usar diferentes códigos distintos de cero ayuda al script llamante a diagnosticar el problema exacto:

Código Significado (Ejemplo)
0 Éxito
1 Error general de captura total
2-10 Errores de sintaxis, problemas de análisis de argumentos
20 Requisito previo faltante (por ejemplo, archivo no encontrado)
30 Problema de permiso

Haciendo que los Scripts Falle Rápidamente: El Comando set

Para máxima confiabilidad en scripts complejos, es una fuerte mejor práctica habilitar la verificación de errores globalmente usando las opciones del comando set al inicio de tu script:

#!/bin/bash

# Sale inmediatamente si un comando sale con un estado distinto de cero.
set -e

# Trata las variables no definidas como un error al sustituir.
set -u

# Pipefail: Asegura que el estado de retorno de una tubería sea el estado del comando más a la derecha que salió con un estado distinto de cero.
set -o pipefail

# (Opcional pero útil) Imprime los comandos a medida que se ejecutan para depuración
# set -x 

# Si algún comando a continuación falla, el script se detiene inmediatamente.
ls /ruta/valida && grep patron archivo.txt && ./siguiente_paso.sh

# La siguiente línea SOLO se ejecutará si todos los comandos anteriores tuvieron éxito.
echo "Todos los pasos completados."

Cuando set -e está activo, muchos estados distintos de cero no manejados detienen el script antes de que comandos posteriores se ejecuten con suposiciones incorrectas. Tiene excepciones en condicionales, tuberías y comandos compuestos, por lo que aún debes manejar explícitamente los fallos esperados.

Por ejemplo, grep devuelve 1 cuando no encuentra una coincidencia. Eso puede ser un resultado normal, no un error fatal:

if grep -q "LISTO" estado.txt; then
    echo "El servicio está listo."
else
    echo "El servicio aún no está listo."
fi

Conclusión

Verifica los comandos críticos donde se ejecutan, escribe errores a stderr y sal con un estado distinto de cero cuando el script no pueda continuar de manera segura. Usa set -euo pipefail para scripts de fallo rápido, pero no confíes en ello como tu única estrategia de manejo de errores.