Variables Locales vs Globales

Domina el alcance de variables para código más seguro

Módulo 4 ⏱️ 30-35 min 🎯 Alcance 🔒 Seguridad 📚 Intermedio

¿Qué es el Alcance de Variables?

El alcance (scope) de una variable determina dónde en el código esa variable es visible y puede ser utilizada. En Bash, las variables pueden ser globales (accesibles desde cualquier parte del script) o locales (accesibles solo dentro de la función donde se definen).

Entender el alcance de variables es crucial para escribir código seguro, predecible y libre de efectos secundarios no deseados.

Variables Globales

Accesibles desde cualquier parte del script

Variables Locales

Solo accesibles dentro de su función

Variables Globales

Por defecto, todas las variables en Bash son globales. Esto significa que cualquier variable que declares puede ser leída y modificada desde cualquier parte del script, incluyendo dentro de las funciones.

Ejemplo de variables globales Copiar
#!/bin/bash

# Variables globales
contador=0
nombre="Usuario Global"
configuracion="produccion"

# Función que modifica variables globales
incrementar_contador() {
    contador=$((contador + 1))  # Modifica la variable global
    echo "Contador incrementado a: $contador"
}

# Función que lee variables globales
mostrar_info() {
    echo "Información global:"
    echo "  Nombre: $nombre"
    echo "  Configuración: $configuracion"
    echo "  Contador: $contador"
}

# Función que modifica configuración
cambiar_modo() {
    configuracion="desarrollo"  # Modifica la variable global
    echo "Modo cambiado a: $configuracion"
}

# Programa principal
echo "=== Estado inicial ==="
mostrar_info

echo ""
echo "=== Después de incrementar ==="
incrementar_contador
incrementar_contador
mostrar_info

echo ""
echo "=== Después de cambiar modo ==="
cambiar_modo
mostrar_info
$ ./variables_globales.sh
=== Estado inicial ===
Información global:
  Nombre: Usuario Global
  Configuración: produccion
  Contador: 0

=== Después de incrementar ===
Contador incrementado a: 1
Contador incrementado a: 2
Información global:
  Nombre: Usuario Global
  Configuración: produccion
  Contador: 2

=== Después de cambiar modo ===
Modo cambiado a: desarrollo
Información global:
  Nombre: Usuario Global
  Configuración: desarrollo
  Contador: 2
Peligros de Variables Globales

Las variables globales pueden causar efectos secundarios no deseados. Si una función modifica una variable global sin que te des cuenta, puede afectar otras partes del código de manera inesperada.

Variables Locales

Las variables locales se declaran usando la palabra clave local y solo existen dentro de la función donde se definen. Esto proporciona encapsulamiento y previene efectos secundarios no deseados.

Ejemplo de variables locales Copiar
#!/bin/bash

# Variable global
nombre="Usuario Global"

# Función con variables locales
procesar_usuario() {
    local nombre="Usuario Local"      # Variable local, no afecta la global
    local edad=25                     # Solo existe en esta función
    local temp_file="/tmp/proceso.tmp" # Variable temporal local
    
    echo "Dentro de la función:"
    echo "  Nombre local: $nombre"
    echo "  Edad local: $edad"
    echo "  Archivo temporal: $temp_file"
    
    # Crear archivo temporal (solo como ejemplo)
    echo "Datos procesados" > "$temp_file"
    echo "  Archivo creado: $temp_file"
}

# Función que intenta acceder a variables locales de otra función
intentar_acceso() {
    echo "Intentando acceder a variables locales de otra función:"
    echo "  Nombre: '$nombre' (esta es la global)"
    echo "  Edad: '$edad' (esta está vacía porque no existe aquí)"
    echo "  Archivo temporal: '$temp_file' (también vacía)"
}

# Programa principal
echo "=== Variable global inicial ==="
echo "Nombre global: $nombre"

echo ""
echo "=== Ejecutando función con variables locales ==="
procesar_usuario

echo ""
echo "=== Después de ejecutar la función ==="
echo "Nombre global sigue igual: $nombre"

echo ""
echo "=== Intentando acceso desde otra función ==="
intentar_acceso

# Limpiar archivo temporal si existe
rm -f /tmp/proceso.tmp
$ ./variables_locales.sh
=== Variable global inicial ===
Nombre global: Usuario Global

=== Ejecutando función con variables locales ===
Dentro de la función:
  Nombre local: Usuario Local
  Edad local: 25
  Archivo temporal: /tmp/proceso.tmp
  Archivo creado: /tmp/proceso.tmp

=== Después de ejecutar la función ===
Nombre global sigue igual: Usuario Global

=== Intentando acceso desde otra función ===
Intentando acceder a variables locales de otra función:
  Nombre: 'Usuario Global' (esta es la global)
  Edad: '' (esta está vacía porque no existe aquí)
  Archivo temporal: '' (también vacía)

Comparación Práctica

Veamos un ejemplo lado a lado que muestra la diferencia entre usar variables locales y globales:

Comportamiento con y sin Variables Locales

SIN variables locales (problemático)
Código problemático Copiar
#!/bin/bash

# Variable global
contador=10

procesar_datos() {
    contador=0  # ¡Modifica la global sin querer!
    
    for archivo in *.txt; do
        if [ -f "$archivo" ]; then
            contador=$((contador + 1))
        fi
    done
    
    echo "Archivos procesados: $contador"
}

# Programa principal
echo "Contador inicial: $contador"
procesar_datos
echo "Contador después: $contador"  # ¡Sorpresa! Cambió
CON variables locales (seguro)
Código seguro Copiar
#!/bin/bash

# Variable global
contador=10

procesar_datos() {
    local contador=0  # Variable local, no afecta la global
    
    for archivo in *.txt; do
        if [ -f "$archivo" ]; then
            contador=$((contador + 1))
        fi
    done
    
    echo "Archivos procesados: $contador"
}

# Programa principal
echo "Contador inicial: $contador"
procesar_datos
echo "Contador después: $contador"  # Sigue igual

Casos de Uso Avanzados

1. Sombreado de Variables

Cuando una variable local tiene el mismo nombre que una global, la local "sombrea" (oculta) la global dentro de la función:

Ejemplo de sombreado de variables Copiar
#!/bin/bash

# Variables globales
debug=true
log_level="INFO"
database_url="prod://database.com"

# Función que usa sombreado para configuración temporal
ejecutar_en_desarrollo() {
    # Sombrea las variables globales con valores locales
    local debug=true
    local log_level="DEBUG"
    local database_url="dev://localhost:5432"
    
    echo "=== Configuración de desarrollo ==="
    echo "Debug: $debug"
    echo "Log level: $log_level"
    echo "Database: $database_url"
    
    # Simular operación de desarrollo
    realizar_operacion
}

ejecutar_en_produccion() {
    # Usa las variables globales directamente
    echo "=== Configuración de producción ==="
    echo "Debug: $debug"
    echo "Log level: $log_level"
    echo "Database: $database_url"
    
    # Simular operación de producción
    realizar_operacion
}

realizar_operacion() {
    echo "Realizando operación con:"
    echo "  Debug activo: $debug"
    echo "  Nivel de log: $log_level"
    echo "  Conectando a: $database_url"
}

# Mostrar configuración inicial
echo "=== Configuración global inicial ==="
echo "Debug: $debug"
echo "Log level: $log_level"
echo "Database: $database_url"

echo ""
ejecutar_en_desarrollo

echo ""
ejecutar_en_produccion

echo ""
echo "=== Configuración global después ==="
echo "Debug: $debug"
echo "Log level: $log_level"
echo "Database: $database_url"

2. Variables de Solo Lectura

Variables readonly y declare Copiar
#!/bin/bash

# Variable global de solo lectura
readonly APLICACION="MiApp"
readonly VERSION="1.0.0"

# Función que demuestra diferentes tipos de declaraciones
demo_declaraciones() {
    # Variable local normal
    local nombre="Juan"
    
    # Variable local de solo lectura
    local -r configuracion="desarrollo"
    
    # Variable local con tipo específico
    local -i numero=42  # Solo acepta enteros
    local -a lista=("item1" "item2" "item3")  # Array
    
    echo "=== Dentro de la función ==="
    echo "Aplicación: $APLICACION"
    echo "Versión: $VERSION"
    echo "Nombre: $nombre"
    echo "Configuración: $configuracion"
    echo "Número: $numero"
    echo "Lista: ${lista[*]}"
    
    # Intentar modificar variable readonly (causará error)
    echo ""
    echo "Intentando modificar variable readonly..."
    configuracion="produccion" 2>/dev/null || echo "Error: No se puede modificar variable readonly"
    
    # Modificar variable normal
    nombre="María"
    echo "Nombre modificado: $nombre"
}

