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.
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 |
#!/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"
#!/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
#!/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
#!/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 "$@"
#!/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
Crea un script de backup que:
Desarrolla un script de monitoreo que: