Scripting en Bash: Una Inmersión Profunda en Códigos de Salida y Estado

Comprende los códigos de salida de Bash, inspecciona $? de forma segura, establece estados con exit y construye un flujo de control confiable.

Scripting en Bash: Una Inmersión Profunda en Códigos de Salida y Estado

Los códigos de salida de Bash son la forma en que los comandos le indican a tu script lo que sucedió. 0 significa éxito, y un estado distinto de cero indica que el comando falló o produjo un resultado que tu script necesita manejar.

Esta guía te muestra cómo leer $?, establecer estados con exit y usar códigos de salida para construir un flujo de control más seguro en la automatización con Bash.

Entendiendo los Códigos de Salida

Cada comando, función o script ejecutado en Bash devuelve un código de salida al finalizar. Este es un valor entero que indica el resultado de la ejecución. Por convención:

  • 0 (Cero): Indica éxito. El comando se completó sin errores.
  • Distinto de cero (Cualquier otro entero): Indica fallo o un error. Diferentes valores distintos de cero pueden a veces significar tipos específicos de errores.

Esta simple convención de 0 vs. distinto de cero es fundamental para cómo opera Bash y cómo puedes construir lógica condicional en tus scripts.

Recuperando el Último Código de Salida: $?

Bash proporciona un parámetro especial, $?, que contiene el código de salida del comando en primer plano ejecutado más recientemente. Puedes verificar su valor inmediatamente después de cualquier comando para determinar su resultado.

# Ejemplo 1: Comando exitoso
ls /tmp
echo "Código de salida para 'ls /tmp': $?"

# Ejemplo 2: Comando fallido (directorio inexistente)
ls /directorio_inexistente
echo "Código de salida para 'ls /directorio_inexistente': $?"

# Ejemplo 3: Grep encontrando una coincidencia (éxito)
grep "root" /etc/passwd
echo "Código de salida para 'grep root /etc/passwd': $?"

# Ejemplo 4: Grep no encontrando una coincidencia (fallo, pero esperado)
grep "usuario_inexistente" /etc/passwd
echo "Código de salida para 'grep usuario_inexistente /etc/passwd': $?"

Salida (puede variar ligeramente según tu sistema y el contenido de /etc/passwd):

ls /tmp
# ... (lista de archivos en /tmp)
Código de salida para 'ls /tmp': 0
ls /directorio_inexistente
ls: no se puede acceder a '/directorio_inexistente': No existe el archivo o el directorio
Código de salida para 'ls /directorio_inexistente': 2
grep "root" /etc/passwd
root:x:0:0:root:/root:/bin/bash
Código de salida para 'grep root /etc/passwd': 0
grep "usuario_inexistente" /etc/passwd
Código de salida para 'grep usuario_inexistente /etc/passwd': 1

Observa que grep devuelve 0 para una coincidencia y 1 para ninguna coincidencia. Ambos son resultados válidos en el contexto de grep, pero para la lógica condicional, 0 significa la búsqueda exitosa del patrón.

Estableciendo Códigos de Salida Explícitamente con exit

Al escribir tus propios scripts o funciones, puedes establecer explícitamente su código de salida usando el comando exit seguido de un valor entero. Esto es crucial para comunicar el resultado del script a procesos llamantes, scripts padre o pipelines de CI/CD.

#!/bin/bash

# script_exitoso.sh
echo "Este script saldrá con éxito (0)"
exit 0
#!/bin/bash

# script_fallido.sh
echo "Este script saldrá con fallo (1)"
exit 1
# Probar los scripts
./script_exitoso.sh
echo "Estado de script_exitoso.sh: $?"

./script_fallido.sh
echo "Estado de script_fallido.sh: $?"

Salida:

Este script saldrá con éxito (0)
Estado de script_exitoso.sh: 0
Este script saldrá con fallo (1)
Estado de script_fallido.sh: 1

Consejo: Si se llama a exit sin un argumento, el estado de salida del script será el estado de salida del último comando ejecutado antes de que se llamara a exit.

Aprovechando los Códigos de Salida para el Flujo de Control

Los códigos de salida son la base de la ejecución condicional en Bash, permitiéndote crear scripts dinámicos y receptivos.

Sentencias Condicionales (if/else)

La sentencia if en Bash evalúa el código de salida de un comando. Si el comando sale con 0 (éxito), se ejecuta el bloque if. De lo contrario, se ejecuta el bloque else (si está presente).

#!/bin/bash

ARCHIVO="/ruta/a/mi/archivo_importante.txt"