demo_declaraciones

echo ""
echo "=== Fuera de la función ==="
echo "Las variables locales no existen aquí:"
echo "Nombre: '$nombre' (vacía)"
echo "Configuración: '$configuracion' (vacía)"

Mejores Prácticas

✅ Usa Siempre Local

Declara todas las variables de función como locales usando local.

mi_funcion() {
    local nombre="$1"
    local resultado=""
    # resto de la función
}
❌ Variables sin Declarar

Evita usar variables sin declararlas como locales.

mi_funcion() {
    nombre="$1"  # ¡Modifica global!
    resultado="" # ¡También global!
    # código problemático
}
✅ Constantes Readonly

Usa readonly para constantes que no deben cambiar.

readonly CONFIG_FILE="/etc/app.conf"
readonly -a VALID_ENVS=("dev" "test" "prod")
❌ Modificar Parámetros Directamente

No modifiques los parámetros $1, $2 directamente.

mi_funcion() {
    $1="nuevo_valor"  # ¡Error!
    # Usa local en su lugar
    local param1="$1"
}
✅ Nombres Descriptivos

Usa nombres claros para distinguir el propósito.

# Globales en mayúsculas
readonly APP_NAME="MiApp"
# Locales descriptivas
local input_file="$1"
local processed_count=0
❌ Nombres Ambiguos

Evita nombres genéricos que puedan causar conflictos.

# Nombres problemáticos
temp="algo"
data="datos"
i=0  # Sin local

Ejemplo Integral: Sistema de Configuración

Veamos un ejemplo completo que demuestra el uso correcto de variables locales y globales:

Sistema de configuración completo Copiar
#!/bin/bash

# ============================================================================
# VARIABLES GLOBALES (Configuración de aplicación)
# ============================================================================
readonly APP_NAME="Sistema de Gestión"
readonly APP_VERSION="2.1.0"
readonly CONFIG_DIR="/etc/miapp"

# Variables globales mutables (estado de aplicación)
DEBUG_MODE=false
LOG_LEVEL="INFO"
DATABASE_CONNECTED=false

# ============================================================================
# FUNCIONES DE CONFIGURACIÓN
# ============================================================================

