Lendo linhas de um arquivo com bash: for vs. while

36

Estou tentando ler um arquivo de texto e fazer algo com cada linha, usando um script bash.

Então, eu tenho uma lista assim:

server1
server2
server3
server4

Eu pensei em fazer um loop por meio de um loop while:

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

O loop while pára após 1 run, portanto, apenas executando uname -a no server1

No entanto, com um loop for usando cat, ele funciona bem:

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

Ainda mais desconcertante para mim é que isso também funciona:

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

Por que meu primeiro exemplo para depois da primeira iteração?

    
por Kenny Rasschaert 09.11.2011 / 14:08

4 respostas

25

O loop for está bem aqui. Mas note que isso ocorre porque o arquivo contém nomes de máquina, que não contêm caracteres de espaço em branco ou caracteres globbing. for x in $(cat file); do … não funciona para iterar as linhas de file em geral, porque o shell primeiro divide a saída do comando cat file em qualquer lugar onde houver espaço em branco e, em seguida, trata cada palavra como um padrão de glob assim \[?* expandido ainda mais. Você pode tornar for x in $(cat file) seguro se você trabalhar nele:

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

Leitura relacionada: Percorrendo arquivos com espaços nos nomes ? ; Como posso ler a linha por linha de uma variável no bash? ; Por que é while IFS= read usado com tanta frequência, em vez de IFS=; while read.. ? Observe que ao usar while read , a sintaxe segura para ler linhas é while IFS= read -r line; do … .

Agora vamos ver o que há de errado com sua tentativa while read . O redirecionamento do arquivo de lista de servidores se aplica ao loop inteiro. Então, quando ssh é executado, sua entrada padrão vem desse arquivo. O cliente ssh não pode saber quando o aplicativo remoto pode querer ler sua entrada padrão. Então, assim que o cliente ssh percebe alguma entrada, ele envia essa entrada para o lado remoto. O servidor ssh está pronto para alimentar essa entrada para o comando remoto, caso ele queira. No seu caso, o comando remoto nunca lê nenhuma entrada, então os dados acabam sendo descartados, mas o lado do cliente não sabe nada sobre isso. Sua tentativa com echo funcionou porque echo nunca lê nenhuma entrada, ela deixa sua entrada padrão sozinha.

Existem algumas maneiras de evitar isso. Você pode dizer ao ssh para não ler a entrada padrão, com a opção -n .

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

A opção -n diz, de fato, que ssh redireciona sua entrada de /dev/null . Você pode fazer isso no nível do shell e funcionará para qualquer comando.

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

Um método tentador para evitar a entrada de ssh proveniente do arquivo é colocar o redirecionamento no comando read : while read server </home/kenny/list_of_servers.txt; do … . Isso não funcionará, porque faz com que o arquivo seja aberto novamente toda vez que o comando read for executado (para que ele leia a primeira linha do arquivo várias vezes). O redirecionamento precisa estar em todo o loop while para que o arquivo seja aberto uma vez pela duração do loop.

A solução geral é fornecer a entrada para o loop em um descritor de arquivo além da entrada padrão. O shell tem construções para transportar entrada e saída de um número de descritor para outro. Aqui, abrimos o arquivo no descritor de arquivo 3 e redirecionamos a entrada padrão do comando read do descritor de arquivo 3. O cliente ssh ignora os descritores não-padrão abertos, portanto, tudo está bem.

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

No bash, o comando read tem uma opção específica para ler um descritor de arquivo diferente, portanto você pode escrever read -u3 server .

Leitura relacionada: Descritores de arquivos & script de shell ; Quando você usaria um descritor de arquivo adicional?

    
por 10.11.2011 / 01:20
26

Você deve usar while , não for . A maneira de evitar comandos engolindo a entrada padrão em tal loop é simplesmente usar outro descritor de arquivo :

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

Para obter mais informações, help [r]ead ( realmente ) e outro artigo explicando por que .

    
por 09.11.2011 / 14:41
22

No seu primeiro código ssh irá "roubar" o STDIN do while . Adicione a opção -n a ssh para evitar isso. De man ssh :

-n     Redirects stdin from /dev/null (actually, prevents reading from stdin).
    
por 09.11.2011 / 14:18
0

O tratamento de entrada padrão padrão de ssh drena a linha restante do loop while.

Para evitar esse problema, altere onde o comando problemático lê a entrada padrão. Se nenhuma entrada padrão precisar ser passada para o comando, leia a entrada padrão do dispositivo especial /dev/null .

    
por 20.05.2015 / 14:10