Funciones y argumentos en Bash: scope, paso de datos y parseo

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.

75

Dejar un comentario

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *

Scroll al inicio