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
- Asignar variables: la regla de oro del
=sin espacios - Nombres válidos y convenciones de mayúsculas
- Usar variables:
$varfrente a${var} - Bash no tiene tipos: todo es una cadena
- Sustitución de comandos con
$(...) - Constantes con
readonlyydeclare -r declarey sus opciones útiles- Ámbito: variables de shell frente a variables de entorno
- Variables especiales del shell
unsety el refactor deinforme-diario.sh
- Asignar variables: la regla de oro del
= sin espacios
= sin espaciosUna 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:
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.
- 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.
- Usar variables:
$var frente a ${var}
$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.txtLa 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}.
- 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 que10 -gt 9es 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.5se guarda sin problema como texto, pero cualquier operación aritmética con él dará error. Para decimales se recurre abcoawk(04-06 y 06-01).
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).
- 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:
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: 118Existe 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.
- Constantes con
readonly y declare -r
readonly y declare -rAlgunas 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 equivalenteSi 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.
declare y sus opciones útiles
declare y sus opciones útilesdeclare 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"
- Á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.
- 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.
unset y el refactor de informe-diario.sh
unset y el refactor de informe-diario.shunset 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 0Compá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 = Valenciaintenta ejecutar el comandociudad. Es el error número uno del principiante. - Olvidar el
$al leer, o ponerlo al asignar.echo ciudadimprime la palabra;$ciudad=Valenciaes un error de sintaxis. El$solo se usa para leer. - Nombres pegados a texto sin llaves.
$fecha_v2busca la variablefecha_v2. Usa${fecha}_v2. - Valores con espacios sin comillas.
msg=Informe diariointenta ejecutardiarioconmsg=Informeen el entorno. - Usar MAYÚSCULAS para todo. Antes o después pisarás
PATH,HOMEoIFS, con consecuencias difíciles de diagnosticar. - Esperar que un script vea las variables del padre. Solo hereda lo exportado; para eso está
exportosource. - Confiar en aritmética con texto.
total=$a+$bproduce la cadena5+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_salidaEjercicio 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.txtLa 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
- ¿Qué es Bash?
- Configurando tu Entorno
- Navegación Básica en la Línea de Comandos
- Entendiendo el Shell
- Encontrar Ayuda: man, help y --help
Módulo 2: Comandos Básicos de Bash
- Operaciones con Archivos y Directorios
- Comandos de Procesamiento de Texto
- Permisos y Propiedad de Archivos
- Redirección y Tuberías
- Comodines y Expansión de Rutas
- Historial y Atajos de Teclado
Módulo 3: Fundamentos de Scripting
- Creando y Ejecutando un Script
- Variables y Constantes
- Operadores Básicos
- Sentencias Condicionales
- Argumentos y Entrada del Usuario
- Comillas, Expansión y Sustitución
Módulo 4: Scripting Intermedio
- Bucles en Bash
- Funciones en Bash
- Arrays y Arrays Asociativos
- Manipulación de Cadenas
- La Sentencia case y los Menús Interactivos
- Aritmética y Cálculos Numéricos
Módulo 5: Técnicas Avanzadas de Scripting
- Operaciones Avanzadas con Archivos
- Gestión de Procesos
- Manejo de Errores y Depuración
- Expresiones Regulares
- Entrada/Salida Avanzada: Descriptores y Here-Documents
- Scripts Modulares y Librerías Reutilizables
Módulo 6: Trabajando con Herramientas Externas
Módulo 7: Automatización y Programación
- Trabajos Cron
- Automatizando Tareas
- Scripts de Respaldo y Restauración
- Monitoreo y Registro
- Servicios y Temporizadores con systemd
- Automatización Remota con SSH
Módulo 8: Mejores Prácticas y Optimización
- Escribiendo Código Legible
- Optimizando Scripts en Bash
- Consideraciones de Seguridad
- Control de Versiones con Git
- Análisis Estático con ShellCheck y shfmt
- Pruebas Automatizadas con Bats
- Portabilidad: POSIX sh frente a Bashismos
