Volver
Módulo 7 - Manejo Avanzado AVANZADO

Manejo de Errores y Debugging

Aprende a crear scripts robustos y fáciles de depurar con técnicas profesionales

El manejo adecuado de errores y las técnicas de debugging son fundamentales para crear scripts de Bash robustos y mantenibles. Un script bien diseñado debe anticipar posibles fallos, manejarlos graciosamente y proporcionar información útil para el debugging.

Códigos de Salida

En Unix/Linux, cada comando retorna un código de salida que indica si se ejecutó exitosamente. El código 0 significa éxito, cualquier otro valor indica error.

Código Significado Uso Común
0 Éxito Operación completada correctamente
1 Error general Error de sintaxis, argumentos inválidos
2 Mal uso de comando Argumentos incorrectos o faltantes
126 Comando no ejecutable Archivo encontrado pero no ejecutable
127 Comando no encontrado PATH no contiene el comando
128+n Señal fatal n Script terminado por señal

Manejo Básico de Errores

Manejo básico - error_handling.sh
bash
#!/bin/bash

# Manejo básico de errores en Bash

# Verificar código de salida del último comando
echo "Ejecutando comando que puede fallar..."
ls /directorio_inexistente

if [ $? -eq 0 ]; then
    echo "✓ Comando ejecutado exitosamente"
else
    echo "✗ El comando falló con código: $?"
fi

echo ""

# Manera más elegante usando && y ||
echo "Usando operadores lógicos..."
ls /etc/passwd && echo "✓ Archivo encontrado" || echo "✗ Archivo no encontrado"

echo ""

# Capturar salida y código de error
echo "Capturando salida y error..."
output=$(ls /directorio_inexistente 2>&1)
exit_code=$?

if [ $exit_code -eq 0 ]; then
    echo "Salida: $output"
else
    echo "Error (código $exit_code): $output"
fi

echo ""

# Función para manejar errores
manejar_error() {
    local mensaje="$1"
    local codigo="${2:-1}"
    
    echo "ERROR: $mensaje" >&2
    exit $codigo
}

# Ejemplo de uso de función de error
echo "Verificando archivo crítico..."
if [ ! -f "/etc/hosts" ]; then
    manejar_error "Archivo /etc/hosts no encontrado" 2
fi

echo "✓ Archivo crítico encontrado"

Opciones de Shell para Manejo de Errores

Opciones de shell - strict_mode.sh
bash
#!/bin/bash

# Modo estricto de Bash para mejor manejo de errores

# set -e: Salir inmediatamente si un comando falla
# set -u: Tratar variables no definidas como errores
# set -o pipefail: Fallar si cualquier comando en un pipe falla
set -euo pipefail

# Función para mostrar información de debug
debug_info() {
    echo "DEBUG: $1" >&2
}

# Función de limpieza
cleanup() {
    debug_info "Ejecutando limpieza..."
    # Aquí iría código de limpieza
    rm -f /tmp/script_temp_* 2>/dev/null || true
}

# Trap para ejecutar limpieza al salir
trap cleanup EXIT

echo "=== SCRIPT CON MODO ESTRICTO ==="

# Esto causará error porque la variable no está definida
# echo "Variable no definida: $VARIABLE_INEXISTENTE"

# En su lugar, usar valores por defecto
VARIABLE_CON_DEFAULT="${VARIABLE_OPCIONAL:-valor_por_defecto}"
echo "Variable con default: $VARIABLE_CON_DEFAULT"

# Ejemplo de comando que puede fallar
echo "Creando archivo temporal..."
TEMP_FILE=$(mktemp /tmp/script_temp_XXXXXX)
debug_info "Archivo temporal creado: $TEMP_FILE"

echo "Escribiendo en archivo temporal..."
echo "Contenido de prueba" > "$TEMP_FILE"

# Verificar que el archivo se creó correctamente
if [ ! -s "$TEMP_FILE" ]; then
    echo "Error: No se pudo crear el archivo temporal" >&2
    exit 1
fi

echo "✓ Operaciones completadas exitosamente"

# La limpieza se ejecutará automáticamente gracias al trap

Traps Avanzados

Manejo de señales - advanced_traps.sh
bash
#!/bin/bash

# Manejo avanzado de señales y traps

# Variables globales
declare -g SCRIPT_PID=$$
declare -g CLEANUP_DONE=false
declare -g TEMP_FILES=()
declare -g CHILD_PIDS=()

# Función de logging
log() {
    local nivel="$1"
    shift
    echo "[$(date +'%Y-%m-%d %H:%M:%S')] [$nivel] $*" >&2
}

