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 que se parece com isso:

server1
server2
server3
server4

Eu pensei que poderia fazer um loop sobre isso usando um loop while, assim:

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

O loop while para após uma execução, sendo executada apenas uname -ano 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 é interrompido após a primeira iteração?

Kenny Rasschaert
fonte

Respostas:

25

O forloop está bom aqui. Mas observe que isso ocorre porque o arquivo contém nomes de máquinas, que não contêm caracteres de espaço em branco ou globbing. for x in $(cat file); do …não funciona para iterar sobre as linhas de fileem geral, porque o shell primeiro divide a saída do comando em cat filequalquer lugar em que haja espaço em branco e, em seguida, trata cada palavra como um padrão glob, para que \[?*sejam expandidas ainda mais. Você pode se for x in $(cat file)proteger se trabalhar nele:

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

Leitura relacionada: Fazendo loop em arquivos com espaços nos nomes? ; Como posso ler linha por linha de uma variável no bash? ; Por que é while IFS= readusado 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 ao que deu errado com sua while readtentativa. O redirecionamento do arquivo da lista de servidores se aplica a todo o loop. Portanto, quando sshexecutado, sua entrada padrão vem desse arquivo. O cliente ssh não pode saber quando o aplicativo remoto pode querer ler de sua entrada padrão. Portanto, assim que o cliente ssh percebe alguma entrada, envia essa entrada para o lado remoto. O servidor ssh então está pronto para alimentar essa entrada para o comando remoto, caso deseje. No seu caso, o comando remoto nunca lê nenhuma entrada; portanto, os dados acabam sendo descartados, mas o lado do cliente não sabe nada sobre isso. Sua tentativa com echofuncionou porque echonunca lê nenhuma entrada, deixa sua entrada padrão em paz.

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

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

A -nopção de fato diz sshpara redirecionar 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 entrada evitar de ssh que vem do arquivo é colocar o redirecionamento no readcomando: while read server </home/kenny/list_of_servers.txt; do …. Isso não funcionará, pois faz com que o arquivo seja aberto novamente toda vez que o readcomando for executado (para ler a primeira linha do arquivo repetidamente). O redirecionamento precisa estar no loop while geral, para que o arquivo seja aberto uma vez durante a duração do loop.

A solução geral é fornecer a entrada para o loop em um descritor de arquivo diferente da entrada padrão. O shell possui 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 readentrada padrão do comando do descritor de arquivo 3. O cliente ssh ignora os descritores não padronizados abertos, para que tudo esteja bem.

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

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

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

Gilles 'SO- parar de ser mau'
fonte
5
Cara, essa troca de pilha com certeza é diferente da falha do servidor. As pessoas aqui realmente se esforçam para não apenas responder, mas também educar. Muito obrigado.
Kenny Rasschaert
26

Você deveria usar while, nãofor . A maneira de evitar que os comandos engolam a entrada padrão nesse 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 mais informações, help [r]ead( realmente ) e outro artigo explicando o porquê .

l0b0
fonte
1
Interessante, nunca usei descritores de arquivos antes. O que a -ubandeira faz? Não sei em que página de manual devo procurar.
precisa saber é o seguinte
O comando read faz parte do shell bash, portanto está na página de manual do bash na seção "SHELL BUILTIN COMMANDS" no parágrafo de leitura. Você encontrará "-u fd Leia a entrada do descritor de arquivo fd". neste parágrafo
f4m8 9/11/11
6
Alternativamente help read- helpé o comando para obter o equivalente de manpáginas para os componentes internos do shell.
l0b0
22

No seu primeiro código ssh, "roubará" o STDIN do while. Adicione a -nopção para sshevitá-lo. De man ssh:

-n     Redirects stdin from /dev/null (actually, prevents reading from stdin).
homem a trabalhar
fonte
Ok, tem alguma idéia de por que isso não acontece com o loop for?
precisa saber é o seguinte
7
Porque o forloop recebe os dados como parâmetro, não como entrada.
9118 manatwork
1
O senhor é um cavalheiro e um estudioso.
precisa saber é o seguinte
0

A manipulação de entrada padrão padrão sshdrena a linha restante do loop while.

Para evitar esse problema, altere de 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 /dev/nulldispositivo especial .

nixguru
fonte