¿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.
#!/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.
#!/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)
#!/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)
#!/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:
#!/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
#!/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:
#!/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:
#!/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
#!/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 "$@"