# Función de limpieza avanzada
cleanup() {
    if [ "$CLEANUP_DONE" = true ]; then
        return
    fi
    
    log "INFO" "Iniciando proceso de limpieza..."
    CLEANUP_DONE=true
    
    # Terminar procesos hijo
    if [ ${#CHILD_PIDS[@]} -gt 0 ]; then
        log "INFO" "Terminando ${#CHILD_PIDS[@]} procesos hijo..."
        for pid in "${CHILD_PIDS[@]}"; do
            if kill -0 "$pid" 2>/dev/null; then
                kill "$pid" 2>/dev/null || true
                log "INFO" "Proceso $pid terminado"
            fi
        done
    fi
    
    # Limpiar archivos temporales
    if [ ${#TEMP_FILES[@]} -gt 0 ]; then
        log "INFO" "Limpiando ${#TEMP_FILES[@]} archivos temporales..."
        for file in "${TEMP_FILES[@]}"; do
            if [ -f "$file" ]; then
                rm -f "$file"
                log "INFO" "Archivo temporal eliminado: $file"
            fi
        done
    fi
    
    log "INFO" "Limpieza completada"
}

# Manejador de señal SIGINT (Ctrl+C)
handle_sigint() {
    log "WARN" "Recibida señal SIGINT (Ctrl+C)"
    log "INFO" "Terminando script graciosamente..."
    cleanup
    exit 130
}

# Manejador de señal SIGTERM
handle_sigterm() {
    log "WARN" "Recibida señal SIGTERM"
    log "INFO" "Terminando script..."
    cleanup
    exit 143
}

# Manejador para errores
handle_error() {
    local exit_code=$?
    local line_number=$1
    log "ERROR" "Error en línea $line_number (código: $exit_code)"
    log "ERROR" "Comando: $BASH_COMMAND"
    cleanup
    exit $exit_code
}

# Configurar traps
trap 'handle_error $LINENO' ERR
trap handle_sigint SIGINT
trap handle_sigterm SIGTERM
trap cleanup EXIT

# Función para crear archivo temporal
create_temp_file() {
    local prefix="${1:-script}"
    local temp_file
    
    temp_file=$(mktemp "/tmp/${prefix}_XXXXXX")
    TEMP_FILES+=("$temp_file")
    log "INFO" "Archivo temporal creado: $temp_file"
    echo "$temp_file"
}

# Función para ejecutar proceso en background
run_background() {
    local comando="$1"
    log "INFO" "Ejecutando en background: $comando"
    
    $comando &
    local pid=$!
    CHILD_PIDS+=("$pid")
    
    log "INFO" "Proceso iniciado con PID: $pid"
    echo "$pid"
}

# Función principal
main() {
    log "INFO" "Iniciando script (PID: $SCRIPT_PID)"
    
    # Simular trabajo con archivos temporales
    log "INFO" "Creando archivos temporales de trabajo..."
    local temp1 temp2
    temp1=$(create_temp_file "data")
    temp2=$(create_temp_file "config")
    
    # Escribir contenido en archivos
    echo "Datos importantes" > "$temp1"
    echo "Configuración del sistema" > "$temp2"
    
    # Simular proceso largo en background
    log "INFO" "Iniciando proceso de análisis..."
    local analysis_pid
    analysis_pid=$(run_background "sleep 10")
    
    # Simular trabajo principal
    log "INFO" "Procesando datos principales..."
    for i in {1..5}; do
        log "INFO" "Procesando lote $i/5..."
        sleep 2
        
        # Verificar si el proceso background sigue activo
        if ! kill -0 "$analysis_pid" 2>/dev/null; then
            log "WARN" "Proceso de análisis terminó prematuramente"
        fi
    done
    
    # Esperar a que termine el proceso background
    log "INFO" "Esperando a que termine el análisis..."
    wait "$analysis_pid" 2>/dev/null || log "WARN" "Proceso de análisis no encontrado"
    
    log "INFO" "Proceso completado exitosamente"
}

# Función para mostrar uso del script
show_usage() {
    cat << EOF
Uso: $0 [OPCIONES]

OPCIONES:
    -h, --help      Mostrar esta ayuda
    -v, --verbose   Modo verbose
    -t, --test      Modo de prueba (simula errores)

Ejemplo:
    $0 --verbose
    $0 --test
EOF
}

# Procesar argumentos
while [ $# -gt 0 ]; do
    case $1 in
        -h|--help)
            show_usage
            exit 0
            ;;
        -v|--verbose)
            log "INFO" "Modo verbose activado"
            shift
            ;;
        -t|--test)
            log "WARN" "Modo de prueba: simulando error..."
            false  # Esto causará que se ejecute el trap de error
            ;;
        *)
            log "ERROR" "Opción desconocida: $1"
            show_usage
            exit 1
            ;;
    esac
done

# Ejecutar función principal
main

Técnicas de Debugging

Opciones de Debugging

  • set -x: Muestra cada comando antes de ejecutarlo
  • set -v: Muestra las líneas del script mientras se leen
  • bash -x script.sh: Ejecuta el script en modo debug
  • PS4: Personaliza el prompt de debug
Debugging avanzado - debug_techniques.sh
bash
#!/bin/bash

# Técnicas avanzadas de debugging

# Personalizar el prompt de debug
export PS4='+(${BASH_SOURCE}:${LINENO}): ${FUNCNAME[0]:+${FUNCNAME[0]}(): }'

# Variable para controlar debug
DEBUG=${DEBUG:-false}

# Función de debug
debug() {
    if [ "$DEBUG" = true ]; then
        echo "DEBUG: $*" >&2
    fi
}

# Función para debug condicional
debug_section() {
    local seccion="$1"
    shift
    
    if [ "$DEBUG" = true ]; then
        echo "=== DEBUG: $seccion ===" >&2
        set -x
        "$@"
        set +x
        echo "=== FIN DEBUG: $seccion ===" >&2
    else
        "$@"
    fi
}

# Función para mostrar variables
debug_vars() {
    if [ "$DEBUG" = true ]; then
        echo "=== VARIABLES DE DEBUG ===" >&2
        echo "PWD: $PWD" >&2
        echo "USER: ${USER:-no definido}" >&2
        echo "SHELL: $SHELL" >&2
        echo "BASH_VERSION: $BASH_VERSION" >&2
        echo "Argumentos: $*" >&2
        echo "===========================" >&2
    fi
}

# Función para tracing de funciones
trace_function() {
    local func_name="${FUNCNAME[1]}"
    local args="$*"
    
    if [ "$DEBUG" = true ]; then
        echo "TRACE: Entrando a $func_name($args)" >&2
    fi
}

# Función de ejemplo que usa tracing
procesar_archivo() {
    trace_function "$@"
    local archivo="$1"
    
    debug "Procesando archivo: $archivo"
    
    if [ ! -f "$archivo" ]; then
        debug "WARN: Archivo no encontrado: $archivo"
        return 1
    fi
    
    debug_section "LECTURA_ARCHIVO" cat "$archivo"
    
    debug "Archivo procesado exitosamente"
    return 0
}

# Función para debugging interactivo
debug_pause() {
    if [ "$DEBUG" = true ]; then
        echo "DEBUG: Pausa para inspección. Presiona Enter para continuar..." >&2
        read -r
    fi
}

# Función para mostrar stack trace
show_stack_trace() {
    local frame=0
    echo "=== STACK TRACE ===" >&2
    while caller $frame; do
        ((frame++))
    done >&2
    echo "==================" >&2
}

# Función para logging con niveles
log_with_level() {
    local nivel="$1"
    shift
    local mensaje="$*"
    local timestamp=$(date '+%Y-%m-%d %H:%M:%S')
    
    case "$nivel" in
        DEBUG)
            [ "$DEBUG" = true ] && echo "[$timestamp] DEBUG: $mensaje" >&2
            ;;
        INFO)
            echo "[$timestamp] INFO: $mensaje" >&2
            ;;
        WARN)
            echo "[$timestamp] WARN: $mensaje" >&2
            ;;
        ERROR)
            echo "[$timestamp] ERROR: $mensaje" >&2
            show_stack_trace
            ;;
    esac
}