if [ -f "$ARCHIVO" ]; then # El comando de prueba `[` sale con 0 si el archivo existe
    echo "El archivo '$ARCHIVO' existe. Procediendo con el procesamiento..."
    # Agregar lógica de procesamiento de archivos aquí
    # Ejemplo: cat "$ARCHIVO"
    exit 0
else
    echo "Error: El archivo '$ARCHIVO' no existe."
    echo "Abortando script."
    exit 1
fi

Operadores Lógicos (&&, ||)

Bash proporciona poderosos operadores lógicos de cortocircuito que dependen de los códigos de salida:

  • comando1 && comando2: comando2 se ejecuta solo si comando1 sale con 0 (éxito).
  • comando1 || comando2: comando2 se ejecuta solo si comando1 sale con un valor distinto de cero (fallo).

Estos son extremadamente útiles para comandos secuenciales y mecanismos de respaldo.

#!/bin/bash

DIR_LOG="/var/log/mi_app"

# Crear directorio solo si no existe
mkdir -p "$DIR_LOG" && echo "Directorio de logs '$DIR_LOG' asegurado."

# Intentar iniciar un servicio, si falla, intentar un comando de respaldo
systemctl start mi_servicio || { echo "Fallo al iniciar mi_servicio. Intentando respaldo..."; ./iniciar_respaldo.sh; }

# Un comando que debe tener éxito para que el script continúe
copiar_datos_a_ubicacion_respaldo && echo "Copia de seguridad de datos exitosa." || { echo "¡Copia de seguridad de datos fallida!"; exit 1; }

echo "Script completado exitosamente."
exit 0

set -e: Salir ante Error

La opción set -e es una herramienta poderosa para hacer tus scripts más robustos. Cuando set -e está activo, Bash saldrá inmediatamente del script si algún comando sale con un estado distinto de cero. Esto previene fallos silenciosos y errores en cascada.

#!/bin/bash
set -e # Salir inmediatamente si un comando sale con un estado distinto de cero

echo "Iniciando script..."

# Este comando tendrá éxito
ls /tmp

echo "Primer comando exitoso."

# Este comando fallará, y debido a 'set -e', el script saldrá aquí
ls /ruta_inexistente

echo "Esta línea nunca se alcanzará si el comando anterior falló."

exit 0 # Esta línea solo se alcanzará si todos los comandos anteriores tuvieron éxito

Salida (si /ruta_inexistente no existe):

Iniciando script...
# ... (salida de ls /tmp)
Primer comando exitoso.
ls: no se puede acceder a '/ruta_inexistente': No existe el archivo o el directorio

El script termina después del comando ls fallido, y el mensaje "Esta línea nunca se alcanzará" no se imprime.

Advertencia: set -e tiene excepciones, y algunos comandos devuelven legítimamente un valor distinto de cero para resultados esperados. Por ejemplo, grep devuelve 1 cuando no encuentra una coincidencia. Prefiere un if grep -q "patrón" archivo; then ... fi explícito cuando te importe el resultado.

Escenarios Comunes de Códigos de Salida y Mejores Prácticas

Aunque 0 para éxito y distinto de cero para fallo es la regla general, algunos códigos distintos de cero tienen significados comunes, especialmente para comandos del sistema y built-ins:

  • 0: Éxito.
  • 1: Error general, catchall para problemas diversos.
  • 2: Mal uso de built-ins del shell o argumentos de comando incorrectos.
  • 126: El comando invocado no se puede ejecutar (ej., problema de permisos, no es un ejecutable).
  • 127: Comando no encontrado (ej., error tipográfico en el nombre del comando, no está en PATH).
  • 128 + N: El comando fue terminado por la señal N. Por ejemplo, 130 (128 + 2) significa que el comando fue terminado por SIGINT (Ctrl+C).

Al crear tus propios scripts, usa 0 para éxito. Para fallos, 1 es un valor predeterminado seguro para un error general. Si tu script maneja múltiples condiciones de error distintas, puedes usar valores más altos distintos de cero (ej., 10, 20, 30) para diferenciarlos, pero documenta estos códigos personalizados claramente.

