informe-diario.sh ya guarda sus rutas en constantes y sus recuentos en variables, pero sigue siendo ciego: tiene un UMBRAL_ERRORES=50 que nadie compara con nada, y da por hecho que envios.csv existe. Para que el script empiece a decidir necesita antes un vocabulario: los operadores. En esta lección aprenderás a encadenar comandos según su resultado, a comprobar si un fichero existe, a comparar números y cadenas —y a no confundir ambas cosas, que es el error más frecuente del scripting en Bash—. Todavía no escribiremos ningún if: eso es la lección siguiente. Aquí construimos las piezas que irán dentro.

Contenido

  1. Panorama: cuatro familias de operadores
  2. Operadores de control de flujo: ;, &&, ||, !
  3. Agrupación: { ...; } frente a ( ... )
  4. El comando test y sus tres formas
  5. Comparación numérica frente a comparación de cadenas
  6. Cadenas: igualdad, orden y vacío
  7. Operadores de fichero
  8. Operadores lógicos dentro de [[ ]]
  9. Aritmética como operador: (( )) y $(( ))
  10. Precedencia y paréntesis

  1. Panorama: cuatro familias de operadores

En Bash conviven varios sistemas de operadores que se parecen pero no son intercambiables. Distinguirlos desde el principio te ahorrará horas de desconcierto:

Familia Símbolos Dónde se escriben Qué operan
Control de flujo ; && || ! & Entre comandos Códigos de salida
Comparación (test) -eq -lt = -f -z Dentro de [ ] o [[ ]] Cadenas, números y ficheros
Aritméticos + - * / % > < Dentro de (( )) o $(( )) Solo enteros
Expansión ${var:-x} $(cmd) En cualquier palabra Texto

Los tres primeros grupos son el contenido de esta lección; la familia de expansión es la de 03-06. La confusión típica está entre las filas 2 y 3: > dentro de [[ ]] compara texto alfabéticamente, mientras que > dentro de (( )) compara números. Mismo símbolo, semántica distinta según el contexto.

  1. Operadores de control de flujo: ;, &&, ||, !

Todo comando en Linux devuelve un código de salida: 0 si tuvo éxito, distinto de 0 si falló (lección 03-01). Los operadores de control de flujo encadenan comandos usando ese código como criterio.

Operador Nombre Comportamiento
; Secuencia Ejecuta el segundo siempre, pase lo que pase con el primero
&& Y lógico Ejecuta el segundo solo si el primero tuvo éxito (código 0)
|| O lógico Ejecuta el segundo solo si el primero falló (código ≠ 0)
! Negación Invierte el código de salida del comando
& Segundo plano Lanza el comando sin esperarlo (02-06)
mkdir -p ~/veloz-ops/logs ; echo "seguimos pase lo que pase"
mkdir -p ~/veloz-ops/logs && echo "directorio listo"   # solo si el mkdir fue bien
grep -q ERROR /var/log/veloz/app.log || echo "hoy no hay errores"

Se llaman de evaluación perezosa (o cortocircuito) porque Bash solo ejecuta el segundo comando si hace falta para conocer el resultado: con &&, si el primero falla ya sabe que el conjunto falla; con ||, si el primero funciona ya sabe que el conjunto funciona.

El idioma más valioso del apartado es comando_crítico || exit 1, que se lee «haz esto o, si falla, aborta». Es la forma más compacta de escribir una validación:

cd /srv/veloz/datos || exit 1     # si el cd falla, no sigas trabajando a ciegas
grep -q ERROR "$LOG" && echo "hay errores" || echo "todo limpio"
! grep -q ERROR "$LOG" && echo "el log está limpio"    # ! invierte el resultado

La primera línea es importante: un cd que falla y un script que continúa es una receta para desastres, porque los comandos siguientes se ejecutarán en el directorio equivocado.