# Función principal para demostrar técnicas
main() {
    debug_vars "$@"
    
    log_with_level "INFO" "Iniciando demostración de debugging"
    
    # Crear archivo temporal para demostración
    local temp_file="/tmp/debug_test_$$"
    echo "Contenido de prueba" > "$temp_file"
    debug "Archivo temporal creado: $temp_file"
    
    # Procesar archivo con debug
    debug_pause
    procesar_archivo "$temp_file"
    
    # Simular error para mostrar stack trace
    debug_pause
    log_with_level "ERROR" "Simulando error para mostrar stack trace"
    
    # Limpiar
    rm -f "$temp_file"
    debug "Archivo temporal eliminado"
    
    log_with_level "INFO" "Demostración completada"
}

# Verificar si DEBUG está activado via variable de entorno
if [ "${DEBUG:-false}" = true ]; then
    echo "Modo DEBUG activado via variable de entorno" >&2
    DEBUG=true
fi

# Procesar argumentos de línea de comandos
while [ $# -gt 0 ]; do
    case $1 in
        --debug)
            DEBUG=true
            echo "Modo DEBUG activado via argumento" >&2
            shift
            ;;
        --trace)
            DEBUG=true
            set -x
            echo "Modo TRACE activado" >&2
            shift
            ;;
        *)
            break
            ;;
    esac
