Bash while read循环提前中断

32

我已经被这个问题困扰了一段时间。

我想要SSH连接到一组机器并检查它们是否可用(接受连接且未被使用)。我编写了一个小脚本“tssh”,可以实现这一点:

#!/bin/bash

host=$1
timeout=${2:-1}

ssh -qo "ConnectTimeout $timeout" $host "[ \`who | cut -f1 | wc -l \` -eq 0 ] && exit 0 || exit 1"

这个脚本正常工作。如果出现连接问题,返回255;如果机器忙碌,则返回1;如果一切正常,则返回0。如果有更好的方法,请告诉我。

接下来,我尝试使用while read循环调用我的一组机器上的tssh,这就是问题所在。只要tssh返回0,循环就会退出,无法完成整个集合。

while read nu ; do tssh "MYBOXES$nu" ; done < <(ruby -e '(0..20).each { |i| puts i }')

一开始我以为这是个子shell的问题,但显然不是。
非常感谢任何帮助和对样式/内容的评论!
我知道当我找到原因时会很后悔...


我在使用Perl的Net::SSH::Perl时遇到了类似的问题,@Foo-Bah已经很好地描述了这个问题,解决方法是将一个空字符串作为参数添加到cmd中: https://metacpan.org/pod/Net::SSH::Perl#out-err-exit-ssh-cmd-cmd-stdin my($stdout, $stderr, $exit) = $ssh->cmd($cmd, ""); - Thomas B in BDX
9个回答

49

Chris 是正确的。循环中断的原因是 SSH 使用标准输入,但 Guns 在使用循环方法方面是正确的。

如果您正在循环输入(例如一个包含主机名列表的文件)并调用 SSH,则需要传递 -n 参数,否则基于输入的循环将失败。

while read host; do
  ssh -n $host "remote command" >> output.txt
done << host_list_file.txt

37
在这个结构中
something | 
while read x; do 
    ssh ...
done

while循环看到的标准输入是something的输出。

ssh的默认行为是读取标准输入。这使您可以执行以下操作:

cat id_rsa.pub | ssh new_box "cat - >> ~/.ssh/authorized_keys"

话虽如此,当读取第一个值时,第一个ssh命令将从something读取整个输入。然后,在ssh完成时,没有剩余输出,read停止。

解决方法是使用ssh -n ...,例如:

cat /etc/hosts | awk '{print $2}' | while read x; do
    ssh -n $x "do_something_on_the_machine"
done

使用 while read x; do ...; done < <(awk '{print $2}' </etc/hosts) 会更加可靠,避免了 BashFAQ #24 中描述的问题。 - Charles Duffy

13

大多数答案都针对ssh。其他命令也会劫持标准输入,但没有-n选项。这应该适用于任何其他命令。这也适用于ssh。

while read x; do 
    # Make sure command does not hijack stdin
    echo "" | command $x
done < /path/to/some/file

2
command "$x" </dev/null 相比,这相当低效。echo "" | 需要进行 fork()mkfifo() 等操作,并将 command 移出进程,因此即使它是一个 shell 函数,也无法设置在循环外持续存在的变量。 - Charles Duffy
1
这解决了我的问题。在我的情况下,使用 echo "" | 由于某种原因使我无法将命令的结果打印到标准输出。使用 @CharlesDuffy 的 </dev/null 就可以了。 - Andrew Cheong
在运行一个吃输入的命令时,也使用了 sudo。谢谢! - Deanna

8
我今天遇到了这个问题 -- rsh和/或ssh可能会破坏while read循环,因为它使用stdin。我在ssh行中加入了-n,这样它就不会尝试使用stdin,问题得到了解决。

4
不知道这是否有帮助,但更规范的写法是:
for nu in `ruby -e '(0..20).each { |i| puts i}'`; do
  tssh "MYBOXES$nu" 
done

3
如果您拥有GNU Coreutils,您可以使用seq 0 20命令来代替ruby命令。 - Jouni K. Seppänen
@Paul谢谢,这个方法有效!现在只要我知道为什么就好了! 也许这真的是一个子shell问题。这确实让我想起了以前遇到的子shell问题。@Jouni我用seq替换了丑陋的ruby部分。之前不知道有这个命令,谢谢。@All有人能解释一下为什么这个修复方法有效吗? - wopwop
1
@Paul 这是一个ssh问题。我会发帖解决。 - Foo Bah
2
我希望给这篇文章点踩的人能解释一下原因。 - Paul Tomblin
while..done 循环在其中运行 ssh 时也存在问题。使用 for 循环通常可以解决这个问题。 - Peter

3

正如Kaii所说,仅为输出一系列数字而调用Ruby或seq(在BSD或OSX机器上不起作用)实在是过头了。如果您愿意使用bash,则可以执行以下操作:

for i in {0..20}; do
    # command
done

我相信这应该适用于bash 2.05b及以上版本。


3

我不确定为什么它失败了,但我喜欢使用xargsseq

seq 0 20 | xargs -n1 tssh MYBOXES

2

谈论一种“拆东墙补西墙”的问题。我花了几个小时努力想弄清楚为什么我的ssh会杀死我的something|while read循环。

另一种同时保持while read循环和ssh的方法是使用“-n”开关将ssh的STDIN设置为/dev/null。对我来说非常有效:

#!/bin/bash
[...]
something|while read host
   do
      ssh -nx ${host} fiddleAround
   done

我倾向于使用“-x”选项来避免在隧道中浪费时间协商X。

2

我无法相信是0的结果打破了你的循环,你可以通过将循环中的tssh命令替换为“/bin/true”(也返回0)来测试。

关于样式,我不明白为什么一个简单的循环shell脚本需要ruby、perl、seq或jot等其他二进制文件,这些文件在我的*BSD上都没有。

你可以选择使用shell内置的循环结构,它至少在ksh和bash中有效:

for ((i=0; $i<=20; i++)); do
    tssh "MYBOXES$i"
done

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接