← Automatización con Scripts en Bash Bloque 3 · Códigos de salida y condicionales · Tema 11 de 42

Tema 11

if, test y [[ ]]

Un script que no decide hace siempre lo mismo, aunque el archivo no exista, la variable esté vacía o el disco esté lleno. Con if elige qué hacer según la situación, y con test y [[ ]] formula las preguntas: ¿existe este archivo?, ¿son iguales estos textos?, ¿es este número mayor que aquel? Este tema explica cómo se escriben esas preguntas, por qué los corchetes de Bash tienen reglas tan estrictas y cuál de las formas conviene usar.

1. La estructura de if

Como vimos en el tema 10, if no evalúa expresiones: ejecuta un comando y mira su código de salida. Si es 0, ejecuta el bloque de then; si no, el de else, cuando lo hay. La palabra fi, que es if al revés, cierra la estructura.

la forma de if
if comando; then
  # se ejecuta si comando terminó con código 0
else
  # se ejecuta en cualquier otro caso (opcional)
fi

El ; antes de then separa el comando de la palabra siguiente; también podría ir un salto de línea. Como establecimos en el tema 4, en este curso then va en la misma línea que if, y el contenido de cada bloque con dos espacios de sangría. Los bloques no pueden estar vacíos: si hace falta uno sin contenido, se usa el comando :, que no hace nada.

Cualquier comando sirve de condición, y muchos scripts no necesitan otra cosa:

comandos como condición
if grep -q "^$usuario:" /etc/passwd; then
  echo "El usuario $usuario ya existe"
fi

if ! mkdir -p "$destino"; then
  echo "No se pudo crear $destino" >&2
  exit 1
fi

Pero muchas preguntas no las responde ningún programa en particular: si dos textos son iguales, si un número es mayor que otro, si un archivo existe. Para esas existe un comando especial.