done

# Ejecutar función principal
main "$@"

Sistema de Logging Profesional

Logging system - logger.sh
bash
#!/bin/bash

# Sistema de logging profesional para Bash

# Configuración de logging
declare -g LOG_LEVEL="${LOG_LEVEL:-INFO}"
declare -g LOG_FILE="${LOG_FILE:-}"
declare -g LOG_FORMAT="${LOG_FORMAT:-standard}"
declare -g SCRIPT_NAME="$(basename "$0")"

# Niveles de log con valores numéricos
declare -Ag LOG_LEVELS=(
    ["TRACE"]=0
    ["DEBUG"]=1
    ["INFO"]=2
    ["WARN"]=3
    ["ERROR"]=4
    ["FATAL"]=5
)

# Colores para output en terminal
declare -Ag LOG_COLORS=(
    ["TRACE"]="\033[0;37m"   # Blanco
    ["DEBUG"]="\033[0;36m"   # Cian
    ["INFO"]="\033[0;32m"    # Verde
    ["WARN"]="\033[0;33m"    # Amarillo
    ["ERROR"]="\033[0;31m"   # Rojo
    ["FATAL"]="\033[1;31m"   # Rojo brillante
    ["RESET"]="\033[0m"      # Reset
)

# Función para obtener timestamp
get_timestamp() {
    case "$LOG_FORMAT" in
        "iso")
            date -u +"%Y-%m-%dT%H:%M:%SZ"
            ;;
        "epoch")
            date +%s
            ;;
        *)
            date '+%Y-%m-%d %H:%M:%S'
            ;;
    esac
}

# Función principal de logging
log() {
    local nivel="$1"
    shift
    local mensaje="$*"
    
    # Verificar si el nivel está configurado
    if [[ ! "${LOG_LEVELS[$nivel]}" ]]; then
        echo "ERROR: Nivel de log inválido: $nivel" >&2
        return 1
    fi
    
    # Verificar si debe loggear este nivel
    local current_level_value="${LOG_LEVELS[$LOG_LEVEL]}"
    local message_level_value="${LOG_LEVELS[$nivel]}"
    
    if [ "$message_level_value" -lt "$current_level_value" ]; then
        return 0
    fi
    
    # Construir mensaje de log
    local timestamp=$(get_timestamp)
    local pid=$$
    local caller_info=""
    
    # Agregar información del caller si es DEBUG o TRACE
    if [[ "$nivel" == "DEBUG" || "$nivel" == "TRACE" ]]; then
        caller_info=" [${BASH_SOURCE[2]##*/}:${BASH_LINENO[1]}]"
    fi
    
    local log_message
    case "$LOG_FORMAT" in
        "json")
            log_message=$(printf '{"timestamp":"%s","level":"%s","script":"%s","pid":%d,"message":"%s"}' \
                "$timestamp" "$nivel" "$SCRIPT_NAME" "$pid" "$mensaje")
            ;;
        "syslog")
            log_message=$(printf "<%d>%s %s[%d]: [%s] %s" \
                "16" "$timestamp" "$SCRIPT_NAME" "$pid" "$nivel" "$mensaje")
            ;;
        *)
            log_message=$(printf "[%s] [%s] [%s:%d]%s %s" \
                "$timestamp" "$nivel" "$SCRIPT_NAME" "$pid" "$caller_info" "$mensaje")
            ;;
    esac
    
    # Output a archivo si está configurado
    if [ -n "$LOG_FILE" ]; then
        echo "$log_message" >> "$LOG_FILE"
    fi
    
    # Output a stderr con colores si es terminal
    if [ -t 2 ]; then
        local color="${LOG_COLORS[$nivel]}"
        local reset="${LOG_COLORS[RESET]}"
        echo -e "${color}${log_message}${reset}" >&2
    else
        echo "$log_message" >&2
    fi
}

# Funciones de conveniencia
log_trace() { log "TRACE" "$@"; }
log_debug() { log "DEBUG" "$@"; }
log_info()  { log "INFO" "$@"; }
log_warn()  { log "WARN" "$@"; }
log_error() { log "ERROR" "$@"; }
log_fatal() { log "FATAL" "$@"; exit 1; }

