informe-diario.sh ya funciona, pero es un script rígido: /var/log/veloz/app.log y /srv/veloz/datos/envios.csv aparecen escritos literalmente en mitad del cuerpo, y el umbral de errores que quieres vigilar no está en ninguna parte. El día que operaciones mueva el CSV a otro directorio tendrás que buscar y reemplazar por todo el fichero, y cada aparición que se te escape será un fallo silencioso. Esta lección resuelve ese problema y abre la puerta a todo lo demás: las variables son el mecanismo con el que un script deja de ser una lista de comandos y empieza a manipular información.

Contenido

  1. Asignar variables: la regla de oro del = sin espacios
  2. Nombres válidos y convenciones de mayúsculas
  3. Usar variables: $var frente a ${var}
  4. Bash no tiene tipos: todo es una cadena
  5. Sustitución de comandos con $(...)
  6. Constantes con readonly y declare -r
  7. declare y sus opciones útiles
  8. Ámbito: variables de shell frente a variables de entorno
  9. Variables especiales del shell
  10. unset y el refactor de informe-diario.sh

  1. Asignar variables: la regla de oro del = sin espacios

Una variable es un nombre asociado a un valor. Se crea asignándole algo, y en Bash la sintaxis es implacable: ciudad=Valencia, repartidor=alopez, umbral=50.

No puede haber espacios alrededor del =. Esta es la primera regla que rompe todo el mundo, y merece que entiendas por qué existe en lugar de memorizarla. Bash separa la línea en palabras por los espacios: si escribes ciudad = Valencia, ve tres palabras y deduce que quieres ejecutar el comando ciudad con los argumentos = y Valencia, de ahí el mensaje bash: ciudad: orden no encontrada. La variante ciudad =Valencia produce el mismo error, mientras que ciudad= Valencia intenta ejecutar Valencia con la variable ciudad vacía. Tres formas de equivocarse, tres síntomas distintos, una sola regla: pegado al =, sin excepciones.

Si el valor contiene espacios, hay que entrecomillarlo, porque si no la segunda palabra se interpretaría como un comando:

mensaje="Informe generado correctamente"
ruta_larga="/srv/veloz/datos/historico 2026/envios.csv"

Las comillas dobles son la opción por defecto y las estudiarás a fondo en 03-06; por ahora quédate con que entrecomillar nunca hace daño.

  1. Nombres válidos y convenciones de mayúsculas

Un nombre de variable puede contener letras, dígitos y guiones bajos, y no puede empezar por un dígito. No admite guiones, puntos ni espacios: mi-variable no es válido (Bash lo lee como una resta), pero mi_variable sí.

Además de las reglas del lenguaje, existe una convención universal que debes seguir:

Estilo Se usa para Ejemplos
MAYÚSCULAS Constantes del script y variables de entorno RUTA_LOG, UMBRAL_ERRORES, PATH, HOME
minúsculas Variables internas del script, temporales, contadores fecha, total_errores, ciudad

La razón es práctica: el entorno del sistema usa mayúsculas (PATH, HOME, USER, LANG), así que usarlas para variables cualquiera te expone a pisar una del sistema —un PATH=/tmp descuidado deja al script sin encontrar ningún comando—. Reserva las mayúsculas para lo que de verdad es constante o de entorno.

  1. Usar variables: $var frente a ${var}

Para leer el valor se antepone $: si ciudad=Valencia, entonces echo "Ciudad: $ciudad" imprime Ciudad: Valencia. La forma con llaves ${ciudad} es equivalente... hasta que deja de serlo. Las llaves son obligatorias cuando lo que sigue al nombre podría confundirse con parte del propio nombre:

fecha=2026-08-03
echo "envios_$fecha.csv"        # envios_2026-08-03.csv  (el punto corta el nombre)
echo "informe_$fecha_v2.txt"    # informe_               ← ¡MAL!
echo "informe_${fecha}_v2.txt"  # informe_2026-08-03_v2.txt

La segunda línea sale mutilada porque el guion bajo sí es un carácter válido en un nombre de variable: Bash buscó una variable llamada fecha_v2, que no existe, y la sustituyó por nada. Las llaves marcan dónde acaba el nombre y eliminan la ambigüedad.

