而循环在 Bash 中的第一行之后停止读取

我有以下 shell 脚本。其目的是循环遍历目标文件的每一行(其路径是脚本的输入参数)并对每一行执行操作。现在,它似乎只能处理目标文件中的第一行,并在该行被处理后停止。我的剧本有什么问题吗?

#!/bin/bash
# SCRIPT: do.sh
# PURPOSE: loop thru the targets


FILENAME=$1
count=0


echo "proceed with $FILENAME"


while read LINE; do
let count++
echo "$count $LINE"
sh ./do_work.sh $LINE
done < $FILENAME


echo "\ntotal $count targets"

do_work.sh中,我运行两个 ssh命令。

82688 次浏览

问题是 do_work.sh运行 ssh命令,默认情况下 ssh从 stdin 读取,stdin 是您的输入文件。因此,您只能看到处理的第一行,因为命令将使用文件的其余部分,而 while 循环将终止。

这不仅发生在 ssh中,而且发生在读取 stdin 的任何命令中,包括 mplayerffmpegHandBrakeCLIhttpiebrew install等。

为了防止这种情况,请将 -n选项传递给 ssh命令,使其从 /dev/null而不是 stdin 读取。其他命令也有类似的标志,或者您可以普遍使用 < /dev/null

Ssh-n 选项可以防止在使用 HEREdoc 将输出通过管道传输到另一个程序时检查 ssh 的退出状态。 因此,最好使用/dev/null 作为 stdin。

#!/bin/bash
while read ONELINE ; do
ssh ubuntu@host_xyz </dev/null <<EOF 2>&1 | filter_pgm
echo "Hi, $ONELINE. You come here often?"
process_response_pgm
EOF
if [ ${PIPESTATUS[0]} -ne 0 ] ; then
echo "aborting loop"
exit ${PIPESTATUS[0]}
fi
done << input_list.txt

这种情况发生在我身上是因为我有 set -e,而且循环中的 grep返回时没有输出(这给出了一个非零错误代码)。

更一般地说,不是特定于 ssh的解决方案是重定向任何命令的标准输入,否则这些命令可能会占用 while循环的输入。

while read -r LINE; do
let count++
echo "$count $LINE"
sh ./do_work.sh "$LINE" </dev/null
done < "$FILENAME"

</dev/null的添加是这里的关键点(尽管更正的引用也有些重要; 也参见 什么时候给 shell 变量加引号?)。您将希望使用 read -r,除非您特别需要在没有 -r的情况下获得稍微有点奇怪的遗留行为。

另一种对 ssh比较特殊的变通方法是确保任何 ssh命令的标准输入被绑定,例如,通过更改

ssh otherhost some commands here

而是从 here 文档中读取命令,这样可以方便地(对于这个特定场景)将 ssh的标准输入绑定在命令上:

ssh otherhost <<'____HERE'
some commands here
____HERE

一个非常简单和健壮的解决方案是更改 文件描述符文件描述符read命令从该 文件描述符文件描述符接收输入。

这是通过两个修改实现的: -u参数到 read,以及 < $FILENAME的重定向操作符。

在 BASH 中,默认的文件描述符值(即 read-u的值)是:

  • 0 = stdin
  • 1 = stdout
  • 2 = stderr

因此,只要选择一些其他未使用的文件描述符,如 9只是为了好玩。

因此,以下是解决办法:

while read -u 9 LINE; do
let count++
echo "$count $LINE"
sh ./do_work.sh $LINE
done 9< $FILENAME

注意这两个修改:

  1. read变成 read -u 9
  2. < $FILENAME变成 9< $FILENAME

作为一个最佳实践,我对用 BASH 编写的所有 while循环都这样做。 如果您有使用 read的嵌套循环,那么对每个循环使用不同的 文件描述符文件描述符(9、8、7、 ...)。