En la lección anterior escribiste las validaciones que informe-diario.sh necesitaba, pero encadenadas con || y llaves: un estilo que aguanta bien dos comprobaciones y se vuelve ilegible con cinco. Ahora les damos su forma definitiva con if. Y por el camino descubrirás la idea que más veces se explica mal en los cursos de Bash: if no evalúa un booleano, evalúa un código de salida. Entender eso cambia por completo la manera en que se escriben las condiciones, y explica por qué if grep -q ERROR fichero —sin ningún corchete— es la construcción más idiomática del lenguaje.
Contenido
- Sintaxis de
if/then/fi - La idea clave:
ifevalúa un código de salida elseyelif- Condicionales de una línea
ifsobre comandos reales- Condiciones compuestas, negación y validación de entrada
- Anidamiento, cláusulas de guarda y salida temprana
- Códigos de salida distintos según el error
informe-diario.shvalida su entorno
- Sintaxis de
if / then / fi
if / then / fiLa estructura básica es if condición, luego then con los comandos y fi para cerrar. Habitualmente se escribe con el then en la misma línea, separado por un punto y coma:
Tres detalles que causan la mayoría de los errores iniciales: fi cierra el bloque (es if al revés, convención heredada del shell de Bourne, igual que case/esac); el ; antes de then es obligatorio si van en la misma línea, porque si no Bash lee then como un argumento más de la condición; y la indentación es puramente estética para Bash, pero imprescindible para quien lee —cuatro espacios en este curso—.
- La idea clave:
if evalúa un código de salida
if evalúa un código de salidaAquí está el concepto que hay que interiorizar. En la mayoría de lenguajes, if recibe una expresión booleana. En Bash, if recibe un comando, lo ejecuta y mira su código de salida. Si es 0, ejecuta el bloque then; si es distinto de 0, no. Esto tiene una consecuencia que desconcierta al venir de otros lenguajes: 0 significa verdadero, al revés de lo habitual. La razón es que en Unix 0 es «sin errores» y hay muchas formas de fallar, cada una con su número (lección 03-01).
Y tiene otra consecuencia mucho más importante: si if ejecuta un comando cualquiera, entonces [[ ... ]] no es sintaxis especial de if, sino simplemente el comando que sueles poner ahí. Compruébalo:
Ejecuta [[ -f /srv/veloz/datos/envios.csv ]] seguido de echo $? y obtendrás 0 si el fichero existe y 1 si no. Ese [[ ]] funciona perfectamente fuera de cualquier if, porque es un comando por derecho propio. Por eso if [[ -f "$RUTA_CSV" ]] e if test -f "$RUTA_CSV" son idénticos para Bash.
Retén esta frase: cualquier comando puede ir en un if. Es la puerta de entrada al apartado 5, que es donde if despliega toda su potencia.
else y elif
else y elifelse cubre el caso contrario, y elif (contracción de else if) encadena condiciones alternativas:
if (( total_errores == 0 )); then
echo "Jornada sin errores en veloz-api"
elif (( total_errores < UMBRAL_ERRORES )); then
echo "Errores dentro de lo tolerable: $total_errores"
elif (( total_errores < UMBRAL_ERRORES * 2 )); then
echo "AVISO: $total_errores errores, por encima del umbral"
else
echo "CRÍTICO: $total_errores errores, más del doble del umbral"
fi # con total_errores=73 → AVISO: 73 errores...Bash evalúa las condiciones en orden y se detiene en la primera verdadera. Por eso el orden importa: si pusieras primero total_errores < UMBRAL_ERRORES * 2, esa rama capturaría también los casos normales. Ordena siempre de lo más específico a lo más general. Puedes encadenar tantos elif como quieras, pero cuando pasas de cuatro o cinco comparando el mismo valor contra valores fijos, la herramienta adecuada es case (04-05).
- Condicionales de una línea
Cuando el cuerpo es un solo comando corto, cabe todo en una línea usando puntos y coma. La alternativa, ya conocida de 03-03, es && y ||:
if [[ -z $ciudad ]]; then ciudad="Valencia"; fi # un ; antes de then, otro antes de fi
[[ -z $ciudad ]] && ciudad="Valencia" # equivalente, más compactoEn la forma larga, cada ; sustituye a un salto de línea. ¿Cuál usar? La tabla resuelve la duda:
| Situación | Estilo recomendado |
|---|---|
| Una acción corta, sin alternativa | condición && acción |
| Una acción solo si algo falla | condición || acción |
| Validación que aborta | condición || { mensaje; exit N; } |
| Dos ramas, o más de un comando en el cuerpo | if ... else ... fi |
| Tres o más casos | if/elif o case (04-05) |
La regla de fondo es de legibilidad: && y || para reflejos de una línea, if para lógica. Y nunca uses a && b || c como sustituto de if/else, por el motivo que ya viste en 03-03: si b falla, c se ejecuta igualmente.
if sobre comandos reales
if sobre comandos realesEste es el apartado más importante de la lección. Como if evalúa códigos de salida, puedes poner directamente el comando que te interesa sin corchetes, lo que resulta más corto, más rápido y más expresivo:
if grep -q ERROR "$RUTA_APP_LOG"; then # idiomático
echo "Hay errores registrados hoy"
fi
n=$(grep -c ERROR "$RUTA_APP_LOG") # alternativa torpe
if [[ $n -gt 0 ]]; then echo "Hay errores"; fiLa primera versión es mejor por tres motivos: grep -q para de leer en cuanto encuentra la primera coincidencia (en un log de un gigabyte, la diferencia es abismal), no crea un subshell para capturar la salida, y expresa la intención directamente. Usa la segunda solo cuando necesites de verdad el número.
Los usos idiomáticos que más veces escribirás son estos:
if command -v jq >/dev/null 2>&1; then # ¿está instalada la herramienta?
echo "jq disponible, se usará para la salida JSON"
else
echo "AVISO: jq no instalado, salida en texto plano" >&2
fi
ping -c1 -W2 srv-veloz-01 >/dev/null 2>&1 && echo "servidor accesible"
systemctl is-active --quiet veloz-api && echo "veloz-api en marcha"Presta atención a command -v jq >/dev/null 2>&1: es la forma correcta y portable de comprobar si un programa existe. Verás por ahí which jq, pero which es un programa externo que no está en todos los sistemas y cuyo código de salida no es fiable; command -v es un builtin de Bash (lección 01-04) y funciona siempre. La redirección a /dev/null —el patrón de 02-04 aplicado a las condiciones— silencia la salida en los tres casos, porque aquí solo nos interesa el código de salida; sin ella verías las estadísticas del ping en mitad del informe.
- Condiciones compuestas, negación y validación de entrada
Para combinar condiciones, lo natural en Bash es usar && y || dentro de [[ ]], como viste en 03-03: if [[ -f "$RUTA_CSV" && -r "$RUTA_CSV" && -s "$RUTA_CSV" ]] pregunta de una vez si el CSV existe, es legible y tiene contenido.
También puedes combinar comandos completos con && en la condición del if, algo menos conocido pero perfectamente válido, como en if command -v jq >/dev/null && [[ -f "$RUTA_JSON" ]]; then.
La negación se escribe con !, y puede aplicarse tanto dentro como fuera de los corchetes:
if ! grep -q ERROR "$RUTA_APP_LOG"; then # negando un comando externo
echo "Jornada limpia: ningún error registrado"
fi
[[ ! -f "$RUTA_CSV" ]] && echo "Falta el fichero de envíos" >&2 # dentro del testCuidado con un detalle de estilo: if ! [[ -f "$f" ]] y if [[ ! -f "$f" ]] son equivalentes, pero la segunda se lee mejor porque mantiene la negación pegada a lo que niega. Y if ! comando es la única forma cuando lo que niegas es un comando externo.
Los operadores de fichero de 03-03 encuentran aquí su hogar natural. Un script serio nunca supone que sus datos de entrada están donde deberían:
if [[ ! -e "$RUTA_CSV" ]]; then echo "ERROR: no existe $RUTA_CSV" >&2; exit 4
elif [[ ! -r "$RUTA_CSV" ]]; then echo "ERROR: no puedo leerlo" >&2; exit 4
elif [[ ! -s "$RUTA_CSV" ]]; then echo "ERROR: está vacío" >&2; exit 4
fiEse bloque es didáctico pero exagerado: distinguir cada causa con su mensaje es excelente para diagnosticar y excesivo para un script pequeño. La versión condensada que usarás en la práctica agrupa las tres en if [[ ! -r "$RUTA_CSV" || ! -s "$RUTA_CSV" ]]. El criterio para elegir entre ambas: cuanto más lejos esté quien lea el error, más específico debe ser el mensaje. Un script que ejecutas tú puede permitirse mensajes genéricos; uno que corre por cron a las siete de la mañana y solo deja rastro en un log necesita decir exactamente qué falló.
No olvides validar también las variables, no solo los ficheros: [[ -z $ciudad ]] && { echo "ERROR: no se ha indicado ciudad" >&2; exit 2; }. Esta comprobación cobrará todo su sentido en 03-05, cuando la ciudad llegue como argumento de la línea de comandos y pueda perfectamente no llegar.
- Anidamiento, cláusulas de guarda y salida temprana
Un if puede contener otro if, pero el anidamiento se vuelve ilegible muy rápido:
if [[ -f "$RUTA_CSV" ]]; then # la lógica útil queda a tres niveles
if [[ -r "$RUTA_CSV" ]]; then
if [[ -s "$RUTA_CSV" ]]; then
tail -n +2 "$RUTA_CSV" | cut -d, -f5 | sort | uniq -c
fi
fi
fi # ...y si el fichero no existe, nadie se enteraEl problema no es solo estético. Con esa forma, si el fichero no existe el script no dice nada y termina como si todo hubiera ido bien, porque no hay ninguna rama que trate el fallo. Es un fallo silencioso de manual. La solución profesional se llama cláusula de guarda: invierte cada condición, trata el caso malo primero y sal de inmediato, dejando el código útil al final y sin indentar.
[[ -f "$RUTA_CSV" ]] || { echo "ERROR: no existe $RUTA_CSV" >&2; exit 4; }
[[ -r "$RUTA_CSV" ]] || { echo "ERROR: no puedo leer $RUTA_CSV" >&2; exit 4; }
[[ -s "$RUTA_CSV" ]] || { echo "ERROR: $RUTA_CSV vacío" >&2; exit 4; }
tail -n +2 "$RUTA_CSV" | cut -d, -f5 | sort | uniq -cLa segunda versión gana en cuatro frentes: pasa de tres niveles de indentación a ninguno, dice qué falló con un mensaje específico, devuelve un código de salida distinto de cero en lugar de fingir éxito, y admite una comprobación nueva añadiendo una línea en vez de otro nivel.
La regla, aplicable a cualquier lenguaje: valida y sal al principio; deja el trabajo real al final. Si te encuentras con más de dos niveles de if, casi siempre hay una guarda esperando a ser extraída.
- Códigos de salida distintos según el error
Ya sabes que exit N es el contrato con quien te llama (03-01), y las condicionales son el mecanismo que permite cumplirlo con precisión: cada motivo de fallo, su propio número. Para informe-diario.sh fijamos esta tabla, que documentaremos en la cabecera del fichero:
| Código | Significado | Código | Significado |
|---|---|---|---|
0 |
Informe correcto | 3 |
No se puede leer app.log |
1 |
Error inesperado | 4 |
envios.csv ausente, ilegible o vacío |
2 |
Uso incorrecto (03-05) | 5 |
Destino de informes no escribible |
La ventaja es concreta: quien invoque el script podrá reaccionar de forma distinta a cada fallo sin analizar mensajes de texto —por ejemplo, con un case $? que avise al equipo de datos solo cuando el código sea 4—. Ese case es un adelanto de 04-05. Existe además una herramienta que aborta el script automáticamente ante cualquier comando fallido, set -e, junto con set -u y trap: son el arsenal completo del manejo de errores y se estudian en 05-03. Por ahora, las guardas explícitas te dan más control y te obligan a pensar qué debe pasar en cada caso.
informe-diario.sh valida su entorno
informe-diario.sh valida su entornoReunimos todo; esta versión ya se puede poner en producción sin miedo:
#!/usr/bin/env bash
# informe-diario.sh - Resumen diario de actividad de Veloz Envíos
# Autor : Joan Costa <[email protected]> · Uso: informe-diario.sh
# Códigos : 0 correcto | 3 log ilegible | 4 CSV inválido | 5 destino no escribible
# --- 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"
# --- Validaciones previas (cláusulas de guarda) -----------------------
[[ -r "$RUTA_APP_LOG" ]] || { echo "ERROR: no leo $RUTA_APP_LOG" >&2; exit 3; }
[[ -f "$RUTA_CSV" && -s "$RUTA_CSV" ]] \
|| { echo "ERROR: $RUTA_CSV no existe o está vacío" >&2; exit 4; }
mkdir -p "$DIR_INFORMES" || { echo "ERROR: no creo $DIR_INFORMES" >&2; exit 5; }
[[ -w "$DIR_INFORMES" ]] || { echo "ERROR: $DIR_INFORMES no escribible" >&2; exit 5; }
# --- Datos e informe --------------------------------------------------
total_errores=$(grep -c ERROR "$RUTA_APP_LOG")
total_envios=$(tail -n +2 "$RUTA_CSV" | wc -l)
echo " INFORME DIARIO - VELOZ ENVIOS ($SERVIDOR) · $(date '+%F %T')"
echo "-- Errores en app.log --"
if (( total_errores == 0 )); then
echo "Sin errores registrados. Jornada limpia."
elif (( total_errores < UMBRAL_ERRORES )); then
echo "$total_errores errores (umbral: $UMBRAL_ERRORES). Dentro de lo normal."
else
echo "AVISO: $total_errores errores, por encima del umbral de $UMBRAL_ERRORES."
echo "Últimos 3 errores registrados:"
grep ERROR "$RUTA_APP_LOG" | tail -3
fi
echo "-- Envíos por estado ($total_envios en total) --"
tail -n +2 "$RUTA_CSV" | cut -d, -f5 | sort | uniq -c | sort -rn
# --- Calidad del dato -------------------------------------------------
grep -q ',entregado,' "$RUTA_CSV" \
|| echo "ATENCIÓN: ningún envío entregado hoy. ¿Datos incompletos?" >&2
exit 0Ejecutado en un día con incidencias:
INFORME DIARIO - VELOZ ENVIOS (srv-veloz-01) · 2026-08-03 07:00:04
-- Errores en app.log --
AVISO: 73 errores, por encima del umbral de 50.
Últimos 3 errores registrados:
2026-08-03 06:41:02 [ERROR] timeout consultando ruta de reparto id=88213
2026-08-03 06:44:57 [ERROR] fallo al geocodificar dirección de Bilbao
2026-08-03 06:52:11 [ERROR] respuesta 500 del proveedor de rutas
-- Envíos por estado (1247 en total) --
981 entregado
148 en_reparto
118 incidenciaEl salto cualitativo es real. El script ahora comprueba su entorno antes de trabajar y aborta con un código específico si algo falla; interpreta el recuento de errores en lugar de limitarse a imprimirlo; añade contexto útil (los tres últimos errores) solo cuando hace falta, para no llenar de ruido los días tranquilos; y detecta una anomalía de datos que ningún operador habría notado leyendo cifras. Eso ya no es una tubería guardada en un fichero: es una herramienta de operaciones.
Errores Comunes y Consejos
- Olvidar
fi. Bash daerror sintáctico: final de fichero inesperado, señalando la última línea del script en lugar delifculpable. - Olvidar el
;antes dethen. Produceerror sintáctico cerca del elemento inesperado 'then'. - Escribir
elseifoelse if. En Bash eselif. (else iffunciona, pero abre unifnuevo que necesita su propiofi.) - Creer que
ifnecesita corchetes.if grep -q ...es válido e idiomático: los corchetes son un comando más. - Confundir 0 con falso. En Bash 0 es éxito, es decir, verdadero para un
if. - Comparar números con
>dentro de[[ ]]. Es comparación textual (03-03). Usa-gto(( )). - Anidar tres niveles de
if. Invierte las condiciones y usa cláusulas de guarda. - Validar sin avisar. Un
ifque detecta el problema y no imprime nada ni sale con código de error deja al script fallando en silencio: todo error va a stderr con>&2y conexit N.
Ejercicios
Ejercicio 1 — De anidado a guardas. Reescribe este bloque con cláusulas de guarda, mensajes en stderr y códigos de salida distintos (3 para el directorio, 4 para el fichero, 5 para el permiso).
if [[ -d /srv/veloz/datos ]]; then
if [[ -f /srv/veloz/datos/envios.csv ]]; then
if [[ -r /srv/veloz/datos/envios.csv ]]; then
wc -l /srv/veloz/datos/envios.csv
fi
fi
fiEjercicio 2 — Comprobación previa de herramientas. Escribe un fragmento para informe-diario.sh que verifique que existen grep, cut, sort y uniq antes de empezar, avise por stderr de cuál falta y salga con código 6. Además, si existe column, debe activar una variable formato_tabla="si" para presentar el informe alineado; si no, dejarla en "no" y seguir sin abortar.
Ejercicio 3 — Clasificar la jornada. A partir de la variable incidencias, imprime «jornada excelente» si es 0, «jornada normal» entre 1 y 50, «revisar rutas» entre 51 y 150, y «escalar a dirección» a partir de 151. Explica por qué no hace falta escribir los límites inferiores de cada rango.
Soluciones
Solución al Ejercicio 1
readonly DIR_DATOS="/srv/veloz/datos"
readonly RUTA_CSV="$DIR_DATOS/envios.csv"
[[ -d "$DIR_DATOS" ]] || { echo "ERROR: no existe $DIR_DATOS" >&2; exit 3; }
[[ -f "$RUTA_CSV" ]] || { echo "ERROR: no existe $RUTA_CSV" >&2; exit 4; }
[[ -r "$RUTA_CSV" ]] || { echo "ERROR: no puedo leer $RUTA_CSV" >&2; exit 5; }
wc -l "$RUTA_CSV"Además de eliminar los tres niveles de indentación, esta versión corrige un fallo grave del original: el bloque anidado terminaba con código 0 aunque no hubiera hecho nada. Un cron que lo ejecutara habría dado por bueno un día en el que el fichero de datos ni siquiera existía. Nota también que las rutas se han extraído a constantes, aplicando 03-02.
Solución al Ejercicio 2
for herramienta in grep cut sort uniq; do
command -v "$herramienta" >/dev/null 2>&1 \
|| { echo "ERROR: falta la herramienta '$herramienta'" >&2; exit 6; }
done
if command -v column >/dev/null 2>&1; then
formato_tabla="si"
else
formato_tabla="no"; echo "AVISO: 'column' no disponible" >&2
fiEl for es un adelanto de 04-01, pero encaja de forma natural aquí; sin él tendrías que repetir el mismo if cuatro veces. Lo importante es el criterio de diseño: grep, cut, sort y uniq son dependencias duras —sin ellas el script no puede trabajar, así que aborta—, mientras que column es una mejora opcional —su ausencia degrada la presentación pero no impide generar el informe, así que solo avisa—. Distinguir ambas es una de las decisiones que separan un script robusto de uno frágil.
Solución al Ejercicio 3
if (( incidencias == 0 )); then echo "Jornada excelente: ninguna incidencia"
elif (( incidencias <= 50 )); then echo "Jornada normal: $incidencias"
elif (( incidencias <= 150 )); then echo "Revisar rutas: $incidencias"
else echo "Escalar a dirección: $incidencias"
fiAl evaluarse de arriba abajo y detenerse en la primera condición verdadera, los límites inferiores son implícitos: la rama <= 150 solo se alcanza si las dos anteriores fallaron, así que ya se sabe que el valor es mayor que 50. Escribir (( incidencias > 50 && incidencias <= 150 )) sería correcto pero redundante, y añadiría una oportunidad más de equivocarse al ajustar los umbrales.
Conclusión
Tu script ya piensa. Dominas la sintaxis de if/then/elif/else/fi con sus trampas de puntuación; y sobre todo entiendes la idea central del capítulo, que if evalúa un código de salida y no un booleano, lo que convierte [[ ]] en un comando más y abre la puerta a if grep -q ... o if command -v ..., que es como escribe Bash la gente que lleva años haciéndolo. Sabes cuándo un && de una línea es más legible que un if de tres; niegas con ! sin ambigüedad; validas ficheros y variables antes de usarlos; y has cambiado el anidamiento por cláusulas de guarda que dejan la lógica útil sin indentar, con un código de salida propio para cada fallo.
informe-diario.sh valida su entorno, interpreta el umbral, añade contexto solo cuando hace falta y detecta anomalías en los datos. Pero le queda una limitación de fondo: siempre hace exactamente lo mismo. Analiza el día actual y todas las ciudades, y no hay manera de pedirle el informe del lunes pasado o solo el de Bilbao sin editar el fichero. Sigue siendo, en ese sentido, tan rígido como el alias que dejamos atrás en el Módulo 2.
En la lección 03-05 eso se acaba. Aprenderás a recibir información desde fuera: parámetros posicionales $1 y $@, la diferencia crítica entre "$@" y "$*", argumentos con nombre como --fecha y --ciudad, getopts, valores por defecto y entrada interactiva con read. Al terminar, informe-diario.sh --fecha 2026-07-28 --ciudad Bilbao será un comando real, con su --help y todo.
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