# Función para logging condicional
log_if() {
    local condicion="$1"
    local nivel="$2"
    shift 2
    
    if eval "$condicion"; then
        log "$nivel" "$@"
    fi
}

# Función para configurar logging desde argumentos
setup_logging_from_args() {
    while [ $# -gt 0 ]; do
        case $1 in
            --log-level)
                LOG_LEVEL="$2"
                shift 2
                ;;
            --log-file)
                LOG_FILE="$2"
                shift 2
                ;;
            --log-format)
                LOG_FORMAT="$2"
                shift 2
                ;;
            --verbose|-v)
                LOG_LEVEL="DEBUG"
                shift
                ;;
            --quiet|-q)
                LOG_LEVEL="ERROR"
                shift
                ;;
            *)
                break
                ;;
        esac
    done
}

# Función para rotar logs
rotate_log() {
    local max_size="${1:-1048576}"  # 1MB por defecto
    local max_files="${2:-5}"
    
    if [ -z "$LOG_FILE" ] || [ ! -f "$LOG_FILE" ]; then
        return 0
    fi
    
    local file_size=$(stat -f%z "$LOG_FILE" 2>/dev/null || stat -c%s "$LOG_FILE" 2>/dev/null)
    
    if [ "$file_size" -gt "$max_size" ]; then
        log_info "Rotando log (tamaño: ${file_size} bytes)"
        
        # Rotar archivos existentes
        for i in $(seq $((max_files-1)) -1 1); do
            if [ -f "${LOG_FILE}.$i" ]; then
                mv "${LOG_FILE}.$i" "${LOG_FILE}.$((i+1))"
            fi
        done
        
        # Mover log actual
        mv "$LOG_FILE" "${LOG_FILE}.1"
        
        # Crear nuevo log vacío
        touch "$LOG_FILE"
        log_info "Log rotado exitosamente"
    fi
}

# Ejemplo de uso del sistema de logging
demo_logging() {
    log_info "Iniciando demo del sistema de logging"
    
    log_trace "Mensaje de trace - muy detallado"
    log_debug "Mensaje de debug - información de desarrollo"
    log_info "Mensaje informativo - operación normal"
    log_warn "Mensaje de advertencia - algo puede estar mal"
    log_error "Mensaje de error - algo definitivamente está mal"
    
    # Logging condicional
    local archivo="/etc/passwd"
    log_if "[ -f '$archivo' ]" "INFO" "Archivo $archivo existe"
    log_if "[ ! -f '/archivo/inexistente' ]" "WARN" "Archivo inexistente no encontrado"
    
    log_info "Demo completado"
}

# Configuración inicial si se ejecuta directamente
if [ "${BASH_SOURCE[0]}" = "${0}" ]; then
    # Configurar logging desde argumentos
    setup_logging_from_args "$@"
    
    # Configurar archivo de log si no está definido
    if [ -z "$LOG_FILE" ]; then
        LOG_FILE="/tmp/${SCRIPT_NAME%.*}.log"
    fi
    
    # Ejecutar demo
    demo_logging
    
    echo ""
    echo "Log guardado en: $LOG_FILE"
    echo "Configuración actual:"
    echo "  LOG_LEVEL: $LOG_LEVEL"
    echo "  LOG_FORMAT: $LOG_FORMAT"
fi

Mejores Prácticas

  • Usa set -euo pipefail: Para detectar errores temprano
  • Implementa traps: Para limpieza automática de recursos
  • Logging estructurado: Usa niveles de log consistentes
  • Códigos de salida consistentes: Define significados específicos para cada código
  • Información de contexto: Incluye números de línea y función en errores
  • Debugging granular: Permite activar/desactivar debug por secciones

Cuidados en Producción

  • Logs sensibles: No logues contraseñas o información confidencial
  • Tamaño de logs: Implementa rotación automática de logs
  • Performance: El logging excesivo puede impactar rendimiento
  • Espacio en disco: Monitorea el espacio usado por logs
  • Permisos: Asegúrate de que solo usuarios autorizados puedan leer logs

Ejercicios Prácticos

Ejercicio 1: Sistema de Backup Robusto

Crea un script de backup que:

  • Use set -euo pipefail para manejo estricto de errores
  • Implemente traps para limpieza automática
  • Incluya logging con diferentes niveles
  • Manaje códigos de salida específicos para diferentes tipos de error
  • Proporcione información útil para debugging

Ejercicio 2: Monitor de Sistema

Desarrolla un script de monitoreo que:

  • Capture métricas del sistema con manejo de errores
  • Implemente un sistema de alertas basado en logs
  • Use debugging condicional para troubleshooting
  • Maneje señales para terminación graceful
  • Incluya rotación automática de logs