Чтение строк из файла с помощью bash: for vs. while

36

Я пытаюсь прочитать текстовый файл и сделать что-то с каждой строкой, используя скрипт bash.

Итак, у меня есть список, который выглядит так:

server1
server2
server3
server4

Я думал, что смогу зациклить это, используя цикл while, вот так:

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

Цикл while останавливается после 1 запуска, поэтому выполняется только uname -aна сервере 1

Однако, с циклом for, использующим cat, он работает нормально:

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

Еще более странным для меня является то, что это также работает:

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

Почему мой первый пример останавливается после первой итерации?

Кенни Рассчарт
источник

Ответы:

25

forПетля отлично здесь. Но обратите внимание, что это связано с тем, что файл содержит имена компьютеров, которые не содержат никаких пробельных символов или символов-пробелов. for x in $(cat file); do …не работает для итерации по строкам fileв общем, потому что оболочка сначала разбивает выходные данные команды в cat fileлюбом месте, где есть пробел, а затем обрабатывает каждое слово как шаблон глобуса, поэтому \[?*дополнительно расширяется. Вы можете сделать for x in $(cat file)безопасным, если вы работаете над этим:

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

Связанное чтение: перебирать файлы с пробелами в именах? ; Как я могу читать построчно из переменной в Bash? ; Почему while IFS= readиспользуется так часто, а не IFS=; while read..? Обратите внимание, что при использовании while readбезопасный синтаксис для чтения строк while IFS= read -r line; do ….

Теперь давайте обратимся к тому, что не так с вашей while readпопыткой. Перенаправление из файла списка серверов распространяется на весь цикл. Поэтому при sshзапуске его стандартный ввод поступает из этого файла. Клиент ssh не может знать, когда удаленное приложение может захотеть прочитать данные со своего стандартного ввода. Поэтому, как только клиент ssh замечает какой-либо ввод, он отправляет этот ввод на удаленную сторону. Затем сервер ssh готов передать эти входные данные удаленной команде, если он этого захочет. В вашем случае удаленная команда никогда не читает никаких входных данных, поэтому данные в конечном итоге отбрасываются, но клиентская сторона ничего об этом не знает. Ваша попытка echoсработала, потому что echoникогда не читает какой-либо ввод, он оставляет свой стандартный ввод в покое.

Есть несколько способов избежать этого. Вы можете указать ssh не читать со стандартного ввода с помощью -nопции.

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

-nВариант на самом деле говорит sshперенаправить ввод из /dev/null. Вы можете сделать это на уровне оболочки, и это будет работать для любой команды.

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

Способ заманчиво входа во избежание SSH пришествия из файла поставить переадресацию на readкоманде: while read server </home/kenny/list_of_servers.txt; do …. Это не будет работать, потому что при каждом выполнении readкоманды файл будет открываться снова (поэтому он будет читать первую строку файла снова и снова). Перенаправление должно быть во всем цикле while, чтобы файл открывался один раз на протяжении цикла.

Общее решение состоит в том, чтобы обеспечить ввод для цикла в дескрипторе файла, отличном от стандартного ввода. Оболочка имеет конструкции для передачи ввода и вывода из одного номера дескриптора в другой. Здесь мы открываем файл в файловом дескрипторе 3 и перенаправляем readстандартный ввод команды из файлового дескриптора 3. Клиент ssh игнорирует открытые нестандартные дескрипторы, поэтому все в порядке.

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

В bash readкоманда имеет специальную опцию для чтения из другого файлового дескриптора, поэтому вы можете писать read -u3 server.

Связанное чтение: файловые дескрипторы и сценарии оболочки ; Когда бы вы использовали дополнительный файловый дескриптор?

Жиль "ТАК - прекрати быть злым"
источник
5
Чувак, этот обмен стека наверняка отличается от ошибки сервера. Люди здесь действительно стараются не только отвечать, но и обучать. Большое спасибо.
Кенни Рассчарт
26

Вы должны использовать while, а неfor . Чтобы избежать команд, поглощающих стандартный ввод в таком цикле, просто используйте другой файловый дескриптор :

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

Для получения дополнительной информации, help [r]ead(на самом деле ) и еще одна статья, объясняющая почему .

l0b0
источник
1
Интересно, я никогда не использовал файловые дескрипторы раньше. Что делает -uфлаг? Я не уверен, на какой справочной странице я должен это посмотреть.
Кенни Рассчарт
Команда read является частью оболочки bash, поэтому она находится на странице руководства bash в разделе «Команды сборки оболочки» в абзаце чтения. Вы найдете "-u fd Чтение ввода из файлового дескриптора fd." в этом пункте
f4m8
6
Альтернативно help read- helpэто команда для получения эквивалента manстраниц для встроенных команд оболочки.
10
22

В вашем первом коде sshбудет «украсть» STDIN из while. Добавьте -nопцию, sshчтобы избежать этого. От man ssh:

-n     Redirects stdin from /dev/null (actually, prevents reading from stdin).
manatwork
источник
Хорошо, есть идеи, почему этого не происходит с циклом for?
Кенни Рассчарт
7
Потому что forцикл получает данные как параметр, а не как ввод.
manatwork
1
Вы, сэр, джентльмен и ученый.
Кенни Рассчарт
0

Стандартная стандартная обработка ввода sshвыводит оставшуюся строку из цикла while.

Чтобы избежать этой проблемы, измените, откуда проблемная команда читает стандартный ввод. Если в команду не нужно передавать стандартный ввод, считайте стандартный ввод со специального /dev/nullустройства.

nixguru
источник