Cuidado con el patrón de la segunda línea: no es un if/else de verdad. Si echo "hay errores" fallase (por ejemplo, con el disco lleno), se ejecutaría también la rama del ||. Sirve para casos triviales; para lógica real usa if, que es 03-04.

  1. Agrupación: { ...; } frente a ( ... )

Cuando el && o el || deben afectar a varios comandos, hay que agruparlos. Bash ofrece dos formas con una diferencia crucial:

Forma Dónde se ejecuta Sintaxis
{ cmd1; cmd2; } En el shell actual Necesita espacios tras { y ; (o salto) antes de }
( cmd1; cmd2 ) En un subshell (proceso hijo) Sin requisitos especiales
[[ -f "$RUTA_CSV" ]] || { echo "Falta el CSV de envíos" >&2; exit 3; }

Esa línea es el patrón de validación que más veces escribirás: si el fichero no existe, imprime un error en stderr (>&2, lección 02-04) y sale con el código 3. Las llaves son imprescindibles aquí: sin ellas, el exit 3 se ejecutaría siempre.

Los espacios de las llaves no son opcionales: {echo hola;} da error de sintaxis porque Bash lee {echo como un solo nombre de comando, y hace falta el ; (o un salto de línea) antes de la llave de cierre.

Los paréntesis crean un subshell, con todo lo que eso implica según 01-04: los cambios de directorio y las variables definidas dentro no salen. Por eso ( cd /srv/veloz/datos && wc -l envios.csv ) no altera tu directorio actual. Regla práctica: llaves por defecto (no cuestan un proceso) y paréntesis solo cuando quieras aislamiento deliberado.

  1. El comando test y sus tres formas

Aquí llega la pieza central. Para preguntar «¿existe este fichero?» o «¿es este número mayor que aquel?», Bash usa test. Y test es un comando, no una sintaxis especial: recibe argumentos y devuelve 0 (verdadero) o 1 (falso).

Existen tres formas de invocarlo, históricamente distintas: test -f "$csv" (forma original), [ -f "$csv" ] (sinónimo POSIX) y [[ -f "$csv" ]] (palabra clave de Bash).

Que [ sea un comando y no un símbolo explica el error más desconcertante del principiante: [-f fichero] falla con orden no encontrada, porque Bash busca un programa llamado [-f. Los espacios interiores son obligatorios, y el ] es literalmente el último argumento que [ exige recibir.

Aspecto [ ] (comando test) [[ ]] (palabra clave de Bash)
Portabilidad POSIX: funciona en sh, dash, todos Solo Bash, Zsh, Ksh
Variables sin comillas Peligroso: si está vacía, error de sintaxis Seguro: no hay división en palabras
Globbing del valor Se expande, con sorpresas No se expande
&& y || internos No los admite; hay que usar -a / -o Los admite con precedencia correcta
Comparación de patrones No Sí: [[ $ciudad == Val* ]]
Expresiones regulares No Sí: [[ $l =~ ^ERROR ]] (05-04)
Comparación numérica con < No (compara texto) Tampoco: usa -lt o (( ))

La diferencia práctica más importante es la segunda fila. Con ciudad="", la expresión [ $ciudad = "Valencia" ] produce bash: [: =: se esperaba una expresión unaria, mientras que [[ $ciudad = "Valencia" ]] funciona y simplemente resulta falsa.

Con [ ], la variable vacía desaparece antes de que test la vea, y este recibe [ = Valencia ], que no tiene sentido. Con [[ ]], Bash no expande palabras dentro, así que no hay problema. La solución con [ ] es entrecomillar siempre ([ "$ciudad" = "Valencia" ]), pero es una precaución que hay que recordar cada vez.

Recomendación de este curso: usa [[ ]] siempre que escribas para Bash. Solo recurre a [ ] cuando el script deba funcionar con sh puro (08-07).

  1. Comparación numérica frente a comparación de cadenas

Este es el apartado que más errores previene. Como en Bash todo es texto (03-02), el shell no puede adivinar si quieres comparar números o palabras: se lo tienes que decir con el operador.

Comparación Numérica De cadenas
Igual / distinto -eq / -ne = o == / !=
Menor / menor o igual -lt / -le < / (no existe)
Mayor / mayor o igual -gt / -ge > / (no existe)

Los nombres vienen del inglés y son fáciles de recordar: equal, not equal, less than, less or equal, greater than, greater or equal.

Y ahora el error clásico, que conviene ver con números concretos:

[[ $total_errores -gt $UMBRAL_ERRORES ]]  # ¿más errores de los tolerables?
[[ 100 -gt 9 ]]   ; echo $?    # 0 → verdadero: 100 es mayor que 9
[[ "100" > "9" ]] ; echo $?    # 1 → FALSO: "100" va antes que "9" alfabéticamente

La última comparación es alfabética: carácter a carácter, 1 viene antes que 9, así que "100" es «menor» que "9". Es exactamente el mismo criterio con el que un diccionario coloca «casa» antes que «zapato». Si comparas números con > o <, tu script funcionará por casualidad con cifras de la misma longitud y fallará el día que los errores pasen de 9 a 100.

Regla: números con -eq -ne -lt -le -gt -ge; texto con = == != < >. Sin excepciones. Dos avisos finales. Dentro de [ ], los símbolos < y > deben escribirse escapados (\<, \>) porque si no Bash los interpreta como redirecciones y crea un fichero llamado 9. Dentro de [[ ]] no hace falta. Y los operadores numéricos exigen que ambos lados sean enteros: si la variable está vacía o contiene texto, obtendrás se esperaba una expresión entera.

  1. Cadenas: igualdad, orden y vacío

Además de comparar, a menudo necesitas saber si una variable tiene contenido:

Operador Verdadero si…
-z "$var" / -n "$var" La cadena tiene longitud cero / no está vacía
$a = $b / $a != $b Son idénticas (en [[ ]], == es sinónimo) / son distintas
$a < $b $a va antes alfabéticamente (según la configuración regional)
[[ -n $ciudad ]] || { echo "Falta indicar la ciudad" >&2; exit 2; }
[[ $ciudad == Val* ]] && echo "empieza por Val"
[[ ${ciudad,,} == "valencia" ]] && echo "coincide sin distinguir mayúsculas"

La segunda línea muestra algo que solo [[ ]] permite: el lado derecho sin comillas se trata como un patrón de globbing (02-05), así que Val* casa con cualquier ciudad que empiece por «Val». Si lo entrecomillas ("Val*"), pasa a ser texto literal y no casará.

Un detalle sobre el orden alfabético: depende de LC_COLLATE. En español, [[ "año" < "banco" ]] puede dar un resultado distinto que en la configuración C. Para comparaciones deterministas, la práctica habitual es fijar LC_ALL=C.

  1. Operadores de fichero

Esta es la familia que convierte a informe-diario.sh en un script robusto: comprobar antes de trabajar.

Operador Verdadero si la ruta…
-e ruta Existe (sea lo que sea)
-f ruta Existe y es un fichero regular
-d ruta Existe y es un directorio
-s ruta Existe y no está vacía (tamaño > 0)
-r ruta Es legible por el usuario actual
-w ruta Es escribible
-x ruta Es ejecutable (o directorio atravesable)
-L ruta Es un enlace simbólico (05-01)
f1 -nt f2 / f1 -ot f2 f1 es más reciente / más antiguo que f2
[[ -r "$RUTA_APP_LOG" ]] || echo "sin permiso de lectura sobre el log" >&2
[[ -s "$RUTA_CSV" ]]     || echo "atención: el CSV está vacío" >&2
[[ -d "$DIR_INFORMES" ]] || mkdir -p "$DIR_INFORMES"     # "asegura que existe"

La distinción entre -e, -f y -s importa más de lo que parece. -e solo dice que la ruta existe: un directorio llamado envios.csv también la satisface. -f garantiza que es un fichero de verdad. Y -s añade que tiene contenido, que es lo que quieres saber realmente: un CSV vacío existe, es legible y produce un informe con todo a cero sin dar ningún error. Ese es el fallo silencioso que -s previene.

-nt y -ot tienen un uso muy concreto en operaciones: saber si un informe está al día respecto a sus datos de origen, con [[ "$RUTA_CSV" -nt "$informe" ]].

  1. Operadores lógicos dentro de [[ ]]

Para combinar condiciones, [[ ]] admite && y || dentro de los corchetes:

[[ -f "$RUTA_CSV" && -r "$RUTA_CSV" ]] && echo "existe y puedo leerlo"
[[ -z $ciudad || $ciudad == "todas" ]] && echo "analizar todas las ciudades"

El comando [ ] no los admite; usa -a (and) y -o (or), como en [ -f "$RUTA_CSV" -a -r "$RUTA_CSV" ]. Evita -a y -o. Están marcados como obsoletos en el propio estándar POSIX porque su análisis sintáctico es ambiguo: test recibe una lista plana de argumentos y tiene que adivinar dónde empieza y acaba cada expresión, lo que produce resultados incorrectos con ciertos valores. Además no hacen cortocircuito: [ -f "$f" -a -r "$f" ] evalúa las dos partes siempre.

Hay otra diferencia práctica notable: dentro de [[ ]], && sí cortocircuita, lo que permite escribir comprobaciones dependientes con seguridad. En [[ -f "$f" && $(wc -l < "$f") -gt 0 ]], si el fichero no existe el wc no llega a ejecutarse y no aparece ningún mensaje de error espurio. Con -a no tendrías esa garantía.

  1. Aritmética como operador: (( )) y $(( ))

Bash ofrece un contexto aritmético donde los operadores recuperan su significado matemático habitual. Hay dos variantes:

Sintaxis Devuelve Se usa para
(( expr )) Un código de salida Comparar y evaluar como condición
$(( expr )) Un valor Calcular y sustituir el resultado
total_errores=37; UMBRAL_ERRORES=50

(( total_errores > UMBRAL_ERRORES )) && echo "AVISO: demasiados errores"
porcentaje=$(( total_errores * 100 / UMBRAL_ERRORES ))
echo "Estás al ${porcentaje}% del umbral"     # → Estás al 74% del umbral

Dos ventajas notables. Primera: dentro de (( )) el $ es opcional en los nombres de variable, porque todo lo que no sea un número se interpreta como tal. Segunda: puedes usar <, >, <=, >=, == y != con su significado numérico natural, lo que se lee mucho mejor que -lt y compañía.

Hay un detalle que sorprende y produce fallos silenciosos: (( )) devuelve 1 (fallo) cuando el resultado de la expresión es 0, siguiendo la convención de C, invertida respecto a los códigos de salida del shell.

(( 0 ))  ; echo $?    # 1 → "falso"
(( 5 ))  ; echo $?    # 0 → "verdadero"
contador=0; (( contador++ )) ; echo $?    # 1, aunque la operación funcionó

La última línea es la trampa real: si tienes set -e activo (05-03), un (( contador++ )) con el contador a cero abortará el script. La solución idiomática es (( contador++ )) || true.

El repertorio aritmético completo —%, **, incrementos, bases numéricas, decimales con bc— es la lección 04-06. Aquí nos interesa (( )) solo como operador de comparación.

  1. Precedencia y paréntesis

Cuando mezclas operadores, Bash aplica una precedencia establecida: && se evalúa antes que || dentro de [[ ]], igual que en la mayoría de lenguajes. Pero entre comandos, && y || tienen la misma precedencia y se evalúan de izquierda a derecha, lo cual es una fuente clásica de sorpresas.

[[ -f "$RUTA_CSV" || -f "$RUTA_ALT" ]] && [[ -r "$RUTA_CSV" ]]   # explícito
[[ ( -f "$f" || -d "$f" ) && -r "$f" ]]                          # paréntesis internos

El consejo profesional es sencillo: no confíes en la precedencia, escribe paréntesis. Dentro de [[ ]] se escriben tal cual, con espacios alrededor; dentro de [ ] hay que escaparlos (\\( y \\)), otra razón más para preferir [[ ]].

Con todo esto, las validaciones que informe-diario.sh necesita ya se pueden expresar por completo:

[[ -f "$RUTA_CSV" && -s "$RUTA_CSV" ]] || { echo "CSV ausente o vacío" >&2; exit 3; }
[[ -r "$RUTA_APP_LOG" ]] || { echo "No puedo leer el log" >&2; exit 4; }
(( total_errores > UMBRAL_ERRORES )) && echo "AVISO: $total_errores errores hoy"

Tres líneas que convierten un script confiado en uno defensivo. En 03-04 les daremos la forma de if que corresponde y las integraremos en el fichero.

Errores Comunes y Consejos

  • Olvidar los espacios interiores de los corchetes. [[-f $f]] no es sintaxis: [ y [[ son palabras que necesitan espacios a ambos lados.
  • Comparar números con > o <. [[ "100" > "9" ]] es falso. Usa -gt o, mejor aún, (( )).
  • Usar -eq con texto. [[ "Valencia" -eq "Sevilla" ]] da se esperaba una expresión entera.
  • Usar [ ] con variables sin comillas. Si la variable está vacía, el test recibe menos argumentos de los que espera y falla con un mensaje críptico.
  • Escribir < o > dentro de [ ] sin escapar. Bash lo entiende como redirección y crea un fichero con ese nombre.
  • Encadenar a && b || c creyendo que es if/else. Si b falla, c también se ejecuta.
  • Confiar en (( )) con resultado cero. Devuelve código 1 y puede abortar el script si hay set -e.
  • Usar -a y -o dentro de [ ]. Obsoletos y ambiguos: usa && y || dentro de [[ ]].

Ejercicios

Ejercicio 1 — Traduce a operadores. Escribe, sin usar if, la línea que corresponde a cada frase: (a) si envios.csv no existe, imprime un error en stderr y sale con código 3; (b) crea ~/veloz-ops/logs solo si no existe; (c) si el número de incidencias supera 100, imprime una alerta; (d) si la variable ciudad está vacía, asígnale Valencia (usando operadores, no expansión de parámetros).

Ejercicio 2 — Encuentra los cinco fallos. Este bloque tiene cinco errores de operadores. Identifícalos y reescríbelo con las mejores prácticas de la lección.

errores=`grep -c ERROR /var/log/veloz/app.log`
if [-f /srv/veloz/datos/envios.csv]
[ $errores > 50 ] && echo "muchos errores"
[ -f $csv -a -r $csv ] && echo "legible"
[[ $ciudad -eq "Valencia" ]] && echo "es Valencia"

Ejercicio 3 — Validación completa. Escribe el bloque de validaciones previas de informe-diario.sh usando solo operadores. Debe comprobar, con un código de salida distinto para cada fallo: que el log existe y es legible (código 3), que el CSV existe y no está vacío (código 4), que el directorio de informes existe o puede crearse (código 5), y que hay espacio libre en /srv (código 6). Cada error debe ir a stderr.

Soluciones

Solución al Ejercicio 1

[[ -f "$RUTA_CSV" ]] || { echo "No encuentro $RUTA_CSV" >&2; exit 3; }   # (a)
[[ -d "$DIR_INFORMES" ]] || mkdir -p "$DIR_INFORMES"                     # (b)
(( incidencias > 100 )) && echo "ALERTA: $incidencias incidencias hoy"   # (c)
[[ -z $ciudad ]] && ciudad="Valencia"                                    # (d)

En (a) las llaves son imprescindibles: agrupan el mensaje y la salida en una sola unidad para el ||. En (c) se usa (( )) en lugar de [[ $incidencias -gt 100 ]]; ambas son correctas, pero la aritmética se lee mejor y no admite confusión con la comparación textual. En (d), la forma idiomática real de Bash es ciudad=${ciudad:-Valencia}, que verás en 03-06: hace lo mismo en una expresión.

Solución al Ejercicio 2

Los cinco fallos: (1) comillas invertidas en lugar de $( ); (2) [-f ...] sin espacios interiores y sin cerrar el if —además falta el ; then—; (3) > compara texto y encima crea un fichero llamado 50, hay que usar -gt o (( )); (4) -a obsoleto y variables sin comillas dentro de [ ]; (5) -eq aplicado a texto, que dará se esperaba una expresión entera.

errores=$(grep -c ERROR /var/log/veloz/app.log)
[[ -f /srv/veloz/datos/envios.csv ]] && echo "el CSV existe"
(( errores > 50 )) && echo "muchos errores"
[[ -f $csv && -r $csv ]] && echo "legible"
[[ $ciudad == "Valencia" ]] && echo "es Valencia"

El fallo (3) merece una mirada: con [ $errores > 50 ], Bash interpreta > como redirección, evalúa [ $errores ] (verdadero si la variable no está vacía) y crea un fichero vacío llamado 50 en el directorio actual. El script parece funcionar —siempre dice «muchos errores»— y va dejando basura por el sistema de ficheros. Es el ejemplo perfecto de por qué los operadores importan.

Solución al Ejercicio 3

[[ -f "$RUTA_APP_LOG" && -r "$RUTA_APP_LOG" ]] \
  || { echo "ERROR: no puedo leer $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; }

[[ -d "$DIR_INFORMES" ]] || mkdir -p "$DIR_INFORMES" \
  || { echo "ERROR: no puedo crear $DIR_INFORMES" >&2; exit 5; }

libre=$(df --output=avail -m /srv | tail -1)
(( libre > 100 )) || { echo "ERROR: menos de 100 MB libres en /srv" >&2; exit 6; }

Tres detalles a destacar. El uso de -s además de -f en la segunda comprobación es lo que evita el informe silencioso con todo a cero. La tercera línea encadena || dos veces: si el directorio no existe intenta crearlo, y si eso también falla, aborta; es correcto porque el segundo || recoge el fallo del mkdir. Y los códigos de salida distintos (3, 4, 5, 6) permiten que quien invoque el script —cron, otro script, un panel de monitorización— sepa qué falló sin leer los mensajes, que es justo el contrato del que hablábamos en 03-01.

Conclusión

Ya tienes el vocabulario con el que un script razona. Distingues las cuatro familias de operadores y sabes que > significa cosas distintas según el contexto; encadenas comandos con ;, && y || aprovechando el cortocircuito, y dominas el idioma comando || exit 1; agrupas con { ...; } y con ( ... ) cuando quieres aislamiento; conoces las tres formas de test y por qué [[ ]] es la elección correcta en Bash; separas la comparación numérica de la textual, que es el error más caro de los principiantes; verificas ficheros con -f, -r, -s, -d y -nt, entendiendo por qué -s previene fallos silenciosos; combinas condiciones dentro de [[ ]] en lugar de los obsoletos -a y -o; y usas (( )) con la precaución de su resultado cero.

Y, sobre todo, las tres validaciones que informe-diario.sh necesitaba ya están escritas. Pero escritas como líneas sueltas encadenadas con ||, un estilo que resiste bien dos condiciones y se vuelve ilegible con cinco.

En la lección 03-04 les damos su forma definitiva. Descubrirás que if en Bash no evalúa un booleano sino un código de salida —lo que explica por qué if grep -q ERROR ... funciona sin corchetes—, aprenderás a estructurar validaciones con cláusulas de guarda en lugar de anidamientos, y informe-diario.sh pasará a comprobar su entorno antes de trabajar y a avisar cuando los errores superen el umbral.

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