# Función para cargar configuración desde archivo
cargar_configuracion() {
    local config_file="${1:-$CONFIG_DIR/app.conf}"
    local linea_numero=0
    local configs_cargadas=0
    
    if [ ! -f "$config_file" ]; then
        echo "Advertencia: Archivo de configuración no encontrado: $config_file"
        return 1
    fi
    
    echo "Cargando configuración desde: $config_file"
    
    while IFS= read -r linea || [ -n "$linea" ]; do
        ((linea_numero++))
        
        # Omitir líneas vacías y comentarios
        if [[ "$linea" =~ ^[[:space:]]*$ ]] || [[ "$linea" =~ ^[[:space:]]*# ]]; then
            continue
        fi
        
        # Procesar líneas de configuración (clave=valor)
        if [[ "$linea" =~ ^[[:space:]]*([^=]+)=[[:space:]]*(.*)$ ]]; then
            local clave="${BASH_REMATCH[1]}"
            local valor="${BASH_REMATCH[2]}"
            
            # Limpiar espacios
            clave=$(echo "$clave" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')
            valor=$(echo "$valor" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')
            
            # Aplicar configuración
            case "$clave" in
                "DEBUG_MODE")
                    if [[ "$valor" =~ ^(true|1|yes)$ ]]; then
                        DEBUG_MODE=true
                    else
                        DEBUG_MODE=false
                    fi
                    ;;
                "LOG_LEVEL")
                    LOG_LEVEL="$valor"
                    ;;
                *)
                    echo "Advertencia: Configuración desconocida en línea $linea_numero: $clave"
                    ;;
            esac
            
            ((configs_cargadas++))
        else
            echo "Advertencia: Línea malformada en línea $linea_numero: $linea"
        fi
    done < "$config_file"
    
    echo "✓ Configuración cargada: $configs_cargadas elementos"
    return 0
}

# Función para validar configuración
validar_configuracion() {
    local errores=0
    local advertencias=0
    
    echo "=== Validando configuración ==="
    
    # Validar LOG_LEVEL
    local -a niveles_validos=("DEBUG" "INFO" "WARNING" "ERROR")
    local nivel_valido=false
    
    for nivel in "${niveles_validos[@]}"; do
        if [ "$LOG_LEVEL" = "$nivel" ]; then
            nivel_valido=true
            break
        fi
    done
    
    if [ "$nivel_valido" = false ]; then
        echo "❌ Error: LOG_LEVEL inválido '$LOG_LEVEL'. Valores válidos: ${niveles_validos[*]}"
        ((errores++))
    else
        echo "✓ LOG_LEVEL válido: $LOG_LEVEL"
    fi
    
    # Validar directorios
    if [ ! -d "$CONFIG_DIR" ]; then
        echo "⚠️  Advertencia: Directorio de configuración no existe: $CONFIG_DIR"
        ((advertencias++))
    else
        echo "✓ Directorio de configuración accesible"
    fi
    
    echo ""
    echo "Resumen de validación:"
    echo "  Errores: $errores"
    echo "  Advertencias: $advertencias"
    
    return $errores
}

# Función para mostrar configuración actual
mostrar_configuracion() {
    local mostrar_sensible="${1:-false}"
    
    echo "=== Configuración de $APP_NAME v$APP_VERSION ==="
    echo ""
    echo "Configuración de aplicación:"
    echo "  Nombre: $APP_NAME"
    echo "  Versión: $APP_VERSION"
    echo "  Directorio config: $CONFIG_DIR"
    echo ""
    echo "Configuración de ejecución:"
    echo "  Modo debug: $DEBUG_MODE"
    echo "  Nivel de log: $LOG_LEVEL"
    echo "  Base de datos: $([ "$DATABASE_CONNECTED" = true ] && echo "Conectada" || echo "Desconectada")"
    
    if [ "$mostrar_sensible" = true ]; then
        echo ""
        echo "Información del sistema:"
        echo "  Usuario: $(whoami)"
        echo "  Hostname: $(hostname)"
        echo "  Directorio actual: $(pwd)"
    fi
}

# Función para conectar a base de datos (simulada)
conectar_base_datos() {
    local host="${1:-localhost}"
    local puerto="${2:-5432}"
    local timeout="${3:-10}"
    local intentos=0
    local max_intentos=3
    
    echo "Intentando conectar a base de datos..."
    echo "  Host: $host"
    echo "  Puerto: $puerto"
    echo "  Timeout: ${timeout}s"
    
    while [ $intentos -lt $max_intentos ]; do
        ((intentos++))
        echo "  Intento $intentos de $max_intentos..."
        
        # Simular conexión (en realidad sería algo como: psql -h $host -p $puerto -c "SELECT 1")
        if [ $((RANDOM % 3)) -eq 0 ]; then  # 33% de probabilidad de éxito
            DATABASE_CONNECTED=true
            echo "✓ Conexión exitosa a la base de datos"
            return 0
        else
            echo "✗ Fallo en la conexión"
            if [ $intentos -lt $max_intentos ]; then
                echo "  Esperando 2 segundos antes del siguiente intento..."
                sleep 2
            fi
        fi
    done
    
    DATABASE_CONNECTED=false
    echo "❌ No se pudo conectar a la base de datos después de $max_intentos intentos"
    return 1
}

# ============================================================================
# PROGRAMA PRINCIPAL
# ============================================================================

# Crear archivo de configuración de ejemplo
crear_config_ejemplo() {
    local config_file="/tmp/app.conf"
    
    cat > "$config_file" << 'EOF'
# Configuración de ejemplo
DEBUG_MODE=true
LOG_LEVEL=DEBUG

# Esto es un comentario
# DATABASE_HOST=localhost
EOF
    
    echo "$config_file"
}

# Función principal
main() {
    echo "=== Iniciando $APP_NAME ==="
    
    # Crear y cargar configuración de ejemplo
    local config_ejemplo
    config_ejemplo=$(crear_config_ejemplo)
    cargar_configuracion "$config_ejemplo"
    
    echo ""
    validar_configuracion
    
    if [ $? -eq 0 ]; then
        echo ""
        mostrar_configuracion true
        
        echo ""
        conectar_base_datos "localhost" "5432" "5"
    else
        echo "❌ Configuración inválida, abortando"
        return 1
    fi
    
    # Limpiar archivo de ejemplo
    rm -f "$config_ejemplo"
    
    echo ""
    echo "=== $APP_NAME iniciado correctamente ==="
}

# Ejecutar programa principal
main "$@"
Características del Ejemplo
  • Variables globales readonly: Para constantes de aplicación
  • Variables globales mutables: Para estado de aplicación
  • Variables locales: Para procesamiento interno de funciones
  • Encapsulamiento: Cada función maneja sus datos internos
  • Legibilidad: Código organizado y fácil de mantener

Ejercicio Práctico

Ejercicio: Refactorizar Código

Refactoriza el siguiente código problemático para usar correctamente variables locales:

Código a refactorizar Copiar
#!/bin/bash

# Código problemático - refactorízalo
contador=0
total=0
archivo_actual=""

procesar_directorio() {
    directorio=$1
    contador=0
    
    for archivo in "$directorio"/*; do
        archivo_actual="$archivo"
        
        if [ -f "$archivo_actual" ]; then
            tamaño=$(stat -c%s "$archivo_actual" 2>/dev/null || stat -f%z "$archivo_actual" 2>/dev/null)
            total=$((total + tamaño))
            contador=$((contador + 1))
            
            # Procesar solo archivos .txt
            if [[ "$archivo_actual" == *.txt ]]; then
                procesar_archivo_texto
            fi
        fi
    done
}

procesar_archivo_texto() {
    lineas=$(wc -l < "$archivo_actual")
    palabras=$(wc -w < "$archivo_actual")
    
    echo "Archivo: $(basename $archivo_actual)"
    echo "  Líneas: $lineas"
    echo "  Palabras: $palabras"
}

generar_reporte() {
    echo "=== REPORTE ==="
    echo "Archivos procesados: $contador"
    echo "Tamaño total: $total bytes"
    echo "Último archivo: $archivo_actual"
}

# Usar las funciones
procesar_directorio "/ruta/ejemplo"
generar_reporte
Solución Sugerida
Código refactorizado Copiar
#!/bin/bash

# Variables globales para estadísticas generales
total_archivos_procesados=0
total_tamaño_bytes=0

procesar_directorio() {
    local directorio="$1"
    local contador_local=0
    local tamaño_directorio=0
    
    # Validar parámetro
    if [ ! -d "$directorio" ]; then
        echo "Error: Directorio no existe: $directorio"
        return 1
    fi
    
    echo "Procesando directorio: $directorio"
    
    for archivo in "$directorio"/*; do
        if [ -f "$archivo" ]; then
            local tamaño_archivo
            tamaño_archivo=$(stat -c%s "$archivo" 2>/dev/null || stat -f%z "$archivo" 2>/dev/null)
            
            tamaño_directorio=$((tamaño_directorio + tamaño_archivo))
            ((contador_local++))
            
            # Procesar solo archivos .txt
            if [[ "$archivo" == *.txt ]]; then
                procesar_archivo_texto "$archivo"
            fi
        fi
    done
    
    # Actualizar estadísticas globales
    total_archivos_procesados=$((total_archivos_procesados + contador_local))
    total_tamaño_bytes=$((total_tamaño_bytes + tamaño_directorio))
    
    echo "Directorio procesado: $contador_local archivos, $tamaño_directorio bytes"
    return 0
}

procesar_archivo_texto() {
    local archivo_txt="$1"
    local lineas palabras
    
    lineas=$(wc -l < "$archivo_txt")
    palabras=$(wc -w < "$archivo_txt")
    
    echo "  Archivo texto: $(basename "$archivo_txt")"
    echo "    Líneas: $lineas"
    echo "    Palabras: $palabras"
}

generar_reporte() {
    echo ""
    echo "=== REPORTE FINAL ==="
    echo "Total archivos procesados: $total_archivos_procesados"
    echo "Tamaño total: $total_tamaño_bytes bytes"
    
    if [ $total_tamaño_bytes -gt 0 ]; then
        local promedio=$((total_tamaño_bytes / total_archivos_procesados))
        echo "Tamaño promedio por archivo: $promedio bytes"
    fi
}

# Función principal
main() {
    local directorio_trabajo="${1:-.}"
    
    echo "=== Iniciando procesamiento ==="
    
    if procesar_directorio "$directorio_trabajo"; then
        generar_reporte
    else
        echo "Error en el procesamiento"
        return 1
    fi
}

# Ejecutar con el directorio proporcionado o actual
main "$@"