Las funciones en Bash no son funciones en el sentido que tienen en Python o C. Son bloques de comandos con un nombre, y su mecanismo de comunicación con el exterior es el mismo que usa cualquier proceso: argumentos posicionales, variables de entorno, y códigos de salida. Entender eso desde el principio evita confusiones que persisten durante años.
Definir una función tiene dos sintaxis equivalentes: nombre() { cuerpo; } y function nombre { cuerpo; }. La primera es POSIX, la segunda es Bash-específica. Usa la primera salvo que tengas razón concreta para no hacerlo.
Dentro de la función, $1, $2, $@ y $# se refieren a los argumentos de la función, no del script. Bash reemplaza temporalmente los parámetros posicionales cuando entras en el cuerpo de la función. Esto sorprende a quien viene de otros lenguajes donde las variables globales son accesibles directamente, pero aquí el comportamiento es deliberado: hace que las funciones sean reutilizables independientemente de cómo se llame el script.
La distinción entre "$@" y "$*" es una de las más importantes del lenguaje. Sin comillas ambas se comportan igual —expansión con splitting por espacios—, pero con comillas dobles: "$@" expande cada argumento como elemento separado, preservando espacios internos; "$*" une todos los argumentos con el primer carácter de IFS (por defecto un espacio) formando una sola cadena. En la práctica, usa siempre "$@" cuando quieras pasar argumentos a otro comando o función. Usar "$*" rompe silenciosamente los argumentos que contienen espacios.
Las variables dentro de una función son globales por defecto. Si defines resultado=42 dentro de una función sin local, esa variable queda visible y modificable en el resto del script después de la llamada. Usa local para contener el scope y evitar efectos laterales que son difíciles de depurar.
return N no devuelve un valor de datos: establece el exit code de la función (0-255), accesible luego como $?. Para devolver datos reales, la función imprime por stdout y el caller captura con sustitución de comando: resultado=$(mi_funcion arg1 arg2). Es más lento porque abre un subshell, pero es el patrón correcto.
shift N consume los primeros N argumentos posicionales, desplazando los restantes: $2 pasa a ser $1, etc. Es útil cuando procesas argumentos en un bucle. getopts es la herramienta estándar para parsear flags con estilo -f valor en scripts más formales; soporta opciones con y sin argumento y maneja errores consistentemente.
Si defines una función después de intentar llamarla, Bash falla: las funciones deben estar definidas antes de su primer uso en el flujo de ejecución.
#!/usr/bin/env bash
set -euo pipefail
# --------------------------------------------------------------------------
# Procesa un conjunto de archivos de log y genera un resumen por severidad.
# Uso: ./analizar_logs.sh [-v] [-o salida.txt] archivo1.log [archivo2.log ...]
# --------------------------------------------------------------------------
# Variables globales con valores por defecto
VERBOSE=0
ARCHIVO_SALIDA=""
# ── Funciones de utilidad ──────────────────────────────────────────────────
log_verbose() {
# Solo imprime si el modo verbose está activo; no contamina stdout
[[ $VERBOSE -eq 1 ]] && echo "[DEBUG] $*" >&2
}
contar_severidad() {
local archivo="$1" # local evita pisar variables del scope global
local nivel="$2"
local count
# grep -c falla con exit code 1 si no hay coincidencias; || true lo neutraliza
count=$(grep -ic "\b${nivel}\b" "$archivo" || true)
echo "$count" # único mecanismo para "retornar" un dato
}
generar_resumen() {
# $@ aquí son los archivos, no los argumentos del script
local -a archivos=("$@") # copia la lista preservando elementos con espacios
local total_errores=0
local total_warnings=0
local archivo
for archivo in "${archivos[@]}"; do
if [[ ! -f "$archivo" ]]; then
echo "Advertencia: '$archivo' no existe, se omite." >&2
continue
fi
local errores warnings
errores=$(contar_severidad "$archivo" "error")
warnings=$(contar_severidad "$archivo" "warning")
log_verbose "Archivo: $archivo → errores=$errores, warnings=$warnings"
# Aritmética sobre variables locales; los totales sí son del scope llamante
(( total_errores += errores ))
(( total_warnings += warnings ))
printf "%-40s errores: %4d warnings: %4d\n" \
"$(basename "$archivo")" "$errores" "$warnings"
done
# Separador antes del total
printf '%s\n' "----------------------------------------"
printf "%-40s errores: %4d warnings: %4d\n" \
"TOTAL" "$total_errores" "$total_warnings"
}
# ── Parseo de opciones con getopts ─────────────────────────────────────────
parsear_opciones() {
# getopts procesa los argumentos posicionales del contexto en que se llama.
# Como estamos dentro de una función, recibe "$@" del script principal
# solo si se los pasamos explícitamente — por eso la llamada usa "$@".
local opt
while getopts ":vo:" opt; do
case "$opt" in
v)
VERBOSE=1
;;
o)
# OPTARG contiene el valor del argumento de la opción
ARCHIVO_SALIDA="$OPTARG"
;;
:)
echo "Error: la opción -${OPTARG} requiere un argumento." >&2
exit 1
;;
\?)
echo "Error: opción desconocida: -${OPTARG}" >&2
exit 1
;;
esac
done
# Devuelve cuántos argumentos consumió getopts para que el caller pueda hacer shift
echo "$OPTIND"
}
# ── Flujo principal ────────────────────────────────────────────────────────
main() {
if [[ $# -eq 0 ]]; then
echo "Uso: $0 [-v] [-o salida.txt] archivo1.log [archivo2.log ...]" >&2
exit 1
fi
# parsear_opciones necesita los argumentos del script, no los suyos propios
local optind_final
optind_final=$(parsear_opciones "$@")
# Elimina los argumentos ya procesados por getopts
shift $(( optind_final - 1 ))
# A partir de aquí $@ contiene solo los archivos de log
if [[ $# -eq 0 ]]; then
echo "Error: debes indicar al menos un archivo de log." >&2
exit 1
fi
log_verbose "Archivos a procesar: $#"
log_verbose "Salida: ${ARCHIVO_SALIDA:-stdout}"
# Redirige todo el bloque a un archivo si se pidió con -o, o a stdout si no
if [[ -n "$ARCHIVO_SALIDA" ]]; then
generar_resumen "$@" > "$ARCHIVO_SALIDA"
echo "Resumen guardado en: $ARCHIVO_SALIDA"
else
generar_resumen "$@"
fi
}
main "$@"
Qué está pasando en cada decisión relevante
local -a archivos=("$@") dentro de generar_resumen crea un array local que copia los argumentos posicionales de la función. La alternativa de iterar directamente sobre "$@" funciona, pero asignar a un array con nombre hace el código más legible y permite referenciar ${archivos[@]} sin ambigüedad cuando el cuerpo del bucle es largo.
contar_severidad usa echo "$count" para “devolver” el resultado, que generar_resumen captura con $(contar_severidad ...). El || true en la línea de grep -c es necesario porque set -e está activo: grep devuelve exit code 1 cuando no encuentra coincidencias, lo cual es comportamiento normal aquí, no un error.
parsear_opciones "$@" recibe los argumentos del script porque getopts, cuando opera dentro de una función, procesa los parámetros posicionales locales de esa función, no los del script. Si llamaras a la función sin argumentos, getopts no vería nada. La función devuelve $OPTIND vía echo, y el caller hace shift $(( optind_final - 1 )) para descartar exactamente las flags ya procesadas, dejando $@ con los archivos de log.
total_errores y total_warnings no usan local en generar_resumen deliberadamente: son las variables de acumulación que necesitan persistir durante todo el bucle dentro de la misma función. Lo que sí es local es errores y warnings dentro del mismo scope, porque son valores por iteración que no deben filtrarse.
La llamada final main "$@" es el patrón idiomático para que las funciones definidas en el script sean visibles antes de que el flujo de ejecución intente usarlas, y para que "$@" del script llegue íntegro a main, con cada argumento preservado como elemento independiente —incluidos los que contienen espacios.
N° 75