1. Y, o, no
Tres operadores lógicos permiten construir condiciones a partir de otras:
| Operador | Nombre | Es cierto si… |
|---|---|---|
A && B | Y | A y B son ciertas. |
A || B | O | Al menos una de las dos es cierta. |
! A | No | A es falsa. |
En Bash, los mismos símbolos aparecen en dos lugares distintos, con reglas algo diferentes: dentro de [[ ]], donde combinan pruebas, y entre comandos, donde combinan códigos de salida. Las dos secciones siguientes explican cada caso.
2. Dentro de [[ ]]
Dentro de [[ ]], && y || combinan las pruebas del tema 11 en una sola expresión:
if [[ -f "$archivo" && -r "$archivo" ]]; then
echo "Existe y se puede leer"
fi
if [[ "$respuesta" == s* || "$respuesta" == y* ]]; then
echo "Respuesta afirmativa, en español o en inglés"
fi
Para números se usa (( )), que tiene sus propios && y ||, como vimos en el tema 7. Es la forma más legible de expresar un rango:
if (( dias >= 1 && dias <= 365 )); then
echo "Plazo válido"
fi
-a y -o: no los uses
Con [ ] existían -a (y) y -o (o) para combinar dentro de los corchetes: [ -f "$a" -a -r "$a" ]. El estándar POSIX los declara obsoletos, porque con ciertos valores la expresión se vuelve ambigua: si una variable contiene ! o -a, test no puede saber si es un operador o un dato. Si necesitas [ ] por portabilidad, combina varios corchetes con los operadores de la shell: [ -f "$a" ] && [ -r "$a" ].
3. Entre comandos
Fuera de los corchetes, && y || unen comandos, y deciden según sus códigos de salida, como vimos en el tema 25 del curso de Terminal. Una condición de if puede ser una combinación de comandos y de pruebas:
if [[ -f "$registro" ]] && grep -q "ERROR" "$registro"; then
echo "El registro tiene errores"
fi
if ping -c1 -W2 servidor > /dev/null || ping -c1 -W2 servidor-respaldo > /dev/null; then
echo "Al menos uno de los dos servidores responde"
fi
Mezclar comandos con pruebas no es posible dentro de [[ ]], que solo admite las pruebas de test y comparaciones. Por eso, cuando una de las condiciones es un comando, los operadores van afuera, uniendo un [[ ]] con el comando.
4. Precedencia y agrupación
Cuando se mezclan && y ||, importa cuál se evalúa primero, y aquí los dos contextos difieren:
| Contexto | Regla | A || B && C significa |
|---|---|---|
Dentro de [[ ]] y (( )) | && tiene más precedencia, como en matemática y en otros lenguajes. | A || (B && C) |
| Entre comandos | Los dos tienen la misma precedencia y se evalúan de izquierda a derecha. | (A || B) && C |
$ true || false && false; echo $? # (true || false) && false
1
$ [[ -n x || -z x && -z x ]]; echo $? # -n x || (-z x && -z x)
0
La forma de no depender de estas reglas es agrupar explícitamente. Dentro de [[ ]] se usan paréntesis; entre comandos, llaves, que agrupan sin crear una subshell:
if [[ ( "$dia" == sáb || "$dia" == dom ) && "$FORZAR" != 1 ]]; then
echo "Hoy no se respalda"
fi
if { [[ -f a.log ]] || [[ -f b.log ]]; } && [[ -d /respaldos ]]; then
echo "Hay registros y hay dónde guardarlos"
fi
Aunque la precedencia diera el resultado correcto sin paréntesis, agruparlos ahorra a quien lee el tener que recordarla. Y si una condición necesita más de dos niveles de agrupación, es señal de que conviene partirla en pasos con nombre, como veremos en la sección 8.
5. La evaluación en cortocircuito
Bash evalúa las condiciones compuestas de izquierda a derecha, y se detiene en cuanto conoce el resultado. En A && B, si A es falsa, el conjunto ya es falso y B no se evalúa. En A || B, si A es cierta, B no se evalúa. Esto se llama evaluación en cortocircuito, y tiene dos consecuencias prácticas.
La primera es que el orden sirve de protección: la condición de la izquierda puede garantizar que la de la derecha sea segura.
if [[ -f "$config" ]] && grep -q "^modo=produccion" "$config"; then
… # grep solo se ejecuta si el archivo existe
fi
if [[ -v LIMITE ]] && (( LIMITE > 0 )); then
… # con set -u, LIMITE solo se lee si existe
fi
if (( total > 0 )) && (( errores * 100 / total > 5 )); then
… # no se divide por cero
fi
La segunda es que el orden afecta al rendimiento: si una condición es barata, como una prueba de [[ ]], y otra es cara, como un grep sobre un archivo grande o una consulta por red, conviene poner primero la barata, para que la cara se evalúe solo cuando haga falta.
6. Negar condiciones
El operador ! invierte el resultado, y puede ir en tres lugares, con el mismo efecto:
if [[ ! -d "$dir" ]]; then … # dentro de [[ ]], sobre una prueba
if ! [[ -d "$dir" ]]; then … # fuera, sobre todo el comando [[ ]]
if ! grep -q "$usuario" lista.txt; then … # sobre cualquier comando
Con condiciones compuestas, negar requiere cuidado. Las leyes de De Morgan dicen cómo se transforma una negación: «no (A y B)» equivale a «no A o no B», y «no (A o B)» equivale a «no A y no B». En la práctica, conviene escribir la forma que se lea más natural:
if ! [[ -f "$a" && -r "$a" ]]; then # no (existe y se lee)
if [[ ! -f "$a" || ! -r "$a" ]]; then # no existe, o no se lee
Las dobles negaciones, como if ! [[ ! -e … ]], siempre tienen una forma más directa. Y cuando una condición negada lleva un else, invertirla suele dejar el código más claro: es más fácil leer «si existe… si no…» que «si no existe… si no…».
7. Varias alternativas: elif
Cuando hay más de dos caminos posibles, elif (contracción de else if) encadena condiciones. Se evalúan en orden, y se ejecuta el bloque de la primera que sea cierta; si ninguna lo es, el else final, si existe.
uso=$(df --output=pcent / | tail -1 | tr -dc '0-9')
if (( uso >= 95 )); then
nivel="crítico"
elif (( uso >= 85 )); then
nivel="alto"
elif (( uso >= 70 )); then
nivel="moderado"
else
nivel="normal"
fi
echo "Uso del disco: $uso % ($nivel)"
El orden de las condiciones es parte de la lógica. Como solo se ejecuta la primera que coincide, las más restrictivas van antes: si uso >= 70 estuviera primero, un disco al 98 % se clasificaría como «moderado». Cada elif puede asumir que todas las condiciones anteriores fueron falsas, y por eso la segunda no necesita decir uso >= 85 && uso < 95.
Cuando todas las condiciones comparan una misma variable con distintos valores o patrones, case lo expresa mejor que una cadena de elif. Es el tema siguiente.
8. Salir pronto en lugar de anidar
Un script que debe comprobar varias cosas antes de hacer su trabajo tiende a crecer hacia la derecha, con cada condición dentro de la anterior:
if [[ -n "$origen" ]]; then
if [[ -d "$origen" ]]; then
if [[ -w "$destino" ]]; then
tar -czf "$destino/respaldo.tgz" "$origen"
else
echo "No se puede escribir en $destino" >&2
exit 1
fi
else
echo "No existe $origen" >&2
exit 1
fi
else
echo "Falta el origen" >&2
exit 2
fi
Cada mensaje de error queda lejos de la condición que lo provoca, y el trabajo real, la línea de tar, queda enterrado en el centro. La alternativa es invertir cada condición y salir en cuanto algo no se cumple. A estas comprobaciones iniciales se las llama cláusulas de guarda:
[[ -n "$origen" ]] || { echo "Falta el origen" >&2; exit 2; }
[[ -d "$origen" ]] || { echo "No existe $origen" >&2; exit 1; }
[[ -w "$destino" ]] || { echo "No se puede escribir en $destino" >&2; exit 1; }
tar -czf "$destino/respaldo.tgz" "$origen"
Cada línea es una precondición con su mensaje al lado, y después de ellas el script puede asumir que todo está en orden. Es el patrón que usamos en el tema 11, con la función falla, y el que seguirán los scripts del curso: primero se descarta lo que impide trabajar, después se trabaja.
9. Condiciones de una línea
Para decisiones simples, un if de tres líneas puede ser excesivo. Los operadores && y || permiten escribir una condición y su acción en una sola línea:
| Forma corta | Equivale a | Se lee |
|---|---|---|
condición && acción | if condición; then acción; fi | «si se cumple, haz esto» |
condición || acción | if ! condición; then acción; fi | «si no se cumple, haz esto» |
[[ -d "$destino" ]] || mkdir -p "$destino"
[[ "${DEPURAR:-}" == 1 ]] && set -x
(( errores > 0 )) && echo "Hubo $errores errores" >&2
command -v rsync > /dev/null || { echo "Falta rsync" >&2; exit 1; }
Funcionan bien cuando la acción es breve y no hay alternativa. Si la acción tiene varios pasos, o hace falta un «si no», el if completo es más claro. La última línea muestra un modismo útil: command -v comprueba si un programa está instalado, y es la forma de verificar los requisitos de un script al comenzar.
10. La trampa de a && b || c
Es tentador combinar las dos formas cortas para escribir un if/else en una línea: condición && acción_si || acción_no. Parece equivalente, y casi siempre se comporta igual. Pero no lo es:
$ copiar() { echo "copiando..."; return 1; } # una acción que falla
$ true && copiar || echo "no se cumplió la condición"
copiando...
no se cumplió la condición
La condición se cumplió, y aun así se ejecutó la rama del «no». Por la regla de la sección 4, la línea se agrupa como (true && copiar) || echo: el || no mira solo la condición, sino el resultado de true && copiar. Si la acción del «sí» falla, se ejecuta también la del «no». En el mejor caso aparece un mensaje engañoso; en el peor, se ejecutan las dos ramas de una decisión que debía elegir una.
a && b || c no es un if/else
Úsalo solo si b no puede fallar, como un echo o una asignación, y aun así con cuidado. Si b es un comando que hace algo real, escribe el if completo. ShellCheck señala este patrón con el aviso SC2015.
11. Las condiciones y set -e
El modo set -e, que estudiaremos en el tema 23, detiene el script cuando un comando falla. Pero no todo código distinto de cero es un fallo: un comando usado como condición puede dar falso, y eso es una respuesta, no un error. Por eso set -e no se activa con:
- El comando de la condición de un
if,elif,whileountil. - Cualquier comando de una lista con
&&o||, salvo el último. - Un comando precedido de
!.
Esto tiene una consecuencia que conviene recordar: en [[ -f "$a" ]] && procesar "$a", si procesar falla, el script se detiene, porque es el último de la lista. Pero si el archivo no existe, la prueba da falso, el script sigue, y esa línea deja un código 1. Si fuera la última de una función o del script, el código de salida sería 1 sin que nada hubiera fallado, el caso que vimos en el tema 10. Las condiciones cortas son cómodas en medio de un script; al final, mejor un if.
12. Un script completo
Respaldar un directorio que no cambió desde ayer gasta espacio y tiempo. Este script decide si corresponde hacer un respaldo, y lo explica. Responde con su código de salida, de modo que otro script, o cron, puede usarlo como condición: debe-respaldar ~/proyecto && respaldo ~/proyecto.
#!/usr/bin/env bash
#
# debe-respaldar — decide si corresponde respaldar un directorio.
#
# Uso: debe-respaldar DIRECTORIO
# Entorno: FORZAR=1 respaldar siempre
# FINES_DE_SEMANA=1 respaldar también sábados y domingos
# MARCA=ARCHIVO archivo cuya fecha indica el último respaldo
# (por defecto, DIRECTORIO/.ultimo-respaldo)
# Salida: 0 si corresponde respaldar; 1 si no; 2 si el uso es incorrecto.
#
set -euo pipefail
dir="${1:-}"
if [[ -z "$dir" || ! -d "$dir" ]]; then
echo "Uso: ${0##*/} DIRECTORIO (debe existir)" >&2
exit 2
fi
marca="${MARCA:-$dir/.ultimo-respaldo}"
dia=$(date +%u) # 1 = lunes … 7 = domingo
respaldar() { echo "Respaldar: $*"; exit 0; }
omitir() { echo "No respaldar: $*"; exit 1; }
if [[ "${FORZAR:-0}" == 1 ]]; then
respaldar "se pidió forzarlo"
elif [[ ! -e "$marca" ]]; then
respaldar "nunca se respaldó"
elif (( dia >= 6 )) && [[ "${FINES_DE_SEMANA:-0}" != 1 ]]; then
omitir "es fin de semana"
elif find "$dir" -type f -newer "$marca" -print -quit | grep -q .; then
respaldar "hay archivos modificados desde el último respaldo"
else
omitir "nada cambió desde el último respaldo"
fi
$ debe-respaldar ~/proyecto
Respaldar: nunca se respaldó
$ touch ~/proyecto/.ultimo-respaldo # lo que haría el script de respaldo al terminar
$ debe-respaldar ~/proyecto; echo "código $?"
No respaldar: nada cambió desde el último respaldo
código 1
$ echo "nueva línea" >> ~/proyecto/notas.txt
$ debe-respaldar ~/proyecto && echo "→ ejecutar el respaldo"
Respaldar: hay archivos modificados desde el último respaldo
→ ejecutar el respaldo
- La guarda inicial combina dos pruebas con
||: falta el argumento o no es un directorio. El cortocircuito garantiza que-dnunca se evalúe sobre una cadena vacía. - El orden de la cadena de
elifes la política de respaldo. Forzar gana sobre todo; la falta de un respaldo previo gana sobre el fin de semana; y solo al final se pregunta si algo cambió, que es lo más costoso de averiguar. - La tercera condición mezcla aritmética y una prueba, unidas fuera de los corchetes: «es fin de semana y no se pidió incluirlos».
- La cuarta es una tubería usada como condición.
find -newerlista los archivos más nuevos que la marca, y-print -quitse detiene en el primero, porque basta uno.grep -q .responde si hubo alguna línea. Gracias apipefail, un error defindno se confunde con «nada cambió», y lleva alelse. - Las dos funciones de una línea dan un mensaje y terminan con el código adecuado, y hacen que cada rama se lea como una frase.
13. Errores frecuentes
| Escribiste | Qué pasa | Corrección |
|---|---|---|
[[ -f a ]] && [[ -f b ]] || [[ -f c ]] esperando «a y (b o c)» | Entre comandos se agrupa como «(a y b) o c». | [[ -f a && ( -f b || -f c ) ]] |
[ -f "$a" -a -r "$a" ] | Obsoleto; ambiguo con ciertos valores. | [[ -f $a && -r $a ]] |
[[ -f $a ]] && grep x $a || echo no | Si grep no encuentra nada, también se ejecuta el echo. | Un if/else completo. |
[[ -f $a && grep -q x $a ]] | Error de sintaxis: grep no es una prueba. | [[ -f $a ]] && grep -q x "$a" |
elif en el orden equivocado | Una condición más general atrapa los casos de una más específica. | De la más restrictiva a la más general. |
if [[ … ]]; then if …; then if … | Anidamiento profundo, errores lejos de su causa. | Cláusulas de guarda. |
(( a / b > 2 )) && (( b > 0 )) | Divide antes de comprobar el divisor. | Primero (( b > 0 )). |
Última línea [[ … ]] && accion | El script o la función terminan con 1 si la prueba es falsa. | Un if, o exit 0 después. |
14. Resumen
&&,||y!combinan condiciones dentro de[[ ]]y(( )), y también entre comandos.- Para mezclar un comando con una prueba, los operadores van fuera de los corchetes.
-ay-ode[ ]son obsoletos: se combinan varios[ ]con&&y||.- Dentro de
[[ ]],&&tiene más precedencia que||; entre comandos, tienen la misma y se evalúan de izquierda a derecha. Agrupar con paréntesis o llaves evita depender de eso. - La evaluación se detiene en cuanto se conoce el resultado: la condición de la izquierda puede proteger a la de la derecha, y conviene poner primero las baratas.
!puede negar una prueba, un[[ ]]entero o cualquier comando; las negaciones compuestas siguen las leyes de De Morgan.elifencadena alternativas; se ejecuta la primera que sea cierta, así que las más restrictivas van primero.- Las cláusulas de guarda, que salen en cuanto algo no se cumple, evitan los
ifanidados. condición && acciónycondición || acciónsirven para decisiones simples de una línea.a && b || cno es un if/else: sibfalla, también se ejecutac.set -eno se activa con los comandos usados como condición, pero una condición corta al final puede dejar un código 1 engañoso.
15. Preguntas de repaso
Intenta responder cada una antes de abrir la respuesta.
1. ¿Cómo se pregunta si un archivo existe y contiene la palabra «ERROR»?
Combinando una prueba y un comando fuera de los corchetes: [[ -f "$a" ]] && grep -q ERROR "$a". grep no puede ir dentro de [[ ]].
2. ¿Por qué se desaconsejan -a y -o dentro de [ ]?
Porque son obsoletos en POSIX y producen expresiones ambiguas cuando los datos contienen caracteres como ! o textos que parecen operadores. Se reemplazan por varios [ ] unidos con && y ||, o por [[ ]].
3. ¿Cómo se evalúa A || B && C entre comandos, y cómo dentro de [[ ]]?
Entre comandos, de izquierda a derecha: (A || B) && C. Dentro de [[ ]], && tiene precedencia: A || (B && C).
4. ¿Qué es la evaluación en cortocircuito?
Que Bash deja de evaluar en cuanto conoce el resultado: en A && B, si A es falsa, B no se evalúa; en A || B, si A es cierta, tampoco.
5. ¿Por qué (( total > 0 )) && (( errores * 100 / total > 5 )) no puede dividir por cero?
Porque si total es 0, la primera condición es falsa y, por el cortocircuito, la segunda no llega a evaluarse.
6. ¿Qué condición equivale a ! [[ -f $a || -d $a ]]?
[[ ! -f $a && ! -d $a ]]: «no es archivo ni directorio». Es una de las leyes de De Morgan.
7. En una cadena de elif, ¿qué bloque se ejecuta si varias condiciones son ciertas?
Solo el de la primera que sea cierta, en el orden en que están escritas.
8. ¿Por qué clasificar un número de mayor a menor (≥ 95, ≥ 85, ≥ 70) funciona con elif, y al revés no?
Porque se ejecuta la primera condición cierta. Si la primera fuera ≥ 70, un 98 la cumpliría y nunca llegaría a la de ≥ 95.
9. ¿Qué es una cláusula de guarda, y qué ventaja tiene sobre los if anidados?
Una comprobación al comienzo que termina el script, o la función, si algo no se cumple. Deja cada condición junto a su mensaje de error, evita la sangría creciente y permite que el resto del código asuma que todo está en orden.
10. ¿Qué hace command -v rsync > /dev/null || exit 1?
Comprueba si rsync está disponible; si no lo está, termina el script con código 1.
11. ¿Por qué prueba && accion || alternativa no equivale a un if/else?
Porque || mira el resultado de prueba && accion. Si la prueba es cierta pero la acción falla, también se ejecuta la alternativa.
12. Con set -e, ¿se detiene el script si en [[ -f x ]] && procesar x el archivo no existe? ¿Y si procesar falla?
Si el archivo no existe, no: la prueba no es el último comando de la lista. Si procesar falla, sí: es el último comando.
13. ¿Por qué en debe-respaldar la comprobación con find va en último lugar?
Porque es la más costosa: recorre el directorio. Las condiciones anteriores son pruebas baratas, y si alguna decide el resultado, find no llega a ejecutarse.
Para practicar
- Escribe una condición que sea cierta si una variable
nestá entre 1 y 10, o es exactamente 100. Pruébala con 0, 5, 50 y 100. - Comprueba con
echo $?cómo se evalúatrue || false && falseentre comandos, y lo mismo dentro de[[ ]]con pruebas equivalentes. - Reproduce la trampa de
a && b || ccon una función que falla, y reescríbela con unif. - Escribe un script que clasifique la carga del sistema (el primer número de
/proc/loadavg, multiplicado por 100 para usar enteros) en «baja», «media» y «alta» conelif. - Reescribe con cláusulas de guarda el ejemplo anidado de la sección 8 y comprueba que da los mismos mensajes.
- Agrega al comienzo de un script tuyo una comprobación de requisitos con
command -vpara cada programa externo que use. - Prueba
debe-respaldaren un directorio propio: sin marca, con la marca recién creada y después de modificar un archivo. Combínalo concomprobar-respaldodel tema 11.