Mejores Prácticas para Scripting Robusto:

  1. Siempre Verifica Comandos Críticos: No asumas éxito. Usa sentencias if o && para verificar pasos críticos.
  2. Proporciona Mensajes de Error Informativos: Cuando un script falla, imprime mensajes claros a stderr explicando qué salió mal y cómo solucionarlo potencialmente. Usa >&2 para redirigir la salida al error estándar.
    mi_comando || { echo "Error: mi_comando falló. Revisa los logs." >&2; exit 1; }
    
  3. Limpieza ante Fallos: Usa trap para asegurar que los archivos temporales o recursos se limpien incluso si el script sale prematuramente.
    limpiar() {
        echo "Limpiando archivos temporales..."
        rm -f /tmp/mi_archivo_temp_$$
    }
    trap limpiar EXIT
    
  4. Valida Entradas: Verifica los argumentos del script o las variables de entorno temprano y sal con un error informativo si son inválidos.
  5. Registra el Estado de Salida: Para automatización compleja, registra el estado de salida de las operaciones clave para auditoría y depuración.

Ejemplo del Mundo Real: Un Fragmento de Script de Respaldo Robusto

Así es como podrías combinar estos conceptos en un escenario práctico:

#!/bin/bash
set -e # Salir inmediatamente si un comando sale con un estado distinto de cero

ORIGEN_RESPALDO="/data/app/config"
DESTINO_RESPALDO="/mnt/backup/configs"
MARCA_TIEMPO=$(date +%Y%m%d%H%M%S)
ARCHIVO_LOG="/var/log/backup_config_${MARCA_TIEMPO}.log"

# --- Funciones ---
registrar_mensaje() {
    echo "$(date +%Y-%m-%d_%H:%M:%S) - $1" | tee -a "$ARCHIVO_LOG"
}

limpiar() {
    registrar_mensaje "Limpieza iniciada."
    if [ -n "${DIR_TEMP:-}" ] && [ -d "$DIR_TEMP" ]; then
        rm -rf "$DIR_TEMP"
        registrar_mensaje "Directorio temporal eliminado: $DIR_TEMP"
    fi
}

# --- Trap para salida y señales ---
trap 'limpiar' EXIT
trap 'registrar_mensaje "Script interrumpido (SIGINT). Saliendo."; exit 130' INT
trap 'registrar_mensaje "Script terminado (SIGTERM). Saliendo."; exit 143' TERM

# --- Lógica Principal del Script ---
registrar_mensaje "Iniciando respaldo de configuración."

# 1. Verificar si el directorio de origen existe
if [ ! -d "$ORIGEN_RESPALDO" ]; then
    registrar_mensaje "Error: El origen de respaldo '$ORIGEN_RESPALDO' no existe." >&2
    exit 2 # Código de error personalizado para origen inválido
fi

# 2. Asegurar que el destino de respaldo existe
mkdir -p "$DESTINO_RESPALDO" || {
    registrar_mensaje "Error: Fallo al crear/asegurar el destino de respaldo '$DESTINO_RESPALDO'." >&2
    exit 3 # Código de error personalizado para problema de destino
}

# 3. Crear un directorio temporal para la compresión
DIR_TEMP=$(mktemp -d)
registrar_mensaje "Directorio temporal creado: $DIR_TEMP"

# 4. Copiar datos al directorio temporal
cp -r "$ORIGEN_RESPALDO" "$DIR_TEMP/" || {
    registrar_mensaje "Error: Fallo al copiar datos de '$ORIGEN_RESPALDO' a '$DIR_TEMP'." >&2
    exit 4 # Código de error personalizado para fallo de copia
}
registrar_mensaje "Datos copiados a ubicación temporal."

# 5. Comprimir los datos
NOMBRE_ARCHIVO="config_backup_${MARCA_TIEMPO}.tar.gz"
tar -czf "$DIR_TEMP/$NOMBRE_ARCHIVO" -C "$DIR_TEMP" "$(basename "$ORIGEN_RESPALDO")" || {
    registrar_mensaje "Error: Fallo al comprimir datos." >&2
    exit 5 # Código de error personalizado para fallo de compresión
}
registrar_mensaje "Datos comprimidos en $NOMBRE_ARCHIVO."

# 6. Mover el archivo al destino final
mv "$DIR_TEMP/$NOMBRE_ARCHIVO" "$DESTINO_RESPALDO/" || {
    registrar_mensaje "Error: Fallo al mover el archivo a '$DESTINO_RESPALDO'." >&2
    exit 6 # Código de error personalizado para fallo de movimiento
}
registrar_mensaje "Archivo movido a '$DESTINO_RESPALDO/$NOMBRE_ARCHIVO'."

registrar_mensaje "¡Respaldo completado exitosamente!"
exit 0

Conclusión

Trata los códigos de salida como parte de la interfaz de tu script. Verifica comandos críticos, devuelve estados distintos de cero claros en caso de fallo y documenta cualquier código personalizado que otro script o trabajo de CI pueda necesitar interpretar.