Hay un tercer motivo: las llaves son la sintaxis de la expansión de parámetros, la familia que incluye valores por defecto como ${var:-valor} (03-06) y manipulación de cadenas como ${var^^} (04-04). Por eso muchos equipos adoptan la norma de escribir siempre ${var}.

  1. Bash no tiene tipos: todo es una cadena

Esta es una idea que hay que asimilar bien, porque explica muchos comportamientos desconcertantes. En Bash todos los valores son cadenas de texto. No existen enteros, ni booleanos, ni decimales.

En umbral=50, ese 50 es el texto «cinco, cero», no el número cincuenta. Consecuencias directas:

  • Para comparar como números hay que usar operadores específicos (-eq, -gt, …) o (( )); comparar con = compara texto, y "10" = "9" es falso mientras que 10 -gt 9 es verdadero. Ese contraste es la lección 03-03.
  • Para sumar no basta con a=$b+$c. Hace falta expansión aritmética $(( )), que verás en 03-03 y a fondo en 04-06.
  • No hay decimales. 50.5 se guarda sin problema como texto, pero cualquier operación aritmética con él dará error. Para decimales se recurre a bc o awk (04-06 y 06-01).
a=5; b=3
echo "$a+$b"        # 5+3   (concatenación de texto)
echo $(( a + b ))   # 8     (expansión aritmética)

Que todo sea texto también significa que una variable no declarada y una variable vacía se comportan casi igual: ambas se expanden a nada. Es cómodo y a la vez peligroso, y de ahí que exista ${var:?mensaje} para exigir que una variable tenga valor (03-06).

  1. Sustitución de comandos con $(...)

Aquí es donde las variables se vuelven realmente útiles en operaciones. La sustitución de comandos ejecuta un comando y sustituye la expresión entera por su salida:

fecha=$(date +%F)
echo "Informe del $fecha"     # → Informe del 2026-08-03

Bash ejecuta date +%F, recoge lo que escribe en stdout y lo guarda en fecha. Los saltos de línea finales se eliminan automáticamente, cosa muy conveniente. Esto convierte cualquier tubería del Módulo 2 en un valor manipulable:

total_errores=$(grep -c ERROR /var/log/veloz/app.log)
total_envios=$(tail -n +2 /srv/veloz/datos/envios.csv | wc -l)
incidencias=$(grep -c ',incidencia,' /srv/veloz/datos/envios.csv)

echo "Errores: $total_errores | Envíos: $total_envios | Incidencias: $incidencias"
# → Errores: 37 | Envíos: 1246 | Incidencias: 118

Existe una forma antigua con comillas invertidas, `comando`, que todavía verás en scripts heredados. Está desaconsejada:

Aspecto $(comando) `comando`
Anidamiento Directo: $(dirname $(which bash)) Requiere escapar cada nivel
Legibilidad Los paréntesis se emparejan visualmente Las dos comillas son idénticas
Confusión visual Ninguna Se confunde con ' en muchas fuentes
Escapes internos Naturales Reglas propias y sorprendentes

Usa siempre $( ). Un aviso de rendimiento: cada $(...) lanza un subshell, es decir, un proceso nuevo (lección 01-04). Con tres o cuatro no se nota, pero dentro de un bucle de mil vueltas sí; ese es el tipo de optimización que trata 08-02.

  1. Constantes con readonly y declare -r

Algunas variables no deben cambiar nunca: las rutas base del script, los umbrales acordados con negocio, el nombre del servicio. Marcarlas como constantes documenta esa intención y, además, hace que Bash la haga cumplir.

readonly RUTA_APP_LOG="/var/log/veloz/app.log"
readonly UMBRAL_ERRORES=50
declare -r RUTA_CSV="/srv/veloz/datos/envios.csv"   # forma equivalente

Si algo intenta modificarlas después, Bash se niega con bash: UMBRAL_ERRORES: variable de sólo lectura. Dos matices. Ese error no detiene el script por sí solo (salvo con set -e, lección 05-03), pero deja rastro y devuelve código distinto de cero. Y readonly es irreversible: no se deshace con unset en la sesión actual. En un script eso es lo que quieres; en tu terminal interactiva, ten cuidado antes de declarar constantes a la ligera.

Regla de oro a partir de ahora: todas las constantes van juntas al principio del fichero, bajo la cabecera. Quien abra el script ve en diez segundos qué rutas usa y qué umbrales aplica, sin leer el cuerpo.

  1. declare y sus opciones útiles

declare crea variables con atributos. Las opciones que importan ahora:

Opción Efecto Ejemplo
-r Solo lectura (constante) declare -r MAX=100
-i Trata la variable como entero declare -i contador=0
-x La exporta al entorno (como export) declare -x VELOZ_ENV=prod
-a / -A Array indexado / asociativo Se estudian en 04-03
-p Muestra la declaración de una variable declare -p UMBRAL_ERRORES

El atributo -i es curioso: hace que las asignaciones se evalúen aritméticamente sin necesidad de $(( )). Y declare -p es una herramienta de depuración excelente, porque muestra el valor y los atributos:

declare -i contador=0;  contador=contador+5;  echo "$contador"  # → 5
sin_atributo=0;  sin_atributo=sin_atributo+5;  echo "$sin_atributo"  # → sin_atributo+5
declare -p RUTA_CSV   # → declare -r RUTA_CSV="/srv/veloz/datos/envios.csv"

  1. Ámbito: variables de shell frente a variables de entorno

Retomamos la distinción de la lección 01-04, ahora con consecuencias prácticas. Una variable puede vivir en dos sitios:

  • Variable de shell: existe solo en el shell que la creó. Es lo que obtienes con una asignación normal.
  • Variable de entorno: se copia a todos los procesos hijos. Se consigue con export.
ciudad=Valencia            # variable de shell
export VELOZ_ENTORNO=prod  # variable de entorno

bash -c 'echo "ciudad=[$ciudad] entorno=[$VELOZ_ENTORNO]"'
# → ciudad=[] entorno=[prod]

El shell hijo no ve ciudad porque no se exportó. Esta es la causa número uno de la pregunta «¿por qué mi script no ve la variable que definí antes?»: un script es un proceso hijo, y solo hereda lo exportado.

La herencia es además de una sola dirección: si el hijo modifica VELOZ_ENTORNO, el padre no se entera. Recibe una copia, no un enlace. Por eso un script no puede cambiar el directorio ni las variables de tu terminal, y por eso existe source (03-01).

Consulta el entorno con env o printenv, y todas las variables con set. Para fijar una variable de entorno solo durante una invocación concreta, se antepone a la orden: VELOZ_ENTORNO=pruebas informe-diario.sh. En la práctica se exporta poco: solo lo que otros programas necesitan leer.

  1. Variables especiales del shell

Bash define automáticamente variables que aportan información del contexto. Las más útiles ahora mismo:

Variable Contiene
$? Código de salida del último comando (01-04)
$$ PID del shell o script actual — ideal para ficheros temporales únicos
$0 Nombre con el que se invocó el script
$HOME, $USER, $HOSTNAME Directorio personal, usuario y nombre del equipo
$PWD / $OLDPWD Directorio actual / anterior
$RANDOM Entero pseudoaleatorio entre 0 y 32767, distinto en cada lectura
$SECONDS Segundos transcurridos desde que arrancó el script
$LINENO Número de línea actual — muy útil al depurar (05-03)
temporal="/tmp/informe-$$-$RANDOM.tmp"
echo "Ejecutando $0 como $USER en $HOSTNAME"
echo "Temporal: $temporal · Duración: $SECONDS s"
Ejecutando /home/joan/veloz-ops/bin/informe-diario.sh como joan en srv-veloz-01
Temporal: /tmp/informe-48213-9174.tmp · Duración: 2 s

Combinar $$ y $RANDOM es el truco clásico para nombres temporales que no colisionen si el script se ejecuta dos veces a la vez; en 05-01 lo sustituiremos por mktemp, que además limpia detrás de sí.

Faltan las variables de argumentos —$1, $@, $#—, que son el tema completo de la lección 03-05.

  1. unset y el refactor de informe-diario.sh

unset ciudad elimina la variable por completo, dejándola indefinida: echo "[$ciudad]" imprimirá []. Ojo: unset no funciona sobre constantes declaradas con readonly, y no es lo mismo que asignar la cadena vacía. Para casi todo dan el mismo resultado, pero ${var:?} y ${var+x} (03-06) sí distinguen ambos casos.

Con todas las piezas sobre la mesa, así queda informe-diario.sh:

#!/usr/bin/env bash
#
# informe-diario.sh - Resumen diario de actividad de Veloz Envíos
#
# Propósito : Contar los ERROR de app.log y agrupar los envíos por estado.
# Autor     : Joan Costa <[email protected]>   ·   Uso: informe-diario.sh
# Códigos   : 0 correcto
#

