1. La forma de for
El bucle for asigna a una variable, uno por uno, cada elemento de una lista, y ejecuta el cuerpo del bucle una vez para cada uno:
for variable in elemento1 elemento2 elemento3; do
comandos que usan "$variable"
done
$ for servidor in web01 web02 db01; do echo "Revisando $servidor"; done
Revisando web01
Revisando web02
Revisando db01
Como con if y then, el ; antes de do puede ser un salto de línea, y en este curso do va en la misma línea que for. La variable del bucle es una variable común: se escribe sin $ en el for y con $, entre comillas, al usarla. Conviene darle un nombre que diga qué contiene, en singular: for archivo in, for servidor in.
2. De dónde sale la lista
Lo que sigue a in es una lista de palabras, y pasa por todas las expansiones del tema 3. Eso significa que la lista puede escribirse de muchas formas, y que las reglas de las comillas deciden cuántos elementos tiene:
| Lista | Elementos | Cuándo usarla |
|---|---|---|
a b "c d" | Las palabras escritas: a, b, c d. | Listas fijas y breves. |
"${lista[@]}" | Los elementos de un array, intactos. | Listas construidas por el script (tema 8). |
"$@" | Los argumentos del script, intactos. | Procesar todo lo que se le pasó. |
*.log | Los archivos que coinciden con el patrón. | Procesar archivos. |
{1..10} | Los números del 1 al 10. | Repetir una cantidad fija de veces. |
$(comando) | La salida del comando, dividida en palabras. | Solo si la salida son palabras sin espacios (sección 9). |
$variable | El valor de la variable, dividido en palabras. | Casi nunca: para eso están los arrays. |
Las dos últimas filas son las delicadas, por la división en palabras y la expansión de comodines. Las demás son seguras siempre que se respeten las comillas: "${lista[@]}" y "$@" con comillas; los patrones de archivos, sin ellas.
3. Recorrer los argumentos
Un script que procesa varios archivos, servidores o nombres los recibe como argumentos, y los recorre con "$@". Es tan común que tiene una forma abreviada: un for sin in recorre los argumentos.
for archivo in "$@"; do
gzip -- "$archivo"
done
for archivo; do # sin «in»: recorre "$@"
gzip -- "$archivo"
done
La primera forma es más explícita y es la que usará el curso. Lo que nunca debe hacerse es escribir $@ o $* sin comillas, que dividiría en pedazos los argumentos con espacios. Veremos los argumentos en detalle en el tema 20.
4. Recorrer archivos
Con un patrón de comodines en la lista, el bucle recorre los archivos que coinciden. Cada nombre es un elemento, con sus espacios intactos, porque la expansión de nombres de archivo ocurre después de la división en palabras:
a b.txt y c.txt$ for f in *.txt; do echo "<$f>"; done
<a b.txt>
<c.txt>
El patrón puede incluir una ruta, y entonces cada elemento la incluye también. La parte variable, si la hay, va entre comillas, y el comodín fuera de ellas:
for registro in "$dir"/*.log; do
nombre="${registro##*/}" # sin el directorio (tema 6)
echo "Procesando $nombre"
done
Los archivos se recorren en orden alfabético, según la configuración regional. Y dentro del cuerpo, la variable se usa siempre entre comillas, como cualquier nombre de archivo: "$registro".
5. Cuando no hay archivos: nullglob
Como vimos en el tema 3, un patrón que no coincide con ningún archivo queda tal cual. En un bucle, eso significa una vuelta con un nombre que no existe:
$ for f in *.pdf; do echo "Procesando [$f]"; done
Procesando [*.pdf]
Hay tres formas de resolverlo:
| Solución | Efecto | Cuándo conviene |
|---|---|---|
[[ -e "$f" ]] || continue al comienzo del cuerpo | Salta la vuelta si el archivo no existe. | En un bucle aislado, sin cambiar opciones de Bash. Es lo que usamos hasta ahora. |
shopt -s nullglob | Un patrón sin coincidencias se convierte en nada: el bucle no da ninguna vuelta. | En scripts que recorren archivos a menudo. Se activa una vez al comienzo. |
shopt -s failglob | Un patrón sin coincidencias es un error: el comando no se ejecuta. | Cuando la falta de archivos indica un problema. |
$ shopt -s nullglob
$ for f in *.pdf; do echo "Procesando [$f]"; done # ninguna vuelta
$ pdfs=(*.pdf); echo "Hay ${#pdfs[@]} PDF"
Hay 0 PDF
nullglob hace también que un array llenado con un patrón quede vacío, como en el tema 8, lo que permite contar los archivos de forma fiable. Tiene un efecto secundario que conviene conocer: afecta a todos los patrones del script, incluidos los argumentos de cualquier comando. ls *.pdf, sin coincidencias, se convierte en ls a secas, que lista todo el directorio. Por eso algunos prefieren activarlo solo alrededor del bucle, y desactivarlo después con shopt -u nullglob.
6. Ocultos y subdirectorios: dotglob y globstar
Otras dos opciones cambian qué archivos entran en un patrón:
| Opción | Efecto |
|---|---|
shopt -s dotglob | * incluye también los archivos ocultos, los que empiezan con punto. Nunca incluye . ni ... |
shopt -s globstar | ** coincide con cualquier cantidad de directorios: **/*.txt son los .txt de este directorio y de todos sus subdirectorios. |
a b.txt, c.txt, .oculto.txt y sub/d.txt$ for f in *.txt; do echo -n "<$f> "; done; echo
<a b.txt> <c.txt>
$ shopt -s dotglob
$ for f in *.txt; do echo -n "<$f> "; done; echo
<.oculto.txt> <a b.txt> <c.txt>
$ shopt -u dotglob; shopt -s globstar
$ for f in **/*.txt; do echo -n "<$f> "; done; echo
<a b.txt> <c.txt> <sub/d.txt>
globstar permite recorrer un árbol de directorios sin find, y para árboles pequeños o medianos es cómodo. Para árboles enormes, o cuando hacen falta criterios como el tamaño o la fecha, find sigue siendo la herramienta, como veremos en el tema 32: ** arma la lista completa en memoria antes de dar la primera vuelta.
7. Rangos de números
Para repetir algo una cantidad de veces, o recorrer números, la expansión de llaves genera la lista:
$ echo {1..5}
1 2 3 4 5
$ echo {01..10} # con ceros a la izquierda
01 02 03 04 05 06 07 08 09 10
$ echo {0..20..5} # con paso
0 5 10 15 20
$ echo {a..e}
a b c d e
$ for dia in {01..31}; do mkdir -p "2026-10-$dia"; done
La limitación, como vimos en el tema 3, es que las llaves se expanden antes que las variables: {1..$n} no funciona. Cuando el límite está en una variable, hay dos opciones: el comando seq, que genera la secuencia como salida, o la forma al estilo C de la sección siguiente.
for i in $(seq 1 "$n"); do # la salida son números: dividirla es seguro
echo "Intento $i de $n"
done
for i in $(seq -w 1 "$n"); do # -w: mismo ancho, con ceros (01, 02…)
8. La forma al estilo C
Bash tiene una segunda forma de for, tomada del lenguaje C, que no recorre una lista sino que repite mientras se cumpla una condición aritmética. Entre los dobles paréntesis van tres expresiones, separadas por punto y coma: la inicial, la condición y el paso.
for (( i = 1; i <= n; i++ )); do
echo "Vuelta $i de $n"
done
- Antes de empezar, se evalúa la expresión inicial:
i = 1. una vez - Antes de cada vuelta, se evalúa la condición:
i <= n. Si es falsa, el bucle termina. cada vuelta - Se ejecuta el cuerpo. cada vuelta
- Después de cada vuelta, se evalúa el paso:
i++, y se vuelve al punto 2. cada vuelta
Las tres expresiones son aritméticas, como las de (( )) del tema 7: las variables van sin $, y el límite puede estar en una variable. Aquí i++ es seguro aun con set -e, porque su resultado no se usa como código de salida. Esta forma conviene cuando hace falta contar hacia atrás, avanzar de a varios o calcular el índice:
$ for (( i = 10; i > 0; i -= 3 )); do echo -n "$i "; done; echo
10 7 4 1
$ n=${#lista[@]}; for (( i = n - 1; i >= 0; i-- )); do echo "${lista[i]}"; done
La segunda línea recorre un array del último al primero. Recuerda la advertencia del tema 8: si el array puede tener huecos, recorrer índices calculados se salta elementos; en ese caso, mejor "${!lista[@]}".
9. Por qué no recorrer líneas con for
Uno de los errores más extendidos en Bash es intentar recorrer las líneas de un archivo así:
$ for linea in $(cat lineas.txt); do echo "<$linea>"; done
<línea>
<uno>
<línea>
<dos>
for no recorre líneas: recorre palabras. La salida de $(cat …) pasa por la división en palabras, que corta por espacios además de saltos de línea, y por la expansión de comodines, de modo que una línea con un * se convertiría en una lista de archivos. Además, el archivo entero se carga en memoria antes de empezar.
Para recorrer líneas existe while IFS= read -r línea, que es el tema 16. Y para recorrer la salida de un comando, la regla es la misma que para llenar un array en el tema 8: si la salida son palabras sin espacios ni comodines, como nombres de usuario o números, for x in $(comando) es aceptable; si no, mapfile y un bucle sobre el array, o while read.
for usuario in $(cut -d: -f1 /etc/passwd); do # nombres de usuario: sin espacios
…
done
mapfile -t archivos < <(find . -name '*.log' -mtime +7)
for archivo in "${archivos[@]}"; do # nombres de archivo: pueden tener espacios
…
done
10. La salida de un bucle entero
Un bucle completo, de for a done, es un solo comando compuesto, y su salida puede redirigirse o conectarse a una tubería como la de cualquier comando. La redirección se escribe después de done, y afecta a todo lo que el bucle escribe:
for servidor in "${servidores[@]}"; do
printf '%s\t%s\n' "$servidor" "$(ssh "$servidor" uptime)"
done > informe-carga.txt # un solo archivo, abierto una vez
for archivo in *.log; do
printf '%d %s\n' "$(wc -l < "$archivo")" "$archivo"
done | sort -rn | head -5 # los cinco registros más largos
Redirigir al final es mejor que escribir >> informe.txt en cada vuelta: el archivo se abre una sola vez, y no queda mezclado con el contenido de una ejecución anterior. La tubería después de done, como en el script por-extension del tema 8, permite ordenar o filtrar todo lo que el bucle produjo.
Hay un precio: cuando un bucle es parte de una tubería, se ejecuta en un proceso aparte, y las variables que modifique, como un contador, no conservan su valor después. Es el mismo problema que vimos con read y mapfile, y lo resolveremos en el tema 16.
11. Detalles del funcionamiento
Tres comportamientos de for que conviene conocer:
- La lista se calcula una sola vez, al comienzo. Si el cuerpo crea o borra archivos, no cambia la lista de vueltas. Un bucle que renombra
*.txta*.mdno vuelve a procesar los nuevos, y uno que borra archivos puede encontrarse con nombres que ya no existen. - La variable conserva el último valor al terminar. Después de
for x in a b c; do …; done,xvalec. No conviene depender de eso, pero explica por qué una variable que parecía «del bucle» sigue existiendo. - Un bucle que no da ninguna vuelta termina con código 0. Si da vueltas, su código es el del último comando ejecutado en el cuerpo. En un script con
set -e, un comando que falla dentro del cuerpo detiene todo el script, no solo esa vuelta.
El último punto lleva a una pregunta de diseño: cuando un bucle procesa muchos elementos y uno falla, ¿debe detenerse todo, o seguir con los demás y avisar al final? Para un despliegue en varios servidores, quizá detenerse; para comprimir cien registros, seguramente seguir. La forma de seguir es manejar el fallo dentro del cuerpo, con un if, como hizo verificar-sitios en el tema 10, y contar los errores para informarlos y devolver el código adecuado al terminar. Los comandos continue y break, que saltan una vuelta o terminan el bucle, son el tema 17.
12. Un script completo
Una aplicación que escribe siempre en el mismo archivo de registro lo hace crecer sin límite. La solución clásica es rotarlo: cada día, el registro actual pasa a ser la copia .1, la anterior pasa a .2 comprimida, y así hasta un número de copias, más allá del cual se descartan. Linux trae una herramienta para esto, logrotate, pero escribir la versión en Bash muestra los dos tipos de for trabajando juntos.
#!/usr/bin/env bash
#
# rotar-registros — rota los registros *.log de un directorio:
# app.log → app.log.1 → app.log.2.gz → … conservando un número de copias.
#
# Uso: rotar-registros [DIRECTORIO]
# Entorno: COPIAS copias a conservar, incluida la .1 (5; mínimo 2)
#
set -euo pipefail
shopt -s nullglob
dir="${1:-.}"
copias="${COPIAS:-5}"
[[ -d "$dir" ]] || { echo "No existe el directorio $dir" >&2; exit 1; }
if [[ ! "$copias" =~ ^[0-9]+$ ]] || (( copias < 2 )); then
echo "COPIAS debe ser un número entero mayor o igual que 2" >&2
exit 2
fi
rotados=0
for registro in "$dir"/*.log; do
[[ -s "$registro" ]] || continue # vacío: nada que rotar
rm -f -- "$registro.$copias.gz" # la copia más vieja se descarta
for (( i = copias - 1; i >= 2; i-- )); do # .4.gz → .5.gz, …, .2.gz → .3.gz
if [[ -e "$registro.$i.gz" ]]; then
mv -- "$registro.$i.gz" "$registro.$(( i + 1 )).gz"
fi
done
if [[ -e "$registro.1" ]]; then # la copia anterior se comprime
gzip -f -- "$registro.1"
mv -- "$registro.1.gz" "$registro.2.gz"
fi
cp -p -- "$registro" "$registro.1" # la copia nueva, sin comprimir
: > "$registro" # vaciar el original sin borrarlo
rotados=$(( rotados + 1 ))
done
echo "Registros rotados: $rotados"
COPIAS=3$ ls logs
app.log app.log.1 app.log.2.gz app.log.3.gz vacio.log web.log web.log.1 web.log.2.gz web.log.3.gz
$ zcat logs/app.log.3.gz; zcat logs/app.log.2.gz; cat logs/app.log.1
registro del miércoles
registro del jueves
registro del viernes
- Un
forcon lista recorre los registros, y uno al estilo C, dentro de él, desplaza las copias. El interior cuenta hacia atrás a propósito: si moviera primero.2a.3, pisaría la.3antes de haberla movido a.4. nullglobhace que el bucle no dé ninguna vuelta si el directorio no tiene registros, sin necesidad de comprobar cada nombre.- La copia
.1queda sin comprimir, porque es la que más probablemente se consulte, y la aplicación podría seguir escribiendo en ella si se hubiera movido en lugar de copiado. cpy luego: > "$registro"en lugar demv: una aplicación en ejecución mantiene abierto su registro, y si se lo moviera seguiría escribiendo en el archivo movido. Copiarlo y vaciar el original, que es lo quelogrotatellama copytruncate, evita reiniciar la aplicación, a cambio de un riesgo pequeño: las líneas escritas entre la copia y el vaciado se pierden.- La validación de
COPIAScombina la expresión regular y la comparación aritmética de los temas 7 y 12. Con menos de dos copias, el desplazamiento no tendría sentido.
13. Errores frecuentes
| Escribiste | Qué pasa | Corrección |
|---|---|---|
for f in $(ls *.txt) | Los nombres con espacios se parten. | for f in *.txt |
for f in "*.txt" | Una sola vuelta, con el texto *.txt. | El patrón sin comillas. |
for f in *.txt sin archivos | Una vuelta con el texto *.txt. | shopt -s nullglob, o [[ -e $f ]] || continue. |
for a in $@ | Los argumentos con espacios se parten. | for a in "$@" |
for linea in $(cat archivo) | Recorre palabras, no líneas, y expande comodines. | while IFS= read -r linea (tema 16). |
for i in {1..$n} | Una vuelta con el texto {1..5}. | for (( i = 1; i <= n; i++ )) o seq. |
cmd "$f" >> salida.txt en cada vuelta | Se acumula sobre una ejecución anterior; el archivo se abre en cada vuelta. | done > salida.txt |
… done | sort y luego usar un contador del bucle | El contador vale lo que tenía antes del bucle. | Ver el tema 16. |
| Desplazar copias de menor a mayor | Cada copia pisa la siguiente antes de moverla. | Recorrer de mayor a menor. |
14. Resumen
for variable in lista; do …; doneejecuta el cuerpo una vez por cada elemento de la lista.- La lista pasa por las expansiones:
"${array[@]}"y"$@"entre comillas conservan cada elemento; los patrones de archivos, sin comillas, dan un nombre por elemento. for x, sinin, recorre los argumentos.- Un patrón sin coincidencias da una vuelta con el patrón literal.
nullglobhace que no dé ninguna;failglob, que sea un error. dotglobincluye los archivos ocultos en*;globstaractiva**para recorrer subdirectorios.{1..10},{01..10}y{0..20..5}generan rangos fijos; para límites en variables,seqo la forma al estilo C.for (( inicio; condición; paso ))repite mientras la condición aritmética sea cierta; permite contar hacia atrás y avanzar de a varios.forrecorre palabras, no líneas:for x in $(cat archivo)es un error. Para líneas,while read.- La salida de un bucle entero se redirige o se conecta después de
done; en una tubería, las variables del bucle no sobreviven. - La lista se calcula una vez; la variable conserva el último valor; un bucle sin vueltas termina con 0.
- Cuando un elemento falla, se decide si detener todo o seguir y contar los errores, manejando el fallo dentro del cuerpo.
15. Preguntas de repaso
Intenta responder cada una antes de abrir la respuesta.
1. ¿Cuántas vueltas da for x in a "b c" d; do …; done?
Tres: a, b c y d. Las comillas mantienen b c como un solo elemento.
2. ¿Qué recorre for archivo; do …; done?
Los argumentos con que se llamó al script o a la función, igual que for archivo in "$@".
3. ¿Por qué for f in *.txt no parte los nombres con espacios, y for f in $(ls *.txt) sí?
Porque la expansión de nombres de archivo ocurre después de la división en palabras, y cada nombre generado es un elemento. La salida de ls, en cambio, es texto que pasa por la división en palabras y se corta por los espacios.
4. ¿Qué hace un bucle sobre *.pdf en un directorio sin PDF, con y sin nullglob?
Sin nullglob, da una vuelta con el texto literal *.pdf. Con nullglob, el patrón desaparece y el bucle no da ninguna vuelta.
5. ¿Qué efecto secundario tiene activar nullglob en todo el script?
Afecta a todos los patrones, no solo a los de los bucles. Un comando como ls *.pdf sin coincidencias se convierte en ls sin argumentos, y lista todo el directorio.
6. ¿Qué hacen dotglob y globstar?
dotglob hace que * incluya los archivos que empiezan con punto. globstar activa **, que coincide con cualquier cantidad de directorios, para recorrer un árbol.
7. ¿Por qué for i in {1..$n} no funciona, y qué alternativas hay?
Porque la expansión de llaves ocurre antes que la de variables, y en ese momento {1..$n} no es un rango válido. Se usa for (( i = 1; i <= n; i++ )) o for i in $(seq 1 "$n").
8. ¿En qué orden se evalúan las tres expresiones de for (( a; b; c ))?
a una vez al comienzo; b antes de cada vuelta, y si es falsa el bucle termina; c después de cada vuelta.
9. ¿Por qué for linea in $(cat archivo) no recorre las líneas?
Porque for recorre palabras. La salida de la sustitución se divide por espacios, tabuladores y saltos de línea, y además se expanden los comodines. Para leer líneas se usa while IFS= read -r linea.
10. ¿Qué ventaja tiene done > informe.txt sobre >> informe.txt dentro del bucle?
El archivo se abre una sola vez y se reemplaza su contenido, en lugar de abrirlo en cada vuelta y acumular sobre lo que hubiera de ejecuciones anteriores.
11. Si el cuerpo de un bucle sobre *.txt crea archivos .txt nuevos, ¿los procesa el bucle?
No. La lista se calcula una sola vez, antes de la primera vuelta.
12. En rotar-registros, ¿por qué las copias se desplazan de mayor a menor?
Porque si se moviera primero .2 a .3, se pisaría la copia .3 antes de haberla movido a .4. Empezando por la más vieja, cada destino ya quedó libre.
13. ¿Por qué rotar-registros copia y vacía el registro en lugar de moverlo?
Porque la aplicación mantiene el archivo abierto. Si se lo moviera, seguiría escribiendo en el archivo movido. Copiarlo y vaciar el original permite seguir sin reiniciar la aplicación.
Para practicar
- Escribe un bucle que cree los directorios
eneroadiciembredentro de un directorio2026, usando una lista escrita. - Crea archivos con espacios en el nombre y recórrelos con
for f in *y confor f in $(ls). Compara. - Recorre
*.pdfen un directorio sin PDF con y sinnullglob, y prueba tambiénfailglob. - Usa
globstarpara contar cuántos archivos.shhay en tu directorio personal y sus subdirectorios. - Escribe un script que reciba un número
ny muestre una cuenta regresiva dena 1 con la forma al estilo C, esperando un segundo entre números. - Escribe un bucle que muestre, para cada archivo
.txtdel directorio, su cantidad de líneas y su nombre, y conecta la salida asort -n. - Prueba
rotar-registrosen un directorio de prueba, agregando una línea al registro antes de cada ejecución, como en el ejemplo. Comprueba que nunca haya más copias que las indicadas.