Lectura de líneas de un archivo con bash: para vs mientras

36

Estoy tratando de leer un archivo de texto y hacer algo con cada línea, usando un script bash.

Entonces, tengo una lista que se ve así:

server1
server2
server3
server4

Pensé que podría recorrer esto usando un ciclo while, así:

while read server; do
  ssh $server "uname -a"
done < /home/kenny/list_of_servers.txt

El ciclo while se detiene después de 1 ejecución, por lo que solo se ejecuta uname -aen el servidor1

Sin embargo, con un bucle for usando cat funciona bien:

for server in $(cat /home/kenny/list_of_servers.txt) ; do
  ssh $server "uname -a"
done

Aún más desconcertante para mí es que esto también funciona:

while read server; do
  echo $server
done < /home/kenny/list_of_servers.txt

¿Por qué mi primer ejemplo se detiene después de la primera iteración?

Kenny Rasschaert
fuente

Respuestas:

25

El forbucle está bien aquí. Pero tenga en cuenta que esto se debe a que el archivo contiene nombres de máquinas, que no contienen caracteres de espacio en blanco o caracteres globales. for x in $(cat file); do …no funciona para iterar sobre las líneas fileen general, porque el shell primero divide la salida del comando en cat filecualquier lugar donde haya espacios en blanco, y luego trata cada palabra como un patrón glob para \[?*expandirse aún más. Puedes asegurarte for x in $(cat file)si trabajas en ello:

set -f
IFS='
'
for x in $(cat file); do 

Lectura relacionada: ¿ Recorrer archivos con espacios en los nombres? ; ¿Cómo puedo leer línea por línea de una variable en bash? ; ¿Por qué se while IFS= readusa con tanta frecuencia, en lugar de IFS=; while read..? Tenga en cuenta que cuando se usa while read, la sintaxis segura para leer líneas es while IFS= read -r line; do ….

Ahora pasemos a lo que sale mal con su while readintento. La redirección del archivo de lista del servidor se aplica a todo el bucle. Entonces, cuando se sshejecuta, su entrada estándar proviene de ese archivo. El cliente ssh no puede saber cuándo la aplicación remota puede querer leer desde su entrada estándar. Entonces, tan pronto como el cliente ssh nota alguna entrada, envía esa entrada al lado remoto. El servidor ssh allí está listo para alimentar esa entrada al comando remoto, si lo desea. En su caso, el comando remoto nunca lee ninguna entrada, por lo que los datos terminan descartados, pero el lado del cliente no sabe nada al respecto. Su intento con echofuncionó porque echonunca lee ninguna entrada, deja solo su entrada estándar.

Hay algunas maneras de evitar esto. Puede decirle a ssh que no lea desde la entrada estándar, con la -nopción.

while read server; do
  ssh -n $server "uname -a"
done < /home/kenny/list_of_servers.txt

De -nhecho, la opción le dice sshque redirija su entrada desde /dev/null. Puede hacerlo a nivel de shell y funcionará para cualquier comando.

while read server; do
  ssh $server "uname -a" </dev/null
done < /home/kenny/list_of_servers.txt

Un método tentador para evitar la entrada de ssh procedente del archivo es poner la redirección en el readcomando: while read server </home/kenny/list_of_servers.txt; do …. Esto no funcionará, porque hace que el archivo se abra nuevamente cada vez que readse ejecuta el comando (por lo que leería la primera línea del archivo una y otra vez). La redirección debe estar en todo el ciclo while para que el archivo se abra una vez mientras dure el ciclo.

La solución general es proporcionar la entrada al bucle en un descriptor de archivo que no sea la entrada estándar. El shell tiene construcciones para transportar la entrada y la salida de un número de descriptor a otro. Aquí, abrimos el archivo en el descriptor de archivo 3 y redirigimos la readentrada estándar del comando desde el descriptor de archivo 3. El cliente ssh ignora los descriptores abiertos no estándar, por lo que todo está bien.

while read server <&3; do
  ssh $server "uname -a"
done 3</home/kenny/list_of_servers.txt

En bash, el readcomando tiene una opción específica para leer desde un descriptor de archivo diferente, para que pueda escribir read -u3 server.

Lectura relacionada: descriptores de archivo y scripting de shell ; ¿Cuándo utilizarías un descriptor de archivo adicional?

Gilles 'SO- deja de ser malvado'
fuente
55
Hombre, este stackexchange seguro es diferente de serverfault. La gente aquí realmente se esfuerza no solo para responder, sino también para educar. Muchas gracias.
Kenny Rasschaert
26

Deberías usar while, nofor . La forma de evitar que los comandos se traguen la entrada estándar en dicho bucle es simplemente usar otro descriptor de archivo :

while read -u 9 server; do
  ssh $server "uname -a"
done 9< /home/kenny/list_of_servers.txt

Para obtener más información, help [r]ead( realmente ) y otro artículo que explica por qué .

l0b0
fuente
1
Interesante, nunca he usado descriptores de archivos antes. ¿Qué hace la -ubandera? No estoy seguro de en qué página del manual debo buscarlo.
Kenny Rasschaert
El comando de lectura es parte del shell bash, por lo que se encuentra en la página de manual de bash en la sección "COMANDOS SHELL BUILTIN" en el párrafo leído. Encontrará "-u fd Leer entrada del descriptor de archivo fd". en este párrafo
f4m8
66
Alternativamente help read, helpes el comando para obtener el equivalente de manpáginas para shell construidos.
l0b0
22

En su primer código ssh"robará" el STDIN del while. Añadir -nopción a sshpara evitarlo. De man ssh:

-n     Redirects stdin from /dev/null (actually, prevents reading from stdin).
hombre trabajando
fuente
Ok, ¿tienes idea de por qué esto no sucede con el ciclo for?
Kenny Rasschaert el
77
Porque el forbucle recibe los datos como parámetro, no como entrada.
manatwork el
1
Usted, señor, es un caballero y un erudito.
Kenny Rasschaert
0

El manejo de entrada estándar predeterminado de sshdrena la línea restante del bucle while.

Para evitar este problema, modifique de dónde lee el comando problemático la entrada estándar. Si no se necesita pasar una entrada estándar al comando, lea la entrada estándar del /dev/nulldispositivo especial .

nixguru
fuente