1. La forma correcta
Esta es la forma de leer un archivo línea por línea, cada línea exactamente como está escrita:
while IFS= read -r linea; do
printf '%s\n' "$linea"
done < archivo.txt
En cada vuelta, read lee la línea siguiente y la guarda en linea. Al llegar al final del archivo, read termina con código 1, la condición del while falla y el bucle termina. La redirección < archivo.txt, escrita después de done, conecta el archivo a la entrada de todo el bucle: read va consumiendo líneas de esa entrada, una por vuelta.
2. Cada pieza, y por qué
La forma tiene tres detalles que parecen opcionales y no lo son. Cada uno corrige un comportamiento de read que vimos en el tema 9:
| Pieza | Sin ella | Ejemplo de lo que se pierde |
|---|---|---|
IFS= | read quita los espacios y tabuladores del principio y del final de la línea. | La sangría de un archivo de configuración o de código. |
-r | read interpreta las barras invertidas y las elimina. | Rutas de Windows, expresiones regulares, una línea terminada en \, que se une con la siguiente. |
< archivo después de done | Si se escribe en el read, el archivo se reabre en cada vuelta y siempre se lee la primera línea. | Un bucle infinito sobre la primera línea. |
$ cat -A ejemplo.txt # -A muestra los caracteres invisibles
sin sangría$
con cuatro espacios$
C:\temp\nuevo$
$ while read linea; do echo "[$linea]"; done < ejemplo.txt
[sin sangría]
[con cuatro espacios]
[C:tempnuevo]
$ while IFS= read -r linea; do echo "[$linea]"; done < ejemplo.txt
[sin sangría]
[ con cuatro espacios]
[C:\temp\nuevo]
Si lo que interesa es procesar palabras, y no conservar la línea intacta, quitar los espacios de los extremos puede ser deseable, y entonces IFS= sobra. Pero -r va siempre. En caso de duda, la forma completa es la segura.
3. La última línea sin salto final
En Unix, cada línea de un archivo de texto termina con un salto de línea, incluida la última. Pero muchos editores, sobre todo en Windows, guardan la última línea sin él. read lee esa línea, pero como llegó al final del archivo sin encontrar el salto, termina con código 1, y el while la descarta:
$ printf 'uno\ndos\nsin salto' > lineas.txt
$ while IFS= read -r l; do echo "[$l]"; done < lineas.txt
[uno]
[dos]
$ while IFS= read -r l || [[ -n $l ]]; do echo "[$l]"; done < lineas.txt
[uno]
[dos]
[sin salto]
La corrección es agregar || [[ -n $linea ]] a la condición: si read falló pero dejó algo en la variable, esa última línea se procesa igual. En la vuelta siguiente, read falla y la variable queda vacía, y el bucle termina. Cuando el archivo viene de otras personas o de otros sistemas, conviene usar siempre esta versión.
4. Separar cada línea en campos
Con varias variables, read separa cada línea en campos, igual que en el tema 9. Con IFS indicando el separador, el bucle recorre un archivo de datos estructurado:
while IFS=: read -r usuario _ uid _ _ casa shell; do
if (( uid >= 1000 )) && [[ "$shell" != */nologin ]]; then
printf '%-12s %s\n' "$usuario" "$casa"
fi
done < /etc/passwd
ana /home/ana
luis /home/luis
Con separadores de espacio (el valor normal de IFS), los espacios repetidos cuentan como uno, lo que permite alinear las columnas de un archivo de configuración. Con otro separador, como : o ,, cada separador delimita un campo, aunque esté vacío. Y recuerda que la última variable recibe todo lo que sobra: es la forma de capturar un último campo que puede tener espacios, como una descripción.
5. Comentarios y líneas vacías
Los archivos que escriben las personas tienen comentarios y líneas en blanco para hacerlos legibles. El bucle debe saltarlos, y lo habitual es hacerlo al comienzo del cuerpo, con continue, que pasa directamente a la vuelta siguiente (lo veremos en el tema 17):
# Servidores de producción
web01
web02
# Bases de datos
db01 # la principal
while read -r servidor _ || [[ -n $servidor ]]; do
[[ -z "$servidor" || "$servidor" == \#* ]] && continue
echo "Revisando $servidor"
done < servidores.txt
Aquí read se usa sin IFS= y con dos variables a propósito: quita los espacios de los extremos, de modo que una línea con solo espacios queda vacía, y la variable _ se lleva todo lo que sigue al primer campo, incluido un comentario al final de la línea, como el de db01. El patrón \#* reconoce las líneas que empiezan con #; la barra es necesaria para que # no inicie un comentario de Bash.
6. Leer la salida de un comando
El bucle puede leer la salida de un comando en lugar de un archivo. Hay dos formas de conectarlos, y no son equivalentes:
df -h --output=target,pcent | tail -n +2 | while read -r punto uso; do # con una tubería
echo "$punto: $uso"
done
while read -r punto uso; do # con sustitución de procesos
echo "$punto: $uso"
done < <(df -h --output=target,pcent | tail -n +2)
La segunda forma, < <(comando), ya apareció con mapfile en el tema 8. Es una sustitución de procesos: <(comando) ejecuta el comando y presenta su salida como si fuera un archivo, y el primer < redirige ese «archivo» a la entrada del bucle. El espacio entre los dos < es obligatorio. En este ejemplo las dos formas producen la misma salida. La diferencia aparece en cuanto el bucle modifica una variable.
7. El problema de la tubería
Supón que el bucle cuenta algo, y el script usa el total después:
$ total=0
$ printf 'a\nb\nc\n' | while read -r x; do total=$(( total + 1 )); done
$ echo "total: $total"
total: 0
$ while read -r x; do total=$(( total + 1 )); done < <(printf 'a\nb\nc\n')
$ echo "total: $total"
total: 3
Con la tubería, el contador vale 0 al final. No hay ningún error: el bucle contó correctamente, pero lo hizo en otro proceso. En Bash, cada parte de una tubería se ejecuta en una subshell, un proceso hijo con una copia de las variables. El bucle incrementó la copia, y la copia desapareció al terminar la tubería. Es el mismo fenómeno que vimos con read y mapfile, y que explicamos con el diagrama del tema 5: los cambios en un proceso hijo no vuelven al padre.
| Solución | Forma | Cuándo usarla |
|---|---|---|
| Sustitución de procesos | while …; done < <(comando) | La solución general, y la que usa este curso. |
| Leer de un archivo | while …; done < archivo | Cuando los datos ya están en un archivo: no hay subshell. |
La opción lastpipe | shopt -s lastpipe al comienzo del script | Hace que la última parte de una tubería corra en la shell actual. Solo funciona en scripts, no en la terminal interactiva. |
| Usar el resultado dentro de la tubería | comando | { while …; done; echo "$total"; } | Cuando el valor solo hace falta para mostrarlo al final. |
Mientras el bucle solo muestra cosas, la tubería es perfectamente válida y a veces más legible. La regla práctica: si el bucle modifica variables que se usarán después, la entrada se conecta con < <(…), no con una tubería.
8. Nombres de archivo: find -print0
Leer nombres de archivo de a uno por línea, por ejemplo de la salida de find, funciona casi siempre. El «casi» viene de que un nombre de archivo en Linux puede contener cualquier carácter salvo la barra y el carácter nulo, incluido el salto de línea. Un archivo llamado así rompería el bucle en dos líneas.
La solución robusta es separar los nombres con el carácter nulo, el único que no puede aparecer en ellos. find lo hace con -print0, y read lee hasta un nulo con -d '':
while IFS= read -r -d '' archivo; do
echo "Procesando: $archivo"
done < <(find "$dir" -type f -name '*.log' -mtime +7 -print0)
Es la forma que usó el script por-extension del tema 8, y la que conviene usar cuando los nombres vienen de find y el script debe funcionar con cualquier archivo. Muchos otros programas tienen una opción equivalente: xargs -0, sort -z, grep -z. Volveremos sobre ellas en el tema 32.
9. Comandos que se comen la entrada
Esta es la trampa más desconcertante de los bucles de lectura. El bucle debería procesar tres servidores, pero procesa solo el primero, sin ningún error:
while read -r servidor; do
echo "== $servidor"
ssh "$servidor" uptime
done < servidores.txt
La causa es que todo lo que se ejecuta dentro del bucle comparte su entrada estándar, que es el archivo. ssh, que está pensado para reenviar al servidor remoto lo que se escribe en el teclado, lee de su entrada todo lo que encuentra: el resto del archivo. Cuando le toca de nuevo a read, ya no queda nada. Lo mismo hacen ffmpeg, mysql, otro read que pregunte algo al usuario, o cualquier programa que lea de su entrada.
Hay tres soluciones:
| Solución | Forma |
|---|---|
| Quitarle la entrada al comando | ssh "$servidor" uptime < /dev/null |
| Usar su opción para no leer la entrada | ssh -n "$servidor" uptime |
| Leer el archivo por otro descriptor | while read -r -u 3 servidor; do …; done 3< servidores.txt |
La tercera es la más general: el archivo se abre en el descriptor 3 en lugar del 0, y read -u 3 lee de él. La entrada estándar queda libre, conectada al teclado, y los comandos del cuerpo pueden usarla, por ejemplo para preguntar algo al usuario, sin interferir con la lectura del archivo.
10. Cuándo usar awk en su lugar
Un bucle while read es cómodo pero lento: Bash procesa cada línea con varias operaciones internas, y si el cuerpo ejecuta programas externos, crea procesos en cada vuelta. Para un archivo de cien líneas, irrelevante; para uno de un millón, la diferencia entre segundos y muchos minutos.
| Tarea | Conviene |
|---|---|
| Hacer algo con cada línea: ejecutar un comando, conectarse a un servidor, copiar un archivo | while read |
| Calcular sobre las líneas: sumar, contar, promediar, filtrar columnas | awk |
| Transformar el texto: reemplazar, extraer, reordenar | sed, awk, cut |
| Cargar todas las líneas para usarlas después | mapfile -t (tema 8) |
total=0 # Bash: lento en archivos grandes
while read -r _ bytes _; do
total=$(( total + bytes ))
done < transferencias.txt
total=$(awk '{ s += $2 } END { print s }' transferencias.txt) # awk: una sola pasada
La regla de fondo es la del tema 1: Bash coordina; el trabajo pesado sobre los datos lo hacen herramientas pensadas para eso.
11. Un script completo
Este script lee un archivo de configuración con una lista de servicios, uno por línea con su nombre, su servidor y su puerto, y comprueba que cada uno acepte conexiones. Reúne las técnicas del tema: comentarios y líneas vacías, la última línea sin salto, validación de cada línea con su número, y contadores que se usan después del bucle.
# nombre servidor puerto
web www.example.com 443
correo mail.example.com 25
# bases de datos
base-datos 10.0.0.12 5432
cache 10.0.0.13
#!/usr/bin/env bash
#
# comprobar-servicios — comprueba que los servicios de una lista acepten conexiones.
#
# Uso: comprobar-servicios [ARCHIVO] (por defecto, servicios.conf)
# Cada línea: NOMBRE SERVIDOR PUERTO. Admite comentarios con # y líneas vacías.
# Salida: 0 si todos responden; 1 si alguno no; 2 si no se puede leer el archivo.
#
set -euo pipefail
conf="${1:-servicios.conf}"
[[ -r "$conf" ]] || { echo "No se puede leer $conf" >&2; exit 2; }
numero=0 total=0 caidos=0
while read -r nombre host puerto resto || [[ -n "${nombre:-}" ]]; do
numero=$(( numero + 1 ))
[[ -z "${nombre:-}" || "$nombre" == \#* ]] && continue
if [[ -z "${puerto:-}" || -n "${resto:-}" || ! "$puerto" =~ ^[0-9]+$ ]]; then
echo "$conf:$numero: línea mal formada, se ignora" >&2
continue
fi
total=$(( total + 1 ))
if timeout 3 bash -c 'exec 3<> "/dev/tcp/$1/$2"' _ "$host" "$puerto" 2> /dev/null; then
printf 'OK %-14s %s:%s\n' "$nombre" "$host" "$puerto"
else
printf 'CAÍDO %-14s %s:%s\n' "$nombre" "$host" "$puerto"
caidos=$(( caidos + 1 ))
fi
done < "$conf"
echo "$(( total - caidos )) de $total servicios responden"
(( caidos == 0 ))
$ comprobar-servicios; echo "código $?"
OK web www.example.com:443
OK correo mail.example.com:25
CAÍDO base-datos 10.0.0.12:5432
servicios.conf:7: línea mal formada, se ignora
2 de 3 servicios responden
código 1
readcon cuatro variables separa el nombre, el servidor y el puerto, y deja enrestocualquier cosa de más. Así, una línea con un campo de menos o de más se detecta y se informa con su número, en el formatoarchivo:línea:que entienden los editores.${nombre:-}y sus parientes protegen deset -u: en una línea vacía,readdeja las variables vacías, y en la última vuelta pueden no haberse asignado.- La conexión se prueba con
/dev/tcpdentro de unbash -c, para quetimeoutpueda interrumpirla si el servidor no contesta. El servidor y el puerto se pasan como argumentos ($1y$2dentro delbash -c) y no se insertan en el texto del comando, lo que evita que un valor extraño en el archivo se interprete como código. - Los contadores sobreviven al bucle porque la entrada viene de un archivo con
<, no de una tubería. La última línea, una comparación aritmética, deja el código de salida del script: 0 si no hubo caídos, 1 si los hubo.
12. Errores frecuentes
| Escribiste | Qué pasa | Corrección |
|---|---|---|
for l in $(cat archivo) | Recorre palabras, no líneas, y expande comodines. | while IFS= read -r l; do …; done < archivo |
while read l | Se pierden la sangría y las barras invertidas. | while IFS= read -r l |
while read -r l < archivo; do | Lee siempre la primera línea: bucle infinito. | La redirección después de done. |
| Archivo sin salto final | La última línea se pierde. | || [[ -n $l ]] en la condición. |
cmd | while …; done; echo "$total" | El total no refleja lo contado en el bucle. | done < <(cmd) |
<<(cmd) | Error de sintaxis. | < <(cmd), con espacio. |
ssh o read dentro del bucle | Consumen el resto del archivo; el bucle termina antes. | < /dev/null, ssh -n o read -u 3. |
Leer la salida de find por líneas | Falla con nombres que contienen saltos de línea. | find … -print0 y read -d ''. |
Sumar columnas con while read en archivos enormes | Muy lento. | awk. |
13. Resumen
- La forma correcta es
while IFS= read -r linea; do …; done < archivo. IFS=conserva los espacios de los extremos;-rconserva las barras invertidas; la redirección va después dedone.|| [[ -n $linea ]]procesa una última línea sin salto final.- Con varias variables e
IFS, cada línea se separa en campos; la última variable recibe lo que sobra. - Los comentarios y las líneas vacías se saltan al comienzo del cuerpo con
continue. - Una tubería ejecuta el bucle en una subshell, y las variables que modifica se pierden.
done < <(comando)lo evita. - Para nombres de archivo de
find,-print0conread -r -d ''funciona con cualquier nombre. - Los comandos del cuerpo comparten la entrada del bucle:
sshy similares la consumen. Se evita con< /dev/null, con su opción propia o leyendo por otro descriptor conread -u. while readsirve para actuar sobre cada línea; para calcular sobre muchas líneas,awkes mucho más rápido.
14. Preguntas de repaso
Intenta responder cada una antes de abrir la respuesta.
1. ¿Qué se pierde si se escribe while read linea en lugar de while IFS= read -r linea?
Los espacios y tabuladores del principio y del final de cada línea, por la falta de IFS=, y las barras invertidas, por la falta de -r.
2. ¿Por qué la redirección < archivo va después de done y no junto a read?
Porque así el archivo se abre una sola vez para todo el bucle, y cada read lee la línea siguiente. Junto a read, el archivo se abriría en cada vuelta y siempre se leería la primera línea.
3. ¿Por qué se pierde la última línea de un archivo que no termina en salto de línea, y cómo se evita?
Porque read la lee, pero termina con código 1 al no encontrar el salto, y el while termina sin procesarla. Se evita con while IFS= read -r l || [[ -n $l ]].
4. ¿Cómo se recorre /etc/passwd obteniendo el usuario y la shell de cada línea?
Con while IFS=: read -r usuario _ _ _ _ _ shell; do …; done < /etc/passwd, usando _ para los campos que no interesan.
5. ¿Por qué un contador incrementado en cmd | while read … vale 0 después del bucle?
Porque en una tubería el bucle se ejecuta en una subshell, un proceso hijo con una copia de las variables. El incremento ocurre en la copia, que desaparece al terminar la tubería.
6. ¿Qué significa done < <(comando)?
<(comando) es una sustitución de procesos, que presenta la salida del comando como si fuera un archivo; el primer < la redirige a la entrada del bucle. El bucle se ejecuta en la shell actual, y sus variables se conservan.
7. ¿Por qué se usa find -print0 con read -d ''?
Porque los nombres de archivo pueden contener saltos de línea, pero nunca el carácter nulo. Separando los nombres con nulos, el bucle funciona con cualquier nombre.
8. Un bucle que lee una lista de servidores y ejecuta ssh en cada uno procesa solo el primero. ¿Por qué?
Porque ssh lee de su entrada estándar, que es la del bucle, y consume el resto del archivo. Se corrige con ssh -n, con < /dev/null, o leyendo el archivo por otro descriptor con read -u 3.
9. ¿Cuándo conviene awk en lugar de un bucle while read?
Cuando se trata de calcular o transformar datos de muchas líneas, como sumar una columna. awk procesa el archivo en una sola pasada, mucho más rápido. while read conviene cuando hay que ejecutar acciones por cada línea.
10. En comprobar-servicios, ¿para qué sirve la cuarta variable, resto?
Para recibir cualquier campo de más. Si no está vacía, la línea tiene más campos de los esperados y se informa como mal formada.
Para practicar
- Crea un archivo con líneas que tengan sangría, barras invertidas y una última línea sin salto final. Léelo con la forma incompleta y con la completa, y compara.
- Lista los usuarios de
/etc/passwdcon UID mayor o igual que 1000, mostrando su nombre completo (el quinto campo). - Reproduce el problema del contador con una tubería, y corrígelo de dos formas: con sustitución de procesos y con
shopt -s lastpipedentro de un script. - Recorre con
find -print0los archivos de tu directorio personal modificados en el último día y muestra su tamaño. - Escribe un bucle que lea una lista de nombres y, para cada uno, pregunte al usuario si lo procesa, usando
read -u 3para el archivo. - Crea un archivo de un millón de números con
seq 1000000y súmalos con un buclewhile ready conawk. Mide el tiempo de cada uno contime. - Escribe tu propio
servicios.confcon algunos servicios locales y remotos y pruebacomprobar-servicios, incluyendo líneas mal formadas.