Lección 3 de 8: condiciones, bucles y códigos de salida en Bash. Un script casi nunca va en línea recta: comprueba si existe un archivo, repite una tarea con cada uno, lee un registro línea a línea o se detiene si algo falla. Esta lección cubre if y su relación con los códigos de salida, [ ] y [[ ]], && y ||, case, los bucles for, while y until, y cómo terminar con exit.
if: decidir según el código de salida
En la lección 1 viste que cada orden termina con un código de salida, 0 si fue bien. if se basa en eso: ejecuta una orden y, si su código es 0, entra en el bloque then; si no, en el else.
#!/usr/bin/env bash
if grep -q pan notas.txt; then
echo "Hay que comprar pan"
else
echo "No hace falta pan"
fi
if grep -q queso notas.txt; then
echo "Hay que comprar queso"
elif grep -q Luis notas.txt; then
echo "No hay queso, pero hay que llamar a Luis"
else
echo "Nada que hacer"
fisalidaHay que comprar pan No hay queso, pero hay que llamar a Luis
La sintaxis es estricta: then va en la línea siguiente o detrás de un ;, elif es «si no, si», y el bloque se cierra con fi, if al revés. grep -q no escribe nada: solo busca y sale con 0 si encuentra el texto, que es justo lo que necesita if.
Comparar: test, [ ] y [[ ]]
Para comparar textos, números o comprobar archivos hay una orden, test, que también se escribe [. Sí: [ es el nombre de una orden, y por eso necesita espacios alrededor, como cualquier otra. Bash añade una versión mejorada, [[ ]], que es parte del lenguaje:
#!/usr/bin/env bash nombre="Ana" edad=17 if [[ "$nombre" == "Ana" ]]; then echo "1: se llama Ana"; fi if [[ "$nombre" != "Luis" ]]; then echo "2: no es Luis"; fi if [[ -z "$sin_definir" ]]; then echo "3: la variable está vacía"; fi if [[ -n "$nombre" ]]; then echo "4: nombre tiene algo"; fi if (( edad >= 18 )); then echo "5: mayor de edad"; else echo "5: menor de edad"; fi if [[ $edad -lt 18 ]]; then echo "6: también con -lt"; fi if [[ -f informe.txt ]]; then echo "7: informe.txt es un archivo"; fi if [[ -d informe.txt ]]; then echo "8: es una carpeta"; else echo "8: no es una carpeta"; fi if [[ -s vacio.txt ]]; then echo "9: vacio.txt tiene datos"; else echo "9: vacio.txt está vacío"; fi if [[ ! -e no-existe.txt ]]; then echo "10: no-existe.txt no existe"; fi if [[ "$nombre" == A* && $edad -gt 10 ]]; then echo "11: empieza por A y tiene más de 10"; fi
salida1: se llama Ana 2: no es Luis 3: la variable está vacía 4: nombre tiene algo 5: menor de edad 6: también con -lt 7: informe.txt es un archivo 8: no es una carpeta 9: vacio.txt está vacío 10: no-existe.txt no existe 11: empieza por A y tiene más de 10
- Para…Se escribeEjemplo
- Textos iguales o distintos
==,!=[[ "$a" == "$b" ]] - Texto vacío o no vacío
-z,-n[[ -z "$nombre" ]] - Números (dentro de [[ ]])
-eq -ne -lt -le -gt -ge[[ $n -ge 10 ]] - Números, más legible
(( ))con== != < <= > >=(( n >= 10 )) - Archivo, carpeta, existe
-f,-d,-e[[ -d copias ]] - Archivo con datos, legible, escribible
-s,-r,-w[[ -r config.ini ]] - Negar, y, o
!,&&,||[[ -f a && ! -f b ]] - Patrón con comodines
==sin comillas a la derecha[[ $f == *.jpg ]] - Expresión regular
=~[[ $cp =~ ^[0-9]{5}$ ]]
Dos detalles que confunden al principio. Para números, < y > solo funcionan dentro de (( )); dentro de [[ ]] comparan textos en orden alfabético, como verás en el reto 3. Y en [[ ]], lo que va a la derecha de == sin comillas es un patrón (A* es «empieza por A»); con comillas, un texto literal.
Expresiones regulares con =~
#!/usr/bin/env bash
for fecha in 2026-10-30 30/10/2026 2026-1-5; do
if [[ $fecha =~ ^([0-9]{4})-([0-9]{2})-([0-9]{2})$ ]]; then
echo "$fecha: año ${BASH_REMATCH[1]}, mes ${BASH_REMATCH[2]}, día ${BASH_REMATCH[3]}"
else
echo "$fecha: no tiene el formato AAAA-MM-DD"
fi
donesalida2026-10-30: año 2026, mes 10, día 30 30/10/2026: no tiene el formato AAAA-MM-DD 2026-1-5: no tiene el formato AAAA-MM-DD
=~ compara con una expresión regular extendida, y lo que va entre paréntesis queda en el array BASH_REMATCH. La expresión va sin comillas: con comillas, Bash la trataría como texto literal. Las expresiones regulares se ven a fondo en la lección 6.
El error que te ahorra: los corchetes simples con una variable vacía
[ ] es la versión antigua, la que entiende cualquier shell POSIX, y tiene un problema: es una orden normal, así que sus argumentos pasan por la división en palabras. Con una variable vacía y sin comillas, la comparación se queda sin uno de sus lados:
#!/usr/bin/env bash
respuesta=""
if [ $respuesta = si ]; then
echo "Confirmado"
else
echo "No confirmado"
fisalida./corchete_vacio.sh: line 3: [: =: unary operator expected No confirmado
Bash vio [ = si ], que no tiene sentido, se quejó y, como [ falló, siguió por el else, así que el script parece funcionar. ShellCheck lo ve antes:
shellcheckIn corchete_vacio.sh line 3:
if [ $respuesta = si ]; then
^--------^ SC2086 (info): Double quote to prevent globbing and word splitting.
Did you mean:
if [ "$respuesta" = si ]; then
For more information:
https://www.shellcheck.net/wiki/SC2086 -- Double quote to prevent globbing ...Hay dos arreglos: comillas, [ "$respuesta" = si ], o [[ $respuesta == si ]], que no divide en palabras. En scripts de Bash, usa siempre [[ ]]; [ ] solo hace falta en scripts para sh.
&& y ||: condiciones en una línea
orden1 && orden2 ejecuta la segunda solo si la primera fue bien, y orden1 || orden2, solo si falló. Son muy habituales para cosas cortas:
#!/usr/bin/env bash
mkdir -p copias && echo "Carpeta lista"
[[ -f config.ini ]] || echo "Aviso: no hay config.ini, uso los valores por omisión"
cd /no/existe 2>/dev/null || { echo "No pude entrar en la carpeta" >&2; exit 1; }
echo "Esta línea no se llega a ejecutar"salida con errorCarpeta lista Aviso: no hay config.ini, uso los valores por omisión No pude entrar en la carpeta código de salida: 1
La tercera línea es un patrón que verás en muchos scripts: si cd falla, se escribe un mensaje en la salida de errores (>&2, que se explica en la lección 5) y se termina con exit 1. Las llaves agrupan las dos órdenes, y el ; antes de la llave de cierre es obligatorio. Seguir trabajando después de un cd fallido, en otra carpeta, es de los errores más peligrosos de un script.
a && b || c. Parece un «si a entonces b, si no c», pero c también se ejecuta si a fue bien y b falló. Para un si-entonces-si-no de verdad, usa if. ShellCheck lo avisa con SC2015.case: elegir entre varias opciones
#!/usr/bin/env bash
for archivo in foto.JPG datos.csv notas.txt copia.tar.gz script.sh; do
case "$archivo" in
*.jpg|*.JPG|*.png) tipo="imagen" ;;
*.csv|*.txt) tipo="texto" ;;
*.tar.gz|*.zip) tipo="comprimido" ;;
*) tipo="otro" ;;
esac
printf '%-14s %s\n' "$archivo" "$tipo"
donesalidafoto.JPG imagen datos.csv texto notas.txt texto copia.tar.gz comprimido script.sh otro
Cada opción es un patrón con comodines, terminado en ); | separa alternativas, ;; cierra la opción y *) recoge todo lo demás. Se elige la primera que encaja y se cierra con esac. case es la forma más clara de validar lo que llega a un script, como verás en el recuadro de seguridad.
for: repetir con una lista
#!/usr/bin/env bash
for color in rojo verde azul; do
echo "Color: $color"
done
for n in {1..5}; do printf '%s ' "$n"; done; echo
for n in {0..20..5}; do printf '%s ' "$n"; done; echo
for (( i = 3; i > 0; i-- )); do printf '%s... ' "$i"; done; echo "¡ya!"
for registro in *.log; do
lineas=$(wc -l < "$registro")
echo "$registro, líneas: $lineas"
donesalidaColor: rojo Color: verde Color: azul 1 2 3 4 5 0 5 10 15 20 3... 2... 1... ¡ya! abril.log, líneas: 4 enero.log, líneas: 2 febrero.log, líneas: 1 marzo.log, líneas: 3
for variable in lista repite el bloque una vez por elemento. La lista puede ser literal, un rango con llaves ({1..5}, o {0..20..5} de 5 en 5), o un comodín como *.log, que Bash expande a los nombres de archivo, con sus espacios intactos. También existe el for al estilo de C, con (( )). Fíjate en que los archivos salen en orden alfabético, abril antes que enero, y no en el de los meses.
for linea in $(cat archivo). Sin comillas, $( ) se parte por los espacios, no por las líneas, y además expande los comodines que haya en el texto. Para leer un archivo línea a línea está while read, justo debajo.Un comodín que no encuentra nada
#!/usr/bin/env bash
for f in *.pdf; do
echo "Encontrado: $f"
done
shopt -s nullglob
for f in *.pdf; do
echo "Con nullglob: $f"
done
echo "Fin"salidaEncontrado: *.pdf Fin
Si no hay ningún .pdf, Bash deja el patrón tal cual y el bucle se ejecuta una vez con el texto *.pdf, que no es ningún archivo. shopt -s nullglob hace que un comodín sin resultados desaparezca, y el bucle no se ejecuta.
while y until: repetir mientras algo se cumpla
#!/usr/bin/env bash
intentos=0
while (( intentos < 3 )); do
intentos=$(( intentos + 1 ))
echo "Intento $intentos"
done
n=1
until (( n > 100 )); do
n=$(( n * 2 ))
done
echo "La primera potencia de 2 mayor que 100 es $n"
num=0
while IFS= read -r producto || [[ -n "$producto" ]]; do
num=$(( num + 1 ))
echo "$num: [$producto]"
done < compras.txtsalidaIntento 1 Intento 2 Intento 3 La primera potencia de 2 mayor que 100 es 128 1: [pan] 2: [leche con avena] 3: [ huevos] 4: [manzanas]
while repite mientras la orden salga con 0, y until, mientras salga con otro número. La última forma es la manera correcta de leer un archivo línea a línea, y cada pieza tiene su motivo: IFS= conserva los espacios del principio, -r respeta las barras invertidas, < compras.txt manda el archivo a la entrada del bucle y || [[ -n "$producto" ]] recoge la última línea aunque no termine en salto de línea, como pasa con muchos archivos escritos a mano. Hay más casos en Leer un archivo línea a línea.
break y continue
#!/usr/bin/env bash
for n in {1..10}; do
if (( n % 2 == 0 )); then
continue
fi
if (( n > 7 )); then
break
fi
printf '%s ' "$n"
done
echosalida1 3 5 7
continue salta a la siguiente vuelta y break sale del bucle. Los dos aceptan un número para afectar a bucles anidados: break 2 sale de dos niveles.
exit: terminar con un código
Un script bien hecho comprueba lo que recibe y, si algo no cuadra, termina con un mensaje en la salida de errores y un código distinto de 0, para que quien lo llame, sea una persona, otro script o cron, sepa que falló:
#!/usr/bin/env bash
if (( $# != 1 )); then
echo "Uso: $0 NOMBRE" >&2
exit 2
fi
echo "Hola, $1"terminal$ chmod +x saludo.sh $ ./saludo.sh Ana; echo "código: $?" Hola, Ana código: 0 $ ./saludo.sh; echo "código: $?" Uso: ./saludo.sh NOMBRE código: 2 $ ./saludo.sh Ana && echo "Todo bien" Hola, Ana Todo bien
$# es el número de argumentos y $1, el primero (en la lección 4 están todos). Por convención, exit 0 es éxito, exit 1 un error general y exit 2 un mal uso. Si no escribes exit, el script devuelve el código de su última orden.
Ojo con la seguridad: valida lo que recibe tu script con una lista blanca
Un script que usa un argumento para formar una ruta, un nombre de usuario o una orden debe comprobar antes que el valor es exactamente lo que espera. Lo más seguro es una lista blanca: aceptar solo los caracteres permitidos y rechazar todo lo demás, en vez de intentar adivinar qué es peligroso.
#!/usr/bin/env bash
mostrar_informe() {
local nombre=$1
if [[ ! $nombre =~ ^[a-z0-9_-]{1,32}$ ]]; then
echo "Nombre de informe no válido: $nombre" >&2
return 1
fi
echo "Abriría informes/$nombre.txt"
}
for pedido in ventas_2026 "../../etc/passwd" "ventas; rm -rf ~" ""; do
mostrar_informe "$pedido" || echo " (rechazado, sigo con el siguiente)"
donesalidaAbriría informes/ventas_2026.txt
Nombre de informe no válido: ../../etc/passwd
(rechazado, sigo con el siguiente)
Nombre de informe no válido: ventas; rm -rf ~
(rechazado, sigo con el siguiente)
Nombre de informe no válido:
(rechazado, sigo con el siguiente)La expresión regular solo deja pasar minúsculas, cifras, guion y guion bajo, de 1 a 32 caracteres, así que los intentos de salir de la carpeta (../) o de colar otra orden se quedan fuera sin necesidad de preverlos uno a uno. Las funciones y local se ven en la lección siguiente.
Más en Los fallos de seguridad típicos de los scripts de Bash.
Retos
Reto 1. Escribe el clásico FizzBuzz del 1 al 15: para cada número, escribe «Fizz» si es múltiplo de 3, «Buzz» si es múltiplo de 5, «FizzBuzz» si lo es de los dos, y el número en cualquier otro caso.
Ver la solución
#!/usr/bin/env bash
for n in {1..15}; do
if (( n % 15 == 0 )); then
echo "FizzBuzz"
elif (( n % 3 == 0 )); then
echo "Fizz"
elif (( n % 5 == 0 )); then
echo "Buzz"
else
echo "$n"
fi
donesalida1 2 Fizz 4 Buzz Fizz 7 8 Fizz Buzz 11 Fizz 13 14 FizzBuzz
El orden importa: hay que preguntar primero por 15, porque si no, el 15 se quedaría en «Fizz». (( )) devuelve 0 cuando la cuenta es verdadera, así que encaja directamente con if.
Reto 2. Cuenta cuántos archivos de la carpeta actual son imágenes (.png o .jpg), cuántos registros (.log) y cuántos otra cosa, con un for y un case.
Ver la solución
#!/usr/bin/env bash
shopt -s nullglob
imagenes=0 registros=0 otros=0
for f in *; do
[[ -f $f ]] || continue
case "$f" in
*.png|*.jpg) imagenes=$(( imagenes + 1 )) ;;
*.log) registros=$(( registros + 1 )) ;;
*) otros=$(( otros + 1 )) ;;
esac
done
echo "Imágenes: $imagenes, registros: $registros, otros: $otros"salidaImágenes: 2, registros: 2, otros: 2
El script se cuenta a sí mismo entre los «otros», porque también está en la carpeta. [[ -f $f ]] || continue salta las carpetas.
Reto 3. ¿Qué escribe este script? Piénsalo antes de mirar: if [[ 10 < 9 ]]; then echo "10 es menor"; else echo "9 es menor"; fi.
Ver la solución
#!/usr/bin/env bash
if [[ 10 < 9 ]]; then
echo "10 es menor"
else
echo "9 es menor"
fisalida10 es menor
Escribe «10 es menor». Dentro de [[ ]], < compara textos en orden alfabético, y «1» va antes que «9», igual que «abeja» va antes que «zorro». Para números hay que usar -lt o (( 10 < 9 )). ShellCheck lo detecta:
shellcheckIn reto3.sh line 2:
if [[ 10 < 9 ]]; then
^-- SC2071 (error): < is for string comparisons. Use -lt instead.
^-- SC2050 (warning): This expression is constant. Did you forget the $ on a variable?
For more information:
https://www.shellcheck.net/wiki/SC2071 -- < is for string comparisons. Use ...
https://www.shellcheck.net/wiki/SC2050 -- This expression is constant. Did ...Todos los scripts de esta lección se ejecutaron con Bash 5.3 en Ubuntu 26.04, en un contenedor limpio sin acceso a la red, y se revisaron con ShellCheck 0.11 antes de publicarla; las salidas están copiadas de esa ejecución. ¿Algo se comporta distinto? Mira tu versión con bash --version: macOS trae de serie Bash 3.2, y en Windows hay que usar WSL o Git Bash.
