El toolkit funciona solo, pero la pregunta que cerraba el Módulo 7 sigue abierta: ¿es mantenible? Dentro de seis meses, a las tres de la madrugada, con el vigilante avisando de que el respaldo ha fallado, alguien —probablemente tú— abrirá respaldo.sh y tendrá que entender en dos minutos qué hace la línea 47. Si esa línea dice d=${f##*/}; e=${d%%.*}, no lo conseguirá. En Bash la legibilidad no es estética: es la diferencia entre arreglar una incidencia y provocar otra. Esta lección convierte esa intuición en una guía de estilo concreta y en un refactor real del código que ya tienes escrito.

Contenido

  1. Por qué en Bash la legibilidad es supervivencia
  2. Qué significa «legible» de forma medible
  3. Nombres y espacios de nombres
  4. Formato: indentación, longitud de línea y continuación con \
  5. Llaves, corchetes dobles y uniformidad
  6. La estructura canónica de un script
  7. Comentarios: el porqué, no el qué
  8. Funciones cortas y cláusulas de guarda
  9. Tabla de «esto no / esto sí» y mensajes de salida
  10. La guía de estilo del equipo y un refactor real

  1. Por qué en Bash la legibilidad es supervivencia

Todos los lenguajes premian el código claro, pero Bash tiene tres agravantes que lo convierten en obligación:

  • La sintaxis es densa por diseño. ${envio##*/}, 2>&1, "${arr[@]}", <( ). Has aprendido todas esas construcciones en este curso, pero ninguna se explica sola.
  • No hay compilador ni tipos que te protejan. Un error de nombre en Python explota al ejecutarse; en Bash, $carpeta_destino mal escrito se expande a la cadena vacía y rm -rf "$carpeta_destino"/* borra /*. El único mecanismo de defensa es que un humano lea el código y vea el fallo.
  • Los scripts de operaciones se leen bajo presión. Nadie abre vigilante.sh un martes tranquilo por curiosidad: se abre cuando algo está roto, con prisa y sin margen de error.

A esto se suma una realidad incómoda: los scripts viven mucho más de lo previsto. informe-diario.sh nació como diez líneas para salir del paso y hoy publica el informe que lee la dirección cada mañana. El código temporal es el que más dura.

  1. Qué significa «legible» de forma medible

«Legible» suena subjetivo, pero se aterriza en criterios comprobables en treinta segundos:

Criterio Cómo se mide Umbral práctico
Longitud de función Líneas del bloque Cabe en una pantalla: ≤ 40
Profundidad de anidación Niveles de if/for ≤ 3
Longitud de línea Columnas ≤ 100, cortando con \
Nombres de una letra Búsqueda visual Solo i en bucles cortos
Comentarios que explican el qué Lectura Ninguno: sobran
Cabecera de documentación Presencia Obligatoria en scripts y funciones públicas

La prueba definitiva no es una métrica sino una pregunta: ¿puede otra persona del equipo modificar esta función sin preguntarte nada? Si la respuesta es no, el código no es legible por mucho que a ti te lo parezca.

  1. Nombres y espacios de nombres

El nombre es el comentario que nunca se queda obsoleto:

ciudad_destino="Valencia"                    # variables: minusculas, descriptivas
total_envios_entregados=0
readonly VELOZ_DIR_DATOS="/srv/veloz/datos"  # constantes: MAYUSCULAS, readonly (03-02)
veloz_calcula_porcentaje() { :; }            # funciones: verbo + prefijo (05-06)

Tres reglas. Las variables van en minúsculas, porque las mayúsculas están reservadas por convención al entorno y a las constantes; si llamas PATH a tu variable de rutas, rompes el script entero. Las funciones empiezan por verbo (veloz_calcula_…, veloz_lee_…, veloz_requiere), porque una función hace algo; si no encuentras el verbo, probablemente hace dos cosas. Y el prefijo veloz_ de 05-06 no es decoración: al hacer source lib/comun.sh todas esas funciones entran en el espacio global del shell, y sin prefijo una log tuya chocaría con cualquier otra log. Sobre la longitud: i está bien en for i in {1..3}; f no está bien si se usa treinta líneas más abajo. El nombre debe crecer con la distancia entre su definición y su último uso.

  1. Formato: indentación, longitud de línea y continuación con \

Indenta con 4 espacios (lo que usa este curso) o con 2, pero elige uno y no lo mezcles jamás. Los tabuladores dan problemas dentro de here-documents y se ven distintos en cada editor. Esto no se discute en cada revisión: se fija una vez y lo aplica una herramienta (shfmt, en 08-05).

tar --create --gzip --file "$archivo_destino" \
    --exclude='*.tmp' --directory "$VELOZ_DIR_DATOS" .

grep -F 'ERROR' /var/log/veloz/app.log \
    | awk '{print $4}' \
    | sort | uniq -c | sort -rn

Dos detalles importan. El primero es una trampa clásica: la barra invertida debe ser el último carácter de la línea. Si dejas un espacio detrás, Bash escapa ese espacio en vez del salto de línea, el comando se parte en dos y aparece un error incomprensible del tipo --file: orden no encontrada. Es invisible al ojo; ShellCheck lo detecta (SC1101). El segundo: en una tubería puedes prescindir de la barra si cortas después de la |, porque una línea que acaba en | ya continúa sola. Poner la | al principio de cada línea, como el ejemplo, es más legible porque alinea verticalmente las etapas, pero exige la barra invertida.

  1. Llaves, corchetes dobles y uniformidad

echo "$ciudad"                             # correcto y suficiente
echo "${ciudad}_informe"                   # AQUI las llaves son obligatorias
echo "${envios[2]}" "${ruta##*/}"          # y en arrays y expansiones

Hay dos escuelas: llaves siempre (uniformidad absoluta, más ruido visual) o solo cuando aclaran o son obligatorias. Este curso sigue la segunda; lo que no es opcional es ser uniforme dentro de un mismo fichero. Con las condiciones la decisión viene de 03-03: usa [[ ]] siempre. No solo porque evita el word splitting y admite =~ y &&, sino por uniformidad: si el 90 % del toolkit usa [[ ]], el [ ] restante hace que el lector se pare a preguntarse por qué es distinto. Sorprender al lector es un defecto.

  1. La estructura canónica de un script

Todos los scripts del toolkit siguen el mismo orden. Cuando abres flota.sh sabes que la configuración está arriba y main abajo, y encuentras lo que buscas sin leer el fichero entero.

#!/usr/bin/env bash
#
# estado-servicio.sh - Comprueba la salud de veloz-api y avisa si esta caida
#
# Uso: estado-servicio.sh [-v] [-t SEGUNDOS] [HOST...]
#
# Opciones:  -v  Salida detallada
#            -t SEGUNDOS  Espera maxima por host (por defecto 5)
#
# Codigos de salida:  0 todos responden | 1 error de uso | 69 servicio no disponible
#
# Autor: Equipo de Operaciones - Veloz Envios     Desde: 2026-03-11

set -euo pipefail

# --- Constantes -------------------------------------------------------------
readonly VERSION="1.4.0"
readonly SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
readonly VELOZ_ESPERA_POR_DEFECTO=5

# --- Librerias --------------------------------------------------------------
source "${SCRIPT_DIR}/../lib/comun.sh"

# --- Funciones --------------------------------------------------------------
uso()            { sed -n '3,16p' "${BASH_SOURCE[0]}" | sed 's/^# \{0,1\}//'; }
comprueba_host() { local host="$1"; ...; }
main()           { local espera="$VELOZ_ESPERA_POR_DEFECTO"; ...; }

main "$@"   # --- Punto de entrada: SIEMPRE la ultima linea del fichero

De arriba abajo: el shebang portable (03-01); la cabecera de documentación con uso, opciones, códigos de salida y autoría —lo primero que lee quien abre el fichero, y que uso() reaprovecha con sed (06-02) para no duplicar el texto—; set -euo pipefail con las cautelas de 05-03; las constantes agrupadas y readonly; el source de las librerías usando SCRIPT_DIR para no depender del directorio de trabajo (05-06); las funciones; y main "$@" como última línea del fichero, que garantiza que nada se ejecute hasta que el script esté completamente leído (04-02).

  1. Comentarios: el porqué, no el qué

# MAL: repite lo que el codigo ya dice
# Incrementa el contador en 1
(( contador++ ))
# BIEN: explica una decision que el codigo no puede contar
# Reintentamos 3 veces: la API tarda hasta 8 s en arrancar tras el despliegue
# nocturno y el primer curl siempre falla (incidencia INC-2481).
veloz_espera_servicio 3

El código dice qué hace; solo un comentario puede decir por qué. Los valiosos documentan una decisión no obvia, una limitación de una herramienta externa, una referencia a un incidente o una advertencia («no cambies el orden: flock debe adquirirse antes de crear el temporal»). Y hay algo peor que no comentar: el comentario que miente. Uno que dice «avisa si el disco pasa del 80 %» junto a un código que compara con 90 es una trampa activa, porque el lector confía y no comprueba. Cuando cambies el código, cambia el comentario en la misma edición o bórralo. Para las funciones públicas de lib/comun.sh, cabecera con formato fijo:

# veloz_porcentaje <parte> <total>
# Calcula el porcentaje de <parte> sobre <total> con un decimal.
# Escribe el resultado en la salida estandar, sin el simbolo %.
# Codigos: 0 correcto | 1 argumentos no numericos | 2 total igual a cero

Firma, qué hace, qué escribe en stdout, qué códigos devuelve. Cuatro líneas que ahorran abrir la implementación.

  1. Funciones cortas y cláusulas de guarda

Una función debe hacer una cosa y su nombre debe decir cuál. El síntoma de que hace dos es que en su descripción aparece un «y»: «lee el CSV y genera el informe». El límite práctico: si no cabe en una pantalla, extrae. Extraer es mecánico: (1) localiza el bloque coherente, sus entradas —serán argumentos— y su salida —irá a stdout—; (2) muévelo a una función con nombre de verbo, declarando local todo lo interno (04-02); (3) ejecuta el script y confirma que sigue dando lo mismo. El tercer paso se lo salta todo el mundo y es el único que garantiza que el refactor no cambió nada; en 08-06 lo automatizaremos con Bats.

El otro enemigo es la anidación: a partir del tercer nivel hay que subir con la mirada para saber en qué rama estás. Retomando las cláusulas de guarda de 03-04, este bloque de cuatro niveles con el trabajo útil al fondo…

if [[ -f "$fichero" ]]; then
    if [[ -r "$fichero" ]]; then
        if [[ -s "$fichero" ]]; then
            awk -F, -v c="$objetivo" '$3 == c { print $1 }' "$fichero"
        else veloz_log_error "fichero vacio"; return 1; fi
    else veloz_log_error "sin permiso"; return 1; fi
else   veloz_log_error "no existe"; return 1; fi

…se convierte en esto, con un solo nivel de indentación y errores que dicen cuál es el fichero:

[[ -f "$fichero" ]] || { veloz_log_error "no existe: $fichero"; return 1; }
[[ -r "$fichero" ]] || { veloz_log_error "sin permiso: $fichero"; return 1; }
[[ -s "$fichero" ]] || { veloz_log_error "fichero vacio: $fichero"; return 1; }

awk -F, -v c="$objetivo" '$3 == c { print $1 }' "$fichero"

El patrón es: primero todo lo que descalifica la entrada, después el trabajo.

  1. Tabla de «esto no / esto sí» y mensajes de salida

Reescrituras que aparecen una y otra vez en revisiones de código de Bash:

Esto no Esto sí Por qué
comando; if [ $? -eq 0 ] if comando; then $? se pierde con cualquier orden intermedia (03-04)
cat fichero | grep patron grep patron fichero Un proceso menos y menos ruido (02-04)
for f in $(ls *.log) for f in *.log ls rompe con espacios; el glob nunca (02-05)
echo -e "a\tb" printf 'a\tb\n' echo -e no es portable (03-06)
grep x f | wc -l grep -c x f grep ya sabe contar
if [ x$v = xabc ] if [[ $v == abc ]] El truco de la x sobra con [[ ]]
rm -rf $dir/* rm -rf "${dir:?}"/* Si dir está vacía, borras la raíz (03-06)
T=$(date +%s) inicio_ejecucion=$(date +%s) Nombres, apartado 3

Un script de operaciones tiene dos públicos y hay que servir a los dos: el humano quiere leer y la máquina —el vigilante, un grep, el journal— quiere analizar. La solución es la de 07-04: una línea con estructura fija y campos separados, que sigue siendo legible (2026-08-03T09:15:22+02:00 [INFO] informe-diario: envios procesados: 1284). Tres reglas conviene formalizarlas: los diagnósticos van a stderr y los datos a stdout (02-04), para que datos=$(script) no capture avisos; los errores dicen qué falló, con qué valor y qué hacer («no se puede leer /srv/veloz/datos/envios.csv: comprueba los permisos», no «error»); y el código de salida acompaña siempre al mensaje (05-03), porque quien llama al script no lee texto.

  1. La guía de estilo del equipo y un refactor real

Todo lo anterior cabe en un ESTILO.md dentro del repositorio, junto al código —lo pondremos bajo Git en 08-04—. Una página con las decisiones tomadas y, sobre todo, con las excepciones justificadas que el equipo haya acordado. Es un documento vivo: cuando una discusión se repite por segunda vez en una revisión, la decisión se escribe ahí y deja de discutirse. Como referencia externa, la Google Shell Style Guide es el documento más usado del sector y una buena base, aunque conviene adoptarla con criterio: fija cosas útiles (local, main al final, longitud máxima) y otras discutibles (2 espacios, 80 columnas). Lo importante no es qué guía eliges, sino que exista una y que la aplique una herramienta en lugar de una persona: de eso se encarga shfmt, en 08-05.

Esta función de informe-diario.sh lleva desde el Módulo 4 sin tocarse:

# ANTES
gen() {
    T=0; E=0
    while IFS=, read -r a b c d e f; do
        if [ "$a" != "id_envio" ]; then
            T=`expr $T + 1`
            if [ $e = "entregado" ]; then E=`expr $E + 1`; fi
        fi
    done < $1
    echo "Total: $T Entregados: $E `echo "scale=1;$E*100/$T" | bc`%"
}   # 11 lineas, 6 defectos de estilo y 2 procesos por cada envio del fichero

Y así queda tras aplicar la guía:

# DESPUES
# resume_entregas <fichero_csv>
# Resume totales de un CSV de envios (con cabecera).
# Escribe en stdout: "total entregados porcentaje", separados por espacios.
# Codigos: 0 correcto | 1 el fichero no se puede leer | 2 no hay filas de datos
resume_entregas() {
    local fichero_csv="${1:?falta el fichero CSV}"
    [[ -r "$fichero_csv" ]] || { veloz_log_error "no se puede leer: $fichero_csv"; return 1; }

    local total entregados
    read -r total entregados < <(
        awk -F, 'NR > 1 { total++; if ($5 == "entregado") entregados++ }
                 END { print total + 0, entregados + 0 }' "$fichero_csv"
    )
    (( total > 0 )) || { veloz_log_error "sin filas de datos: $fichero_csv"; return 2; }

    printf '%d %d %s\n' "$total" "$entregados" "$(veloz_porcentaje "$entregados" "$total")"
}

Decisión por decisión: el nombre pasa de gen a resume_entregas, verbo primero y sin ambigüedad. Las globales T/E pasan a local con nombres completos, lo que además evita pisar variables del script. La validación sube al principio como cláusula de guarda, con ${1:?} (03-06) y un mensaje que incluye la ruta. El bucle while read de seis campos —cinco sin usar— se sustituye por un solo awk (06-01): más corto, más rápido (lo mediremos en 08-02) y sin romperse con campos vacíos. El obsoleto `expr` desaparece y las comillas invertidas dan paso a $( ), que anida y se lee. El porcentaje se delega en veloz_porcentaje, que ya existe y está probada, en vez de reimplementarlo con bc. La salida deja de ser una frase decorada y pasa a tres campos que el llamante formatea como quiera y read trocea sin esfuerzo. Y los códigos de salida distinguen «no puedo leer» de «no hay datos», que operativamente son incidencias muy distintas.

Errores Comunes y Consejos

  • Espacio detrás de la barra invertida. El error más frustrante de la lección, porque es invisible. Configura el editor para marcar los espacios finales y deja que ShellCheck (SC1101) los cace.
  • Refactorizar sin poder comprobar. Reescribir a ojo y desplegar es tan arriesgado como no refactorizar. Guarda la salida antes (./informe-diario.sh > /tmp/antes.txt), refactoriza y compara con diff.
  • Confundir «corto» con «legible». [[ -f $f ]]&&. $f||exit 1 es cortísimo e ilegible. El objetivo es la claridad, no la brevedad: los caracteres son gratis.
  • Consejo: lee tu script en voz alta. Si al llegar a una línea tienes que pararte a descifrarla, ahí falta un nombre mejor o un comentario que explique el porqué.
  • Consejo: el mejor momento para refactorizar es cuando vas a tocar el código. No abras una tarea de «limpiar el toolkit»; limpia la función que ya estabas modificando.

Ejercicios

Ejercicio 1. Reescribe esta función aplicando la guía: nombre, local, cláusulas de guarda, [[ ]], comillas, printf y cabecera de documentación.

chk() {
  if [ -d $1 ]; then
    if [ -w $1 ]; then echo -e "OK\t$1"
    else echo "no escribible"; return 1; fi
  else echo "no existe"; return 1; fi
}

Ejercicio 2. Localiza los cinco defectos de estilo de este fragmento y corrígelos, explicando cada uno.

for f in `ls /var/log/veloz/*.log`; do
    cat $f | grep ERROR | wc -l > /tmp/c
    if [ $? -eq 0 ]; then echo "$f: `cat /tmp/c`"; fi
done

Soluciones

Solución 1.

# comprueba_directorio_escribible <ruta>
# Verifica que <ruta> es un directorio en el que el usuario actual puede escribir.
# Escribe en stdout una linea "OK <ruta>" si todo es correcto.
# Codigos: 0 correcto | 1 no existe o no es directorio | 2 existe pero no es escribible
comprueba_directorio_escribible() {
    local ruta="${1:?falta la ruta}"
    [[ -d "$ruta" ]] || { veloz_log_error "no es un directorio: $ruta"; return 1; }
    [[ -w "$ruta" ]] || { veloz_log_error "sin permiso de escritura: $ruta"; return 2; }
    printf 'OK\t%s\n' "$ruta"
}

Nombre descriptivo con verbo; local con ${1:?} para fallar pronto si falta el argumento; guardas en lugar de anidación; [[ ]] con la variable entrecomillada, porque sin comillas una ruta con espacios rompería el test; printf en vez de echo -e, que no es portable; los errores a stderr vía veloz_log_error para no contaminar la salida útil; dos códigos distintos porque «no existe» y «no puedo escribir» se arreglan de forma diferente; y cabecera fija.

Solución 2.

for fichero in /var/log/veloz/*.log; do
    [[ -f "$fichero" ]] || continue
    errores=$(grep -c 'ERROR' "$fichero" || true)
    printf '%s: %d\n' "$fichero" "$errores"
done

Los cinco defectos: (1) for f in $(ls ...) recorre la salida de ls, que se parte por espacios; el glob directo es correcto y además no crea procesos. (2) cat | grep | wc -l son tres procesos donde basta grep -c. (3) El temporal /tmp/c es innecesario, no es único —dos ejecuciones simultáneas se pisan— y es inseguro (lo veremos en 08-03); una variable basta. (4) if [ $? -eq 0 ] comprueba el código de la última orden de la tubería, que era wc, no grep: siempre da 0 y la condición no comprueba nada. (5) Las variables sin comillas. El || true es necesario porque grep -c devuelve 1 cuando no encuentra nada y con set -e eso abortaría el bucle (05-03); la guarda [[ -f ]] cubre el caso de que el glob no encaje con ningún fichero.

Conclusión

En Bash la legibilidad es defensa activa: sin compilador ni tipos, el único filtro entre un $carpeta mal escrito y un rm -rf / es que alguien lea el código y lo entienda. «Legible» se mide: funciones que caben en una pantalla, tres niveles de anidación como máximo, líneas de menos de cien columnas y ningún comentario que repita lo que el código ya dice. Las convenciones del toolkit son minúsculas para las variables, MAYÚSCULAS readonly para las constantes, verbo al principio de cada función y prefijo veloz_ para no chocar al hacer source. El formato se fija una vez —4 espacios, [[ ]] siempre, llaves cuando aclaran, continuación con \ sin espacio detrás— y lo aplica una herramienta, no una discusión. Todos los scripts comparten la misma estructura: shebang, cabecera de documentación con uso y códigos de salida, set -euo pipefail, constantes, source de librerías, funciones y main "$@" en la última línea. Los comentarios explican el porqué —la decisión, la limitación, el incidente— y se actualizan con el código, porque un comentario que miente es peor que ninguno. Las funciones hacen una sola cosa, y cuando aparece un «y» en su descripción se extraen; la anidación profunda se disuelve en cláusulas de guarda que sacan los errores al principio. La tabla de reescrituras resume el resto: if comando en vez de $?, glob en vez de ls, printf en vez de echo -e, grep -c en vez de grep | wc -l. Y el refactor de resume_entregas demuestra que legibilidad y calidad llegan juntas: la versión clara es también más corta, más rápida y con mejores códigos de error.

Justo ahí aparece una duda razonable: hemos sustituido un bucle while read por un awk afirmando que es «más rápido». ¿Cuánto más? ¿Importa con 1.200 envíos? ¿Y con 500.000? Eso no se adivina, se mide. La lección 08-02 enseña a medir de verdad con time, SECONDS y date +%s%N, a identificar el coste que domina en Bash —crear procesos— y a optimizar solo lo que de verdad cuesta, empezando por perfilar informe-diario.sh sobre medio millón de líneas.

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