2. test y [: un comando que responde

El comando test evalúa una expresión que recibe como argumentos, y termina con código 0 si es cierta y 1 si es falsa. No muestra nada: su única respuesta es el código.

terminal
$ test -f /etc/hostname; echo $?          # ¿es un archivo común?
0
$ test 5 -gt 8; echo $?                   # ¿5 es mayor que 8?
1

El mismo comando tiene un segundo nombre: [. Con ese nombre exige que su último argumento sea ], de modo que la expresión queda entre corchetes y se lee como una condición:

terminal
$ [ -f /etc/hostname ]; echo $?
0
$ type -a [
[ is a shell builtin
[ is /usr/bin/[

Esta es la clave para entender sus reglas, que de otro modo parecen caprichosas: [ no es un signo de puntuación, es un comando, y lo que hay dentro son sus argumentos. Por eso:

La última consecuencia es la que más errores produce:

terminal
$ respuesta=""
$ [ $respuesta = si ]                     # [ recibe: = si ]
bash: [: =: unary operator expected
$ archivo="mi informe.txt"
$ [ -f $archivo ]                         # [ recibe: -f mi informe.txt ]
bash: [: mi: binary operator expected
$ [ -f "$archivo" ]                       # correcto

Con [, todas las variables van entre comillas, sin excepción. Peor que los errores es cuando no hay error: [ -n $vacia ] con la variable vacía se convierte en [ -n ], que es una prueba con una sola palabra, -n, y una sola palabra no vacía es siempre cierta. La condición dice «tiene contenido» precisamente cuando no lo tiene.

3. [[ ]]: la versión de Bash

Para evitar esas trampas, Bash incorporó [[ ]]. Se parece a [ ], pero no es un comando sino una palabra reservada, parte de la sintaxis del lenguaje, como if o for. Bash la analiza antes de hacer las expansiones, y eso le permite comportarse de forma más sensata:

[ ] y test[[ ]]
Qué esUn comando, interno y también externo.Una palabra reservada de Bash.
Variables sin comillasSe dividen en palabras y se expanden como comodines.No se dividen ni se expanden: una variable es siempre un operando.
Variable vacíaDesaparece, y la expresión queda mal formada.Es una cadena vacía, como debe ser.
PatronesNo.== y != comparan con patrones de comodines.
Expresiones regularesNo.=~ (tema 28).
Combinar condiciones-a y -o, obsoletos y ambiguos.&&, || y paréntesis (tema 12).
< y >Son redirecciones: hay que escaparlos.Comparan cadenas alfabéticamente.
PortabilidadCualquier shell POSIX.Bash, zsh y ksh; no sh ni dash.
terminal
$ respuesta=""
$ [[ $respuesta = si ]]; echo $?          # sin error: simplemente es falso
1
$ archivo="mi informe.txt"
$ [[ -f $archivo ]]; echo $?             # la variable no se divide
0

Los espacios siguen siendo obligatorios, porque [[ y ]] deben ser palabras separadas. Y aunque dentro de [[ ]] las comillas no son necesarias para las variables, ponerlas no hace daño, y mantiene la costumbre que exigen [ ] y el resto del lenguaje. En este curso las seguiremos poniendo, con una excepción que veremos en la sección 5.

4. Comparar cadenas

ExpresiónEs cierta si…
"$a" == "$b"Las cadenas son iguales. = es equivalente; [ ] solo garantiza =.
"$a" != "$b"Son distintas.
-z "$a"La cadena está vacía (zero length).
-n "$a"La cadena no está vacía.
"$a" < "$b"a va antes que b en orden alfabético.
"$a" > "$b"a va después que b.
comparaciones de cadenas
if [[ "$USER" == "root" ]]; then
  echo "No ejecutes este script como administrador." >&2
  exit 1
fi

if [[ -z "$destino" ]]; then
  echo "Falta el destino." >&2
  exit 2
fi

Las comparaciones distinguen mayúsculas de minúsculas: Si y si son distintas. Si no importa, se normaliza antes con ${var,,}, como en el tema 6. Y < y > comparan texto, según el orden alfabético de la configuración regional; con números dan resultados sorprendentes, como veremos en la sección 6.

5. Patrones con ==

Dentro de [[ ]], el lado derecho de == y != es un patrón, con los comodines de los nombres de archivo: *, ? y […]. Es una de las características más útiles de [[ ]]:

terminal
$ f="informe.pdf"
$ [[ $f == *.pdf ]] && echo "es un PDF"
es un PDF
$ [[ $f == "*.pdf" ]] || echo "no se llama literalmente *.pdf"
no se llama literalmente *.pdf

Aquí está la excepción a la costumbre de entrecomillar: las comillas en el lado derecho desactivan el patrón, y la comparación pasa a ser literal. Así se elige entre los dos comportamientos: patrón sin comillas, texto exacto con comillas. Cuando el valor de la derecha viene de una variable y debe compararse literalmente, va entre comillas: [[ $a == "$b" ]]. Si no, un * en $b funcionaría como comodín.

CondiciónPregunta
[[ $f == *.@(jpg|png) ]]¿Termina en .jpg o .png? (patrón extendido, tema 13)
[[ $linea == \#* ]]¿Empieza con #? Es un comentario.
[[ $respuesta == [sS]* ]]¿Empieza con s o S?
[[ $ruta == /* ]]¿Es una ruta absoluta?
[[ $version != *-beta* ]]¿No es una versión beta?

Los patrones de [[ ]] coinciden con la cadena completa: *.pdf exige que termine en .pdf, y pdf solo coincide con la palabra exacta. Para buscar un texto en cualquier posición se rodea de asteriscos: *error*. Y, a diferencia de los nombres de archivo, en [[ ]] el * sí coincide con las barras.

6. Comparar números

Para comparar números, [ ] y [[ ]] usan operadores con letras, heredados de la shell original. Ya conoces la alternativa más legible, (( )), del tema 7:

SignificadoEn [ ] y [[ ]]En (( ))
Igual-eq (equal)==
Distinto-ne (not equal)!=
Menor-lt (less than)<
Menor o igual-le (less or equal)<=
Mayor-gt (greater than)>
Mayor o igual-ge (greater or equal)>=
tres formas de la misma pregunta
if [ "$uso" -ge 90 ]; then      # portable a sh
if [[ $uso -ge 90 ]]; then      # Bash
if (( uso >= 90 )); then        # Bash, la más legible: la que usa este curso

Lo que no hay que hacer es comparar números con los operadores de cadenas, porque comparan carácter por carácter, como en un diccionario:

terminal
$ [[ 10 < 9 ]] && echo "10 < 9, como texto"    # «1» va antes que «9»
10 < 9, como texto
$ (( 10 < 9 )) || echo "falso, como números"
falso, como números
$ [ 5 > 3 ]                                    # en [ ], > es una redirección
$ ls
3

El último caso merece atención: dentro de [ ], > 3 es una redirección, que crea un archivo llamado 3, y la condición [ 5 ] es siempre cierta. No hay ningún mensaje de error.

Si un operando no es un número, [ ] da un error, y [[ ]] y (( )) lo evalúan como una expresión aritmética, con las sorpresas que vimos en el tema 7: una palabra cualquiera vale 0. Por eso, como allí, los números que vienen de afuera se validan antes de comparar.

terminal
$ [ abc -eq 1 ]
bash: [: abc: integer expression expected
$ [[ abc -eq 0 ]] && echo "cierto, en silencio"
cierto, en silencio

7. Comprobar archivos y directorios

La familia más numerosa de pruebas pregunta por archivos. Son las que más usa un script de automatización, que antes de actuar debe saber si lo que va a tocar existe y es lo que espera:

PruebaEs cierta si la ruta…
-e rutaExiste, sea lo que sea.
-f rutaEs un archivo común.
-d rutaEs un directorio.
-L rutaEs un enlace simbólico (aunque apunte a algo que no existe).
-s rutaExiste y no está vacía (size mayor que cero).
-r rutaExiste y se puede leer.
-w rutaExiste y se puede escribir.
-x rutaExiste y se puede ejecutar; en un directorio, se puede entrar.
-p ruta, -S rutaEs una tubería con nombre, o un socket.
comprobar antes de actuar
if [[ ! -d "$origen" ]]; then
  echo "No existe el directorio $origen" >&2
  exit 1
fi

if [[ -e "$destino" ]]; then
  echo "$destino ya existe; no lo sobrescribo" >&2
  exit 1
fi

if [[ ! -w "$(dirname "$destino")" ]]; then
  echo "No tengo permiso para escribir en $(dirname "$destino")" >&2
  exit 1
fi

El signo ! delante de una prueba la invierte: ! -d pregunta si no es un directorio. Tres detalles evitan confusiones:

8. Comparar archivos entre sí

Tres pruebas comparan dos archivos. Las dos primeras, por fecha de modificación, son la base de cualquier tarea que deba rehacer algo solo si su origen cambió, como veremos al automatizar compilaciones en el tema 36:

PruebaEs cierta si…
a -nt ba es más nuevo que b (newer than), o a existe y b no.
a -ot ba es más viejo que b (older than), o b existe y a no.
a -ef ba y b son el mismo archivo (por ejemplo, un enlace duro).
rehacer solo si cambió el origen
if [[ informe.md -nt informe.pdf ]]; then
  pandoc informe.md -o informe.pdf
  echo "PDF actualizado"
else
  echo "El PDF ya está al día"
fi

La regla para archivos inexistentes es justo la que hace falta aquí: si informe.pdf todavía no existe, informe.md -nt informe.pdf es cierto, y el PDF se genera por primera vez.

9. Comprobar variables

Para saber si una variable tiene valor se usan -z y -n. Para saber si existe, aunque esté vacía, -v, que recibe el nombre sin $, como vimos en el tema 5. Con set -u activo, leer una variable que puede no existir requiere el valor por defecto vacío del tema 6:

variables opcionales con set -u
if [[ -n "${DEPURAR:-}" ]]; then        # ¿tiene valor? sin error si no existe
  set -x
fi

if [[ -v CONFIG ]]; then                # ¿existe, aunque esté vacía?
  source "$CONFIG"
fi

10. Cuál usar

Bash ofrece tres formas de condición, y cada una tiene su terreno:

ParaUsaPor qué
Cadenas, patrones, archivos y variables, en un script de Bash[[ ]]No tiene las trampas de la división en palabras, y admite patrones.
Números(( ))Operadores habituales, sin $ ni letras.
Un script que debe funcionar en sh[ ], con todas las variables entre comillasEs la única forma POSIX.
Saber si un comando funcionóEl comando mismoif grep -q …, sin corchetes.

La última fila corrige una confusión frecuente: los corchetes no son parte de la sintaxis de if. Escribir if [ grep -q x f ] no ejecuta grep: le pasa a [ la palabra grep y otras, y da un error. Los corchetes solo se usan cuando la pregunta es de las que responde test.

11. Un script completo

Un respaldo que falla en silencio es peor que no tener ninguno, porque da una falsa seguridad. Este script comprueba que un archivo de respaldo sea razonable: que exista, sea un archivo común, se pueda leer, no esté vacío, tenga un tamaño mínimo y sea reciente. Los límites se pueden cambiar con variables de entorno.

comprobar-respaldo
#!/usr/bin/env bash
#
# comprobar-respaldo — verifica que un archivo de respaldo exista, no esté
# vacío, tenga un tamaño mínimo y sea reciente.
#
# Uso:     comprobar-respaldo ARCHIVO
# Entorno: HORAS_MAX  antigüedad máxima en horas (26)
#          KIB_MIN    tamaño mínimo en KiB (1)
# Salida:  0 si el respaldo es válido; 1 si no.
#
set -euo pipefail

readonly HORAS_MAX="${HORAS_MAX:-26}"
readonly KIB_MIN="${KIB_MIN:-1}"

archivo="${1:?Uso: comprobar-respaldo ARCHIVO}"

falla() {
  printf 'FALLA  %s: %s\n' "$archivo" "$*" >&2
  exit 1
}

if [[ ! -e "$archivo" ]]; then
  falla "no existe"
fi
if [[ ! -f "$archivo" ]]; then
  falla "no es un archivo común"
fi
if [[ ! -r "$archivo" ]]; then
  falla "no se puede leer"
fi
if [[ ! -s "$archivo" ]]; then
  falla "está vacío"
fi

kib=$(( $(stat -c %s "$archivo") / 1024 ))
if (( kib < KIB_MIN )); then
  falla "ocupa $kib KiB; se esperaban al menos $KIB_MIN"
fi

horas=$(( ( $(date +%s) - $(stat -c %Y "$archivo") ) / 3600 ))
if (( horas > HORAS_MAX )); then
  falla "tiene $horas horas; el máximo es $HORAS_MAX"
fi

printf 'OK     %s (%d KiB, %d horas)\n' "$archivo" "$kib" "$horas"
terminal
$ comprobar-respaldo /respaldos/web-2026-10-14.tgz
OK     /respaldos/web-2026-10-14.tgz (48213 KiB, 7 horas)
$ comprobar-respaldo /respaldos/web-2026-10-11.tgz
FALLA  /respaldos/web-2026-10-11.tgz: tiene 79 horas; el máximo es 26
$ comprobar-respaldo /respaldos/db.sql.gz
FALLA  /respaldos/db.sql.gz: está vacío
$ KIB_MIN=100000 comprobar-respaldo /respaldos/web-2026-10-14.tgz
FALLA  /respaldos/web-2026-10-14.tgz: ocupa 48213 KiB; se esperaban al menos 100000

12. Errores frecuentes

EscribisteQué pasaCorrección
if [-f "$a"][-f: command not foundif [ -f "$a" ], con espacios.
[ $a = si ] con a vacíaunary operator expected[ "$a" = si ] o [[ $a = si ]]
[ -n $a ] con a vacíaCierto, sin error: lo contrario de lo esperado.[ -n "$a" ]
[ $n > 5 ]Crea un archivo llamado 5; la condición siempre es cierta.(( n > 5 ))
[[ $n < 10 ]] con númerosCompara texto: 9 < 10 es falso.(( n < 10 ))
[[ $a -eq $b ]] con textoEvalúa aritméticamente: dos palabras distintas son «iguales».[[ $a == "$b" ]]
[[ $f == "*.txt" ]]Compara con el texto literal *.txt.[[ $f == *.txt ]]
[[ $a == $b ]] con * en bb funciona como patrón.[[ $a == "$b" ]]
if [ grep -q x f ][ recibe grep como texto.if grep -q x f
#!/bin/sh con [[ ]][[: not found en dash.#!/usr/bin/env bash

13. Resumen

14. Preguntas de repaso

Intenta responder cada una antes de abrir la respuesta.

1. ¿Qué evalúa if?

El código de salida de un comando. Si es 0, ejecuta el bloque de then; si no, el de else, si existe.

2. ¿Por qué [ necesita espacios a su alrededor?

Porque es un comando, y lo que va dentro son sus argumentos. Sin el espacio, [ forma una palabra junto con lo que sigue, y Bash busca un comando con ese nombre.

3. ¿Qué recibe [ en [ $x = si ] si x está vacía, y qué ocurre?

Recibe = si ], porque la variable vacía sin comillas desaparece. La expresión queda mal formada y da el error unary operator expected.

4. ¿Por qué [ -n $x ] es cierto cuando x está vacía?

Porque la variable desaparece y queda [ -n ], una prueba con una sola palabra. Con una sola palabra, test es cierto si la palabra no está vacía, y -n no lo está.

5. Menciona tres ventajas de [[ ]] sobre [ ].

No divide las variables ni expande comodines, de modo que una variable vacía o con espacios no rompe la expresión; admite patrones con == y expresiones regulares con =~; y combina condiciones con && y ||. Además, < y > no son redirecciones.

6. ¿Qué diferencia hay entre [[ $f == *.log ]] y [[ $f == "*.log" ]]?

La primera compara con un patrón: es cierta si f termina en .log. La segunda compara con el texto literal *.log.

7. ¿Por qué [[ 10 < 9 ]] es cierto?

Porque < compara cadenas en orden alfabético, carácter por carácter, y 1 va antes que 9. Para comparar números se usa (( 10 < 9 )) o -lt.

8. ¿Qué pasa con [ 5 > 3 ]?

El > es una redirección: se crea un archivo llamado 3 y [ recibe solo 5, que es una prueba siempre cierta. No hay ningún error.

9. ¿Qué diferencia hay entre -e, -f y -s?

-e es cierta si la ruta existe, sea lo que sea; -f, si es un archivo común; -s, si existe y no está vacía.

10. ¿Cómo se distingue un enlace simbólico roto?

Con -L es cierto, porque el enlace existe; con -e es falso, porque -e sigue el enlace y lo que apunta no existe.

11. ¿Qué devuelve [[ fuente.c -nt programa ]] si programa no existe?

Cierto. -nt considera que un archivo existente es más nuevo que uno inexistente, que es lo que hace falta para compilar por primera vez.

12. ¿Cómo se pregunta si la variable opcional DEPURAR tiene valor, en un script con set -u?

Con [[ -n "${DEPURAR:-}" ]]. El valor por defecto vacío evita el error de set -u si la variable no existe.

13. ¿Qué está mal en if [ grep -q error log.txt ]; then?

Que los corchetes no forman parte de if: [ recibe las palabras grep, -q, etc., como argumentos de su expresión, y da un error. Se escribe if grep -q error log.txt; then.

14. ¿Cuándo conviene usar [ ] en lugar de [[ ]]?

Cuando el script debe funcionar en sh o en otras shells POSIX que no tienen [[ ]]. En ese caso, todas las variables van entre comillas.

Para practicar

  1. Compara [ $x = si ], [ "$x" = si ] y [[ $x = si ]] con x vacía, con x=si y con x="no lo sé".
  2. Escribe un script que reciba un nombre y diga si es un archivo común, un directorio, un enlace simbólico o no existe, y si puedes leerlo y escribirlo.
  3. Prueba [[ $f == *.txt ]] y [[ $f == "*.txt" ]] con f=notas.txt y con f='*.txt'.
  4. Compara 2 y 10 con < dentro de [[ ]], con -lt y con (( )).
  5. Crea dos archivos con touch, el segundo un segundo después, y compáralos con -nt y -ot. Prueba también contra un archivo que no existe.
  6. Escribe un script que convierta un archivo .md en .txt (por ejemplo, copiándolo) solo si el .txt no existe o es más viejo que el .md.
  7. Instala comprobar-respaldo y pruébalo con un archivo reciente, uno vacío, un directorio y un archivo modificado con touch -d '3 days ago'.