# --- Constantes -------------------------------------------------------
readonly RUTA_APP_LOG="/var/log/veloz/app.log"
readonly RUTA_CSV="/srv/veloz/datos/envios.csv"
readonly DIR_INFORMES="$HOME/veloz-ops/logs"
readonly UMBRAL_ERRORES=50
readonly SERVIDOR="srv-veloz-01"

# --- Datos calculados -------------------------------------------------
fecha=$(date +%F)
hora=$(date +%T)
total_errores=$(grep -c ERROR "$RUTA_APP_LOG")
total_envios=$(tail -n +2 "$RUTA_CSV" | wc -l)

# --- Informe ----------------------------------------------------------
echo "==================================================="
echo "  INFORME DIARIO - VELOZ ENVIOS ($SERVIDOR)"
echo "  Generado: ${fecha} ${hora}"
echo "==================================================="
echo
echo "-- Errores en app.log --"
echo "Total: ${total_errores} (umbral de aviso: ${UMBRAL_ERRORES})"
echo
echo "-- Envíos por estado (${total_envios} en total) --"
tail -n +2 "$RUTA_CSV" | cut -d, -f5 | sort | uniq -c | sort -rn
exit 0

Compáralo con la versión de 03-01 y mide la mejora. Las rutas aparecen una sola vez, agrupadas y visibles en las primeras líneas: mover envios.csv es cambiar un carácter. El umbral acordado con negocio existe ahora como dato del programa, con nombre propio, listo para que la lección 03-04 lo convierta en un aviso automático. Los recuentos se calculan una vez y se reutilizan. Y el informe muestra fecha, hora y servidor, información imprescindible cuando alguien lo lea tres semanas después.

Fíjate en dos detalles que anticipan 03-06: las variables se usan siempre entre comillas dobles ("$RUTA_CSV") y se emplean llaves cuando el valor va pegado a otro texto. Adopta el hábito desde ya.

Errores Comunes y Consejos

  • Espacios alrededor del =. ciudad = Valencia intenta ejecutar el comando ciudad. Es el error número uno del principiante.
  • Olvidar el $ al leer, o ponerlo al asignar. echo ciudad imprime la palabra; $ciudad=Valencia es un error de sintaxis. El $ solo se usa para leer.
  • Nombres pegados a texto sin llaves. $fecha_v2 busca la variable fecha_v2. Usa ${fecha}_v2.
  • Valores con espacios sin comillas. msg=Informe diario intenta ejecutar diario con msg=Informe en el entorno.
  • Usar MAYÚSCULAS para todo. Antes o después pisarás PATH, HOME o IFS, con consecuencias difíciles de diagnosticar.
  • Esperar que un script vea las variables del padre. Solo hereda lo exportado; para eso está export o source.
  • Confiar en aritmética con texto. total=$a+$b produce la cadena 5+3. Necesitas $(( )).

Ejercicios

Ejercicio 1 — Cabecera de constantes. Escribe el bloque de constantes de un script resumen-ciudad.sh que vaya a trabajar con envios.csv, escriba en ~/veloz-ops/logs, considere «muchas incidencias» a partir de 30 y analice Valencia por defecto. Usa la convención correcta de mayúsculas y readonly, y añade una variable no constante con la fecha del día en formato AAAA-MM-DD.

Ejercicio 2 — Depurar un script roto. Un compañero ha escrito esto y no funciona. Identifica los cinco errores y reescríbelo.

#!/usr/bin/env bash
RUTA = "/srv/veloz/datos/envios.csv"
total_envios = `wc -l < $RUTA`
fichero_salida="$HOME/veloz-ops/logs/resumen_$fecha_final.txt"
echo "Envíos: total_envios" > $fichero_salida

Ejercicio 3 — Métricas del informe. Amplía el bloque de datos calculados de informe-diario.sh para que guarde en variables: el número de entregas completadas, el número de incidencias, el repartidor con más envíos del día y el nombre del fichero de salida (que debe incluir la fecha). Imprime las cuatro con etiquetas.

Soluciones

Solución al Ejercicio 1

readonly RUTA_CSV="/srv/veloz/datos/envios.csv"     # --- Constantes ---
readonly DIR_INFORMES="$HOME/veloz-ops/logs"
readonly UMBRAL_INCIDENCIAS=30
readonly CIUDAD_DEFECTO="Valencia"
fecha=$(date +%F)                                   # --- Datos calculados ---

