revision_salud.sh ya existe, pero lleva el umbral de disco escrito a mano en el cuerpo y la IP del servidor justo encima. El día que Marta pida vigilar también el servidor de pruebas tendrás que duplicar el fichero, y a partir de ahí tendrás dos scripts que se separan poco a poco hasta que uno de los dos miente. La cura es sacar los valores del cuerpo y meterlos en variables bien pensadas.
Esta lección va de eso, pero sobre todo va de algo más profundo: en Bash todo es una cadena de texto, y esa única frase explica el 90 % de las sorpresas que te vas a llevar. A cambio, Bash te da un arsenal de manipulación de cadenas —la expansión de parámetros— tan potente que sustituye a basename, dirname, cut, tr y buena parte de sed sin lanzar un solo proceso.
Contenido
- Bash no tiene tipos: todo es cadena
- Asignación: las tres reglas que rompen a todo el mundo
- Ámbito: script, subshell y entorno
declarey sus atributos- Expansión de parámetros a fondo
- Aritmética, y por qué Bash no sabe de decimales
- Arrays indexados
- Arrays asociativos: reservas por casa
- Cadenas y
printf - Aplicación:
revision_salud.shconfigurable
- Bash no tiene tipos: todo es cadena
En Python o Java declaras el tipo y el lenguaje te protege. En Bash no hay tipos: toda variable guarda una cadena de bytes, y el «tipo» aparece solo al usarla, según el contexto.
operador@srv-tramontana:~$ n=42; echo "${n} + 1 = $((n + 1))"
42 + 1 = 43
operador@srv-tramontana:~$ n=42abc; echo "$((n + 1))"
-bash: 42abc: valor demasiado grande para la base (el elemento de error es "42abc")La misma variable es un número dentro de $(( )) y texto fuera. Nadie te avisó al asignar 42abc; el error llegó al intentar sumar. Tres consecuencias que conviene interiorizar hoy:
- Nada valida lo que guardas. Validar es tu trabajo, y lo formalizaremos en 04-04 con
[[ $v =~ ^[0-9]+$ ]]. - Comparar números y comparar cadenas son operaciones distintas.
"10" < "9"es cierto como texto y falso como número; de ahí que existan-lty<por separado. - No hay decimales.
$(( 7842 / 25 ))da 313, no 313.70 (apartado 6). Las únicas excepciones parciales a la regla son los atributos dedeclarey los arrays, que sí son una estructura de datos de verdad.
- Asignación: las tres reglas que rompen a todo el mundo
operador@srv-tramontana:~$ version=3.2.1 # correcto
operador@srv-tramontana:~$ version = 3.2.1
-bash: version: orden no encontrada
operador@srv-tramontana:~$ mensaje=copia completa
-bash: completa: orden no encontrada
operador@srv-tramontana:~$ mensaje="copia completa" # correcto
operador@srv-tramontana:~$ LC_ALL=C sort ~/datos/casas.txt | head -1
cal-ferrer
operador@srv-tramontana:~$ echo "LC_ALL sigue valiendo: '${LC_ALL:-vacío}'"
LC_ALL sigue valiendo: 'vacío'- Sin espacios alrededor del
=.version = 3.2.1no es una asignación: Bash lo lee como el comandoversioncon los argumentos=y3.2.1. Es la regla más quebrantada por quien viene de otros lenguajes, y el mensaje de error no ayuda nada. - Comillas si hay espacios. Sin ellas la asignación termina en el primer espacio. Regla operativa: entrecomilla siempre el valor, tenga espacios o no.
- Al usarla,
$; al asignarla, no.nombre=valorpara escribir,"$nombre"para leer.
Las dos últimas líneas del ejemplo muestran otra cosa: anteponer una variable a un comando la hace existir solo durante esa ejecución, que es exactamente lo que hace la convención LC_ALL=C del curso. Sobre los nombres: letras, dígitos y _, sin empezar por dígito; MAYÚSCULAS para constantes y entorno, minúsculas para las de trabajo. Y evita pisar PATH, HOME, IFS, PWD, RANDOM o SECONDS, que tienen dueño.
- Ámbito: script, subshell y entorno
Retomando 03-01, ahora desde dentro de un script:
| Ámbito | Cómo se crea | Quién la ve |
|---|---|---|
| De shell | VAR=valor |
Solo el script actual, incluidas sus funciones |
| De entorno | export VAR=valor |
El script y todos los procesos hijos que lance |
| Local | local var=valor (en funciones) |
Solo la función y las que ella llame |
operador@srv-tramontana:~$ cat /tmp/ambito.sh
CASA="mas-figueres"; export RELEASE="3.2.1"
bash -c 'echo "hijo ve CASA=[${CASA:-}] RELEASE=[${RELEASE:-}]"'
( CASA="can-ventos"; echo "subshell cambia CASA a $CASA" )
echo "el padre sigue con CASA=$CASA"
operador@srv-tramontana:~$ bash /tmp/ambito.sh
hijo ve CASA=[] RELEASE=[3.2.1]
subshell cambia CASA a can-ventos
el padre sigue con CASA=mas-figueresTres lecciones en seis líneas. El hijo no ve CASA porque no está exportada, pero sí RELEASE. El subshell —los paréntesis— sí ve todo lo del padre porque es una copia de él, pero sus cambios no vuelven: el mismo muro que encontraste con cd, y el que hará que un while read dentro de una tubería pierda sus variables (04-03). En cuanto a local, solo puede usarse dentro de una función y es un adelanto de 04-05, pero la regla es tan importante que la enunciamos ya: toda variable creada dentro de una función debe ser local; sin él es global, y una función puede pisar silenciosamente el contador de otra.
declare y sus atributos
declare y sus atributosdeclare (o su sinónimo typeset) crea variables con atributos, lo más parecido a un tipo que tiene Bash.
| Atributo | Efecto |
|---|---|
-r / -x |
Solo lectura (= readonly) / exportada (= export) |
-i |
Entero: las asignaciones se evalúan como aritmética |
-a / -A |
Array indexado / asociativo — este último es obligatorio declararlo |
-l / -u |
Convierte a minúsculas / mayúsculas al asignar |
-p |
No declara: imprime cómo está declarada una variable |
operador@srv-tramontana:~$ declare -r RELEASE_ACTIVO="3.2.1"; RELEASE_ACTIVO="3.3.0"
-bash: RELEASE_ACTIVO: variable de sólo lectura
operador@srv-tramontana:~$ declare -i errores=0; errores+=14; errores+=5; echo "$errores"
19
operador@srv-tramontana:~$ declare -u codigo="ok"; echo "$codigo"; declare -p errores
OK
declare -i errores="19"Fíjate en errores+=14: con el atributo -i suma, mientras que sin él concatenaría y dejaría 014. Ese comportamiento distinto según un atributo invisible es la razón por la que muchos prefieren la aritmética explícita con $(( )); usaremos -i solo para contadores evidentes. readonly sí lo usaremos mucho: convierte un error lógico —reasignar una constante por accidente— en un error inmediato y ruidoso, siempre mejor que uno silencioso.
- Expansión de parámetros a fondo
Aquí Bash deja de parecer un lenguaje de juguete. Todo lo que sigue ocurre dentro del shell, sin lanzar procesos. Partimos de un nombre real de nuestras copias:
operador@srv-tramontana:~$ f="/srv/tramontana/backups/envios/reservas-2026-08-18.csv.gz"
operador@srv-tramontana:~$ base="${f##*/}"; echo "$base" # tras la última /
reservas-2026-08-18.csv.gz
operador@srv-tramontana:~$ echo "${f%/*}" # antes de la última /
/srv/tramontana/backups/envios
operador@srv-tramontana:~$ echo "${base%%.*}" # hasta el primer punto
reservas-2026-08-18
operador@srv-tramontana:~$ echo "${base#*-}" # tras el primer guion
2026-08-18.csv.gzEso es basename y dirname sin basename ni dirname. La regla mnemotécnica es visual: en un teclado, # está a la izquierda del $ y quita por la izquierda; % está a la derecha y quita por la derecha. Doblar el símbolo hace el patrón codicioso, como la codicia de 03-02. Ojo: son patrones de globbing (*, ?, [...]), no expresiones regulares.
| Expansión | Qué devuelve |
|---|---|
${VAR:-valor} / ${VAR:=valor} |
valor si VAR está vacía o no definida; la segunda forma además asigna |
${VAR:?mensaje} |
El valor, o aborta el script con ese mensaje si está vacía |
${VAR:+valor} |
valor solo si VAR tiene contenido; si no, cadena vacía |
${#VAR} / ${VAR:desp:long} |
Longitud en caracteres / subcadena desde desp (base 0) |
${VAR#patrón} / ${VAR##patrón} |
Quita el prefijo más corto / más largo que case |
${VAR%patrón} / ${VAR%%patrón} |
Quita el sufijo más corto / más largo que case |
${VAR/pat/nuevo} / ${VAR//pat/nuevo} |
Sustituye la primera aparición / todas |
${VAR^^} / ${VAR,,} |
Todo a mayúsculas / a minúsculas |
Los cuatro primeros son la base de la configuración por defecto, y :+ es el menos conocido: añade algo solo si hay algo que añadir.
operador@srv-tramontana:~$ fecha="2026-08-18"; nivel="aviso"; ruta="/casas/el-molí"; verbose=""
operador@srv-tramontana:~$ echo "Año ${fecha:0:4}, mes ${fecha:5:2}, longitud ${#fecha}"
Año 2026, mes 08, longitud 10
operador@srv-tramontana:~$ echo "${ruta//í/i} → [${nivel^^}]"
/casas/el-moli → [AVISO]
operador@srv-tramontana:~$ echo "curl -s ${verbose:+-v} ${URL:?falta la URL a comprobar}"
-bash: URL: falta la URL a comprobarEl ${ruta//í/i} resuelve, sin llamar a sed, la tilde que la aplicación mete en los enlaces a /casas/el-molí y que provoca los cuatro 404 de acceso.log. El ${verbose:+-v} añade la opción solo si la variable tiene contenido, que es como se construyen líneas de comando condicionales. Y ${VAR:?mensaje}, ya visto en 03-01, es la forma más barata de exigir un valor obligatorio.
- Aritmética, y por qué Bash no sabe de decimales
Bash tiene dos formas de hacer cuentas y las dos trabajan solo con enteros con signo de 64 bits.
operador@srv-tramontana:~$ echo "$(( (14 + 5 + 4) * 100 / 412 ))" # % de errores
5
operador@srv-tramontana:~$ i=0; (( i++ )); (( i += 10 )); echo "$i"
11$(( )) sustituye por el resultado, así que se usa donde quieras el valor. (( )) evalúa y no imprime nada: sirve para modificar variables o como condición, devolviendo 0 (éxito) si el resultado es distinto de cero. Dentro de ambas las variables van sin $ y dispones de + - * / % **, comparadores, && || !, ++ --, += y el ternario a ? b : c.
Dos avisos: un número con cero a la izquierda se interpreta en octal: $(( 08 )) es un error de sintaxis, lo que arruina cualquier cuenta con meses (08, 09); fuérzalo con $(( 10#08 )). Y (( i++ )) con i a 0 devuelve 1, porque el valor de la expresión es el previo al incremento, y con set -e eso aborta el script; es una trampa de 04-06, y por ahora usa (( ++i )) o i=$(( i + 1 )).
Para decimales hay que salir de Bash:
operador@srv-tramontana:~$ echo "scale=2; 7842.50 / 25" | bc
313.70
operador@srv-tramontana:~$ LC_ALL=C awk -F';' 'NR>1 { s += $6; n++ } END { printf "%.2f\n", s/n }' \
/home/operador/datos/reservas.csv
313.70El importe medio por reserva de Tramontana es de 313,70 €. A bc hay que decirle la escala (scale=2) porque por defecto trunca a cero decimales al dividir; bc -l carga la librería matemática y la sube a 20. Pero fíjate en el segundo ejemplo: si los datos ya están en un fichero, awk hace lectura y cuenta a la vez. La regla del curso: bc para una cuenta suelta, awk en cuanto haya un fichero. Y el LC_ALL=C no es decorativo: con locale español printf '%.2f' imprimiría 313,70 con coma, y esa coma rompe cualquier comparación posterior.
- Arrays indexados
Un array es una lista ordenada de cadenas con índice numérico desde 0. Resuelve «guardar varias cosas en una variable» sin recurrir a cadenas separadas por espacios, que se rompen en cuanto un elemento contiene uno.
operador@srv-tramontana:~$ releases=(3.1.0 3.2.0 3.2.1); releases+=(3.3.0)
operador@srv-tramontana:~$ echo "${releases[0]} ... ${releases[-1]} (total ${#releases[@]})"
3.1.0 ... 3.3.0 (total 4)
operador@srv-tramontana:~$ echo "${releases[@]:1:2}"
3.2.0 3.2.1
operador@srv-tramontana:~$ unset 'releases[0]'; echo "${!releases[@]}"
1 2 3Se crea con paréntesis, se añade con +=(...), [-1] es el último, ${#array[@]} cuenta elementos y ${array[@]:1:2} es una rebanada. El último comando revela algo importante: unset deja un hueco, los índices no se recolocan; por eso existe ${!array[@]}, que da los índices y no los valores.
${array[@]} frente a ${array[*]}
La diferencia que más bugs causa, y solo se manifiesta dentro de comillas dobles:
operador@srv-tramontana:~$ ficheros=("informe agosto.pdf" "reservas.csv")
operador@srv-tramontana:~$ for f in "${ficheros[@]}"; do echo -n "[$f] "; done; echo
[informe agosto.pdf] [reservas.csv]
operador@srv-tramontana:~$ for f in "${ficheros[*]}"; do echo -n "[$f] "; done; echo
[informe agosto.pdf reservas.csv]
operador@srv-tramontana:~$ for f in ${ficheros[@]}; do echo -n "[$f] "; done; echo
[informe] [agosto.pdf] [reservas.csv]"${array[@]}" produce un argumento por elemento, respetando espacios: es lo que quieres el 99 % de las veces. "${array[*]}" produce un único argumento con los elementos unidos por el primer carácter de IFS, útil solo para imprimir. Y sin comillas Bash divide por espacios, y «informe agosto.pdf» se convierte en dos ficheros que no existen. Grábatelo: arrays siempre con "${array[@]}".
- Arrays asociativos: reservas por casa
Un array asociativo indexa por cadena en vez de por número: es lo que en 03-05 te daba awk, ahora dentro de Bash. Hay que declararlo con declare -A antes de usarlo; si no, Bash lo trata como indexado y todas las claves colapsan en el índice 0.
#!/usr/bin/env bash
set -euo pipefail
CSV="/home/operador/datos/reservas.csv"
declare -A noches_casa
# tail -n +2 salta la cabecera. IFS=';' parte cada línea por el separador
# del CSV; read -r asigna cada campo a una variable (04-03 lo detalla).
# ${var:-0} da 0 la primera vez que aparece una casa, con set -u activo.
while IFS=';' read -r _id _fecha casa _huesped noches _importe; do
noches_casa["$casa"]=$(( ${noches_casa["$casa"]:-0} + noches ))
done < <(tail -n +2 "$CSV")
# ${!array[@]} son las CLAVES; sort las deja en orden reproducible.
for casa in $(printf '%s\n' "${!noches_casa[@]}" | LC_ALL=C sort); do
printf '%-14s %3d noches\n' "$casa" "${noches_casa[$casa]}"
doneoperador@srv-tramontana:~$ bash /tmp/por_casa.sh
cal-ferrer 11 noches can-ventos 17 noches
el-moli 8 noches la-solana 13 noches
mas-figueres 21 nochesLas 70 noches conocidas, repartidas (aquí en dos columnas para ahorrar espacio). Dos piezas nuevas: < <(tail -n +2 "$CSV") es una sustitución de proceso que evita que el while corra en un subshell y pierda los arrays al terminar —lo detalla 04-03—, y los _id, _fecha, _huesped son la convención para «leo este campo pero no lo uso», que además calla a shellcheck. Nota honesta: este informe cabe en una línea de awk. Los arrays asociativos brillan cuando lo que acumulas no es texto tabular —códigos de salida por servicio, rutas ya procesadas, un caché—; con un CSV, awk sigue ganando. La regla se formaliza en 04-04.
- Cadenas y
printf
printfConcatenar es escribir seguido, y += añade al final:
operador@srv-tramontana:~$ host="10.0.2.15"; puerto=8080
operador@srv-tramontana:~$ url="http://${host}:${puerto}"; url+="/salud"; echo "$url"
http://10.0.2.15:8080/saludLas llaves de ${host} no son opcionales: sin ellas $host: funcionaría por casualidad, pero $puertoX buscaría una variable llamada puertoX. Usa llaves siempre que la variable toque texto pegado. Para la salida, printf sustituye a echo en todo lo serio: echo varía entre shells y hace cosas raras con -n y las barras invertidas, mientras que printf está definido con precisión y reutiliza el formato mientras queden argumentos, lo que lo convierte en un generador de tablas.
| Directiva | Significado |
|---|---|
%s / %d |
Cadena / entero decimal |
%.2f |
Coma flotante con dos decimales |
%-20s |
Cadena alineada a la izquierda en 20 columnas |
%10.2f |
Flotante en 10 columnas, alineado a la derecha |
%% / \n / \t |
Un % literal / salto de línea / tabulador |
operador@srv-tramontana:~$ LC_ALL=C printf '%-14s %6d %10.2f\n' \
mas-figueres 21 2450 can-ventos 17 1980 la-solana 13 1425.50
mas-figueres 21 2450.00
can-ventos 17 1980.00
la-solana 13 1425.50Una sola llamada, tres filas: el formato se ha reutilizado tres veces. Esa es la tabla del informe de Marta, y la razón por la que echo queda ya solo para mensajes de una línea sin formato.
- Aplicación:
revision_salud.sh configurable
revision_salud.sh configurableEl cambio de fondo: ningún valor está ya clavado en el cuerpo. Todos son constantes al principio, y las que tiene sentido ajustar desde fuera se leen del entorno con ${VAR:-defecto}.
#!/usr/bin/env bash
#
# revision_salud.sh - Revisión rápida de salud de Tramontana Reservas.
#
# Autor : Operador de sistemas <operador@srv-tramontana>
# Fecha : 2026-08-18
# Uso : revision_salud.sh
# Entorno: TRAMONTANA_HOST, TRAMONTANA_PUERTO, TRAMONTANA_UMBRAL_DISCO,
# TRAMONTANA_LOG_ERRORES
set -euo pipefail
# --- Configuración (entorno > valor por defecto) --------------------------
readonly HOST="${TRAMONTANA_HOST:-10.0.2.15}"
readonly PUERTO="${TRAMONTANA_PUERTO:-8080}"
readonly UMBRAL_DISCO="${TRAMONTANA_UMBRAL_DISCO:-80}"
readonly LOG_ERRORES="${TRAMONTANA_LOG_ERRORES:-/var/log/tramontana/errores.log}"
readonly ESPERA_MAX=5
readonly URL_SALUD="http://${HOST}:${PUERTO}/salud"
# --- Cuerpo --------------------------------------------------------------
LC_ALL=C printf '=== Salud de Tramontana Reservas (%s) ===\n' "$(date '+%F %T')"
printf 'Servidor : %s Objetivo: %s\n\n' "$(hostname)" "$URL_SALUD"
codigo_http=$(curl -s -o /dev/null -w '%{http_code}' \
--max-time "$ESPERA_MAX" "$URL_SALUD" || true)
uso_disco=$(df --output=pcent / | tail -1 | tr -dc '0-9')
libre_kb=$(df --output=avail / | tail -1 | tr -dc '0-9')
# Un entero de KiB no dice nada a un humano: lo pasamos a GiB con un decimal.
libre_gb=$(LC_ALL=C awk -v k="$libre_kb" 'BEGIN { printf "%.1f", k/1048576 }')
hoy=$(date '+%Y-%m-%d')
errores_hoy=$(grep -c "^$hoy" "$LOG_ERRORES" || true)
LC_ALL=C printf '%-14s %s\n' \
"HTTP:" "$codigo_http" \
"Disco:" "${uso_disco}% usado, ${libre_gb} GiB libres (umbral ${UMBRAL_DISCO}%)" \
"Errores:" "$errores_hoy hoy"
exit 0operador@srv-tramontana:~$ ~/scripts/revision_salud.sh
=== Salud de Tramontana Reservas (2026-08-18 09:41:02) ===
Servidor : srv-tramontana Objetivo: http://10.0.2.15:8080/salud
HTTP: 200
Disco: 30% usado, 15.4 GiB libres (umbral 80%)
Errores: 6 hoy
operador@srv-tramontana:~$ TRAMONTANA_PUERTO=9090 ~/scripts/revision_salud.sh | sed -n 2p
Servidor : srv-tramontana Objetivo: http://10.0.2.15:9090/saludSin tocar el fichero, el script apunta a otro puerto. Ese es el primer paso de la cadena de precedencia argumentos > entorno > fichero de configuración > defectos que completaremos en 04-03 y cerraremos en 04-07. Sigue sin comparar nada con UMBRAL_DISCO: eso llega en 04-04.
Errores Comunes y Consejos
- Espacios alrededor del
=.VAR = valorno asigna nada: es el error número uno. El segundo es usar$VARsin comillas: si el valor contiene espacios o un*, Bash lo parte o lo expande contra el disco. SC2086 es esto, y no es un aviso pedante: es un fallo esperando su fichero con espacio. - Olvidar
declare -A. Sin él,datos[mas-figueres]=1evalúa la clave como aritmética, la convierte en 0 y todas las casas se pisan entre sí, sin error alguno. Y no confundas${VAR:-x}con${VAR:=x}: el primero no modifica la variable, el segundo sí. ${array[*]}en un bucle. Recorre un único elemento gigante; usa siempre"${array[@]}". Y cuidado con los ceros a la izquierda:$(( 08 + 1 ))falla por octal, así que escribe10#$mes.- Esperar decimales de
$(( )).$(( 7842 / 25 ))da 313 y se pierde el resto sin avisar. Para dinero,awkobcconLC_ALL=C. - Consejo:
declare -p VARte dice qué es realmente una variable —array, entero, exportada— cuando algo no cuadra. Es elstatde las variables. - Consejo: prefiere expansión de parámetros a
basename,dirname,cutotrpara lo simple: en un bucle de mil iteraciones son mil procesos que te ahorras. Y declarareadonlytoda constante.
Ejercicios
Ejercicio 1. Dado f="/srv/tramontana/backups/pre-despliegue/app-3.2.1-2026-08-11.tar.gz", obtén solo con expansión de parámetros (ni basename, ni cut, ni sed): el directorio, el nombre del fichero, el nombre sin extensión, la versión 3.2.1 y la fecha 2026-08-11. Explica cada expansión.
Ejercicio 2. Escribe ~/scripts/resumen_reservas.sh que, leyendo reservas.csv, imprima una tabla alineada con casa, reservas, noches e importe, más una línea de totales. Usa un array asociativo y printf. Debe coincidir con los datos conocidos: 25 reservas, 70 noches, 7.842,50 €.
Ejercicio 3. Luis propone esto para calcular el porcentaje de peticiones POST sobre las 412 de acceso.log. Explica los tres defectos y reescríbelo.
Soluciones
Solución 1.
operador@srv-tramontana:~$ f="/srv/tramontana/backups/pre-despliegue/app-3.2.1-2026-08-11.tar.gz"
operador@srv-tramontana:~$ base="${f##*/}"; nombre="${base%%.tar.gz}"; sinapp="${nombre#app-}"
operador@srv-tramontana:~$ printf '%s\n%s\n%s\n%s\n%s\n' "${f%/*}" "$base" "$nombre" \
"${sinapp%%-*}" "${nombre##*-[0-9].[0-9].[0-9]-}"
/srv/tramontana/backups/pre-despliegue
app-3.2.1-2026-08-11.tar.gz
app-3.2.1-2026-08-11
3.2.1
2026-08-11${f%/*}quita el sufijo más corto que empiece por/: esdirname.${f##*/}quita el prefijo más largo acabado en/: esbasename.${base%%.tar.gz}recorta la extensión compuesta. Aquí%%y%dan lo mismo porque el patrón es literal, pero%%.*habría sido peligroso: cortaría también en el punto de3.2.1, dejandoapp-3.- La versión sale en dos pasos:
#app-quita el prefijo y%%-*recorta desde el primer guion restante, el que precede a la fecha. Para la fecha,##*-[0-9].[0-9].[0-9]-usa un patrón de globbing —no una regex— que consume todo hasta pasado el número de versión.
Solución 2.
#!/usr/bin/env bash
#
# resumen_reservas.sh - Tabla de reservas, noches e importe por casa.
# Autor : Operador de sistemas <operador@srv-tramontana> Fecha: 2026-08-18
set -euo pipefail
readonly CSV="${TRAMONTANA_CSV:-/home/operador/datos/reservas.csv}"
declare -A n_reservas n_noches importes
declare -i total_reservas=0 total_noches=0
todos=""
# Los importes se acumulan como texto separado por espacios, porque Bash
# no suma decimales; la suma la hace awk con LC_ALL=C para el punto.
suma() { LC_ALL=C awk '{ s=0; for (i=1;i<=NF;i++) s+=$i; printf "%.2f", s }' <<<"$1"; }
while IFS=';' read -r _id _fecha casa _huesped noches importe; do
n_reservas["$casa"]=$(( ${n_reservas["$casa"]:-0} + 1 ))
n_noches["$casa"]=$(( ${n_noches["$casa"]:-0} + noches ))
importes["$casa"]="${importes["$casa"]:-}${importe} "
todos+="${importe} "
total_reservas+=1; total_noches+=noches
done < <(tail -n +2 "$CSV")
LC_ALL=C printf '%-14s %9s %8s %12s\n' CASA RESERVAS NOCHES IMPORTE
for casa in $(printf '%s\n' "${!n_reservas[@]}" | LC_ALL=C sort); do
LC_ALL=C printf '%-14s %9d %8d %12s\n' \
"$casa" "${n_reservas[$casa]}" "${n_noches[$casa]}" "$(suma "${importes[$casa]}")"
done
LC_ALL=C printf '%-14s %9d %8d %12s\n' TOTAL "$total_reservas" "$total_noches" "$(suma "$todos")"
exit 0operador@srv-tramontana:~$ ~/scripts/resumen_reservas.sh
CASA RESERVAS NOCHES IMPORTE
cal-ferrer 4 11 1120.00
can-ventos 6 17 1980.00
el-moli 3 8 867.00
la-solana 5 13 1425.50
mas-figueres 7 21 2450.00
TOTAL 25 70 7842.50Coincide con los datos conocidos. La función suma es un adelanto de 04-05, pero ilustra por qué las funciones son inevitables: la misma línea de awk se usa en dos sitios.
Solución 3. Los tres defectos:
- Espacios alrededor del
=.posts = 114intenta ejecutar el comandopostsy el script muere en la primera línea. - El orden de las operaciones trunca el resultado.
$posts/$total*100calcula primero114/412, que en aritmética entera es 0, y luego0*100= 0. Siempre multiplica antes de dividir. - Los
$sobran dentro de$(( ))—ruido, no error— y sobre todo un porcentaje pide un decimal que Bash no puede dar.
posts=114
total=412
# Multiplicar antes de dividir da el entero correcto: 27
pct_entero=$(( posts * 100 / total ))
# Y con un decimal, saliendo a awk con locale fijo:
pct=$(LC_ALL=C awk -v p="$posts" -v t="$total" 'BEGIN { printf "%.1f", p*100/t }')
LC_ALL=C printf 'POST: %s de %s (%s %% entero, %s %% exacto)\n' \
"$posts" "$total" "$pct_entero" "$pct"
# -> POST: 114 de 412 (27 % entero, 27.7 % exacto)Fíjate en el %% del formato: dentro de printf, un % literal se escribe doblado.
Conclusión
Ya no dependes de valores clavados en el código, y de paso te has llevado la parte de Bash que más productividad da por línea escrita.
- Sabes que en Bash todo es una cadena y que el «tipo» lo pone el contexto: nada se valida, comparar números no es comparar texto, y no hay decimales. Asignas sin espacios alrededor del
=, entrecomillas siempre el valor, distingues los ámbitos de shell, entorno y subshell sabiendo que los cambios del hijo nunca vuelven al padre, y usasdeclarecon sus atributos-r,-i,-a,-A,-xy-p. - Dominas la expansión de parámetros completa:
:-,:=,:?y:+, longitudes, subcadenas, recortes con#,##,%,%%, sustituciones con/y//, y cambio de caso. Ya no necesitasbasenamenicutpara lo simple. - Haces aritmética entera con
$(( ))y(( )), conoces la trampa del octal y la de(( i++ )), y sales abcoawkconLC_ALL=Cen cuanto aparece un decimal: el importe medio de Tramontana es 313,70 €. - Manejas arrays indexados —siempre
"${array[@]}", nunca${array[*]}—, cuentas por clave con arrays asociativos tras el imprescindibledeclare -A, formateas conprintf(que ya produce la tabla del informe de Marta), yrevision_salud.shlee su configuración del entorno con valores por defecto.
Pero el script sigue siendo mudo: no sabes pedirle ayuda, no puedes decirle «usa umbral 60» sin exportar una variable, y no hay forma de que lea el mes de un informe. En la siguiente lección, Entrada, Salida y Argumentos de un Script, le damos una interfaz de verdad: parámetros posicionales y la diferencia crítica entre $@ y $*, una función de ayuda, getopts para opciones cortas, las convenciones -h, -v, --dry-run y -- que hacen que un script parezca profesional, read en todas sus variantes y el patrón canónico para leer un fichero línea a línea, la regla de oro de stdout para resultados y stderr para humanos, y los códigos de salida con significado. Al terminarla, revision_salud.sh aceptará opciones y nacerá informe_reservas.sh.
Curso de Linux: De Principiante a Administrador de Sistemas
Módulo 1: Introducción a Linux
- ¿Qué es Linux?
- Historia de Linux
- Distribuciones de Linux
- Instalando Linux
- Primer Contacto con el Sistema
- Estructura del Sistema de Archivos de Linux
Módulo 2: Comandos Básicos de Linux
- Introducción a la Línea de Comandos
- Obtener Ayuda y Documentación del Sistema
- Navegando el Sistema de Archivos
- Operaciones con Archivos y Directorios
- Visualización y Edición de Archivos
- Enlaces Duros y Simbólicos
- Permisos y Propiedad de Archivos
Módulo 3: Habilidades Avanzadas en la Línea de Comandos
- El Entorno del Shell: Variables, Alias e Historial
- Uso de Comodines y Expresiones Regulares
- Búsqueda de Archivos y Contenido: find, locate y grep
- Tuberías y Redirección
- Procesamiento de Texto: cut, sort, uniq, sed y awk
- Gestión de Procesos
- Programación de Tareas con Cron
- Comandos de Redes
Módulo 4: Scripting en Shell
- Introducción al Scripting en Shell
- Variables y Tipos de Datos
- Entrada, Salida y Argumentos de un Script
- Estructuras de Control
- Funciones y Librerías
- Depuración y Manejo de Errores
- Scripts de Producción: Buenas Prácticas
Módulo 5: Administración del Sistema
- Gestión de Usuarios y Grupos
- sudo y Permisos Especiales
- Gestión de Paquetes
- Gestión de Discos
- systemd y la Gestión de Servicios
- Registros del Sistema: journald y syslog
- Monitoreo del Sistema y Optimización del Rendimiento
- Respaldo y Restauración
Módulo 6: Redes y Seguridad
- Configuración de Redes
- SSH y Acceso Remoto
- Firewall y Seguridad Perimetral
- Sistemas de Detección de Intrusos
- Gestión de Secretos y Certificados TLS
- Asegurando Sistemas Linux
Módulo 7: Temas Avanzados
- El Proceso de Arranque y la Recuperación del Sistema
- Diagnóstico Avanzado: strace, perf y eBPF
- Optimización del Kernel de Linux
- Virtualización con Linux
- Contenedores de Linux y Docker
- Automatización con Ansible
- Alta Disponibilidad y Balanceo de Carga