Las cuatro primeras son constantes, en mayúsculas y con readonly; fecha va en minúsculas y sin readonly porque es un valor calculado en cada ejecución, no una decisión de diseño. Nota que $HOME dentro de comillas dobles se expande correctamente, lo que hace el script válido para cualquier usuario: escribir /home/joan/... a mano lo rompería para todos los demás.

Solución al Ejercicio 2

Los cinco errores: (1) RUTA = "..." tiene espacios alrededor del =; (2) total_envios = ... repite el mismo fallo; (3) usa comillas invertidas en lugar de $( ); (4) $fecha_final busca una variable inexistente —falta ${fecha}_final— y además fecha nunca se define; (5) echo "Envíos: total_envios" imprime el texto literal porque falta el $, y > $fichero_salida va sin comillas.

#!/usr/bin/env bash
readonly RUTA="/srv/veloz/datos/envios.csv"
fecha=$(date +%F)
total_envios=$(tail -n +2 "$RUTA" | wc -l)
fichero_salida="$HOME/veloz-ops/logs/resumen_${fecha}_final.txt"
echo "Envíos: $total_envios" > "$fichero_salida"

Se ha añadido además tail -n +2 para no contar la cabecera del CSV, un fallo de contenido que la versión original arrastraba sin que nadie lo notara.

Solución al Ejercicio 3

entregados=$(grep -c ',entregado,' "$RUTA_CSV")
incidencias=$(grep -c ',incidencia,' "$RUTA_CSV")
top_repartidor=$(tail -n +2 "$RUTA_CSV" | cut -d, -f4 | sort | uniq -c \
                 | sort -rn | head -1 | tr -s ' ' | cut -d' ' -f3)
fichero_salida="${DIR_INFORMES}/informe-$(date +%F).txt"

echo "Entregados      : $entregados"      # → 981
echo "Incidencias     : $incidencias"     # → 118
echo "Top repartidor  : $top_repartidor"  # → mgarcia
echo "Fichero destino : $fichero_salida"  # → ~/veloz-ops/logs/informe-2026-08-03.txt

La línea de top_repartidor merece explicación. uniq -c genera líneas con espacios de relleno al principio ( 412 mgarcia), así que tr -s ' ' los comprime a uno solo y cut -d' ' -f3 toma el tercer campo —el primero queda vacío por el espacio inicial—. Es funcional, pero también un aviso: cuando una tubería necesita tantos ajustes, la herramienta adecuada es awk, y en 06-01 esa misma línea se reducirá a una expresión mucho más limpia.

Conclusión

informe-diario.sh ya no es una lista de comandos: es un programa con datos. Sabes asignar sin espacios alrededor del = y por qué esa regla existe; nombras siguiendo la convención de mayúsculas para constantes y minúsculas para el resto; usas ${var} cuando las llaves son necesarias; asumes que en Bash todo es texto; capturas la salida de cualquier tubería con $( ); blindas rutas y umbrales con readonly; conoces declare y sus atributos; distingues variables de shell de variables de entorno; y manejas las especiales como $?, $$ o $SECONDS.

El script tiene ahora un UMBRAL_ERRORES=50 en la cabecera que no sirve para nada: se imprime, pero nadie lo compara con nada. Y sigue dando por hecho que app.log y envios.csv existen y son legibles; si operaciones renombra el CSV, el script no avisará: escupirá un informe con ceros.

En la lección 03-03 aprenderás a preguntar. Verás los operadores del shell: cómo encadenar comandos con && y ||, cómo comprobar si un fichero existe con [[ -f ... ]], y cómo comparar números —el clásico -gt frente a >— para poder responder a la pregunta que ese umbral lleva esperando desde el principio: ¿hay hoy más errores de los tolerables?

Curso de Programación en Bash

Módulo 1: Introducción a Bash

Módulo 2: Comandos Básicos de Bash

Módulo 3: Fundamentos de Scripting

Módulo 4: Scripting Intermedio

Módulo 5: Técnicas Avanzadas de Scripting

Módulo 6: Trabajando con Herramientas Externas

Módulo 7: Automatización y Programación

Módulo 8: Mejores Prácticas y Optimización

Módulo 9: Proyectos del Mundo Real

© Copyright 2026. Todos los derechos reservados