【问题标题】:Why does the Bash read command return without any input?为什么 Bash 读取命令没有任何输入就返回?
【发布时间】:2016-11-23 20:33:26
【问题描述】:

我有一个 Bash 脚本 foo,它接受 STDIN 上的单词列表。脚本应该将单词读入一个数组,然后请求用户输入:

while IFS= read -r; do
  words+=( "$REPLY" )
done

read -r ans -p "Do stuff? [Yn] "
echo "ans: |$ans|"

问题是,Bash 立即将一个空字符串读入ans 变量,而无需等待实际的用户输入。也就是说,我得到以下输出(没有提示或延迟):

$ cat words.txt | foo
ans: ||

既然第一个read 调用已经消耗了通过管道传输到STDIN 的所有内容,为什么第二个read 调用返回而没有实际读取任何内容?

【问题讨论】:

  • 不读取任何内容就返回是您的while 循环首先终止的方式:read 在没有(完整)行可读取时具有非零退出状态。
  • 由于第一个 read 调用已经消耗了通过管道传输到 STDIN 的所有内容,因此第二个 read 调用没有任何内容可供读取!
  • @WilliamPursell - 我的意思是,这种情况和对终端的read 调用(没有输入重定向)有什么区别?在终端read 呼叫中,也没有什么可阅读的,直到我输入一些内容并按回车键。那么为什么read 等待我对该电话的输入而不是在这里?
  • @mklement0 感谢您指出这一点!是的,^d 不会关闭标准输入。 ^d 导致 tty 刷新其输入缓冲区,这(如果为空)导致 read(系统调用)返回 0,这反过来又使 read(bash 内置)返回 0。
  • @WilliamPursell:感谢您的反馈;我已经添加了一个更详细的评论版本作为我的答案的脚注(顺便说一句:你的意思是“让read(内置bash)返回[设置退出代码]1” ,而不是 0)。

标签: bash unix stdin


【解决方案1】:

根据您的症状判断,您似乎重定向了标准输入以通过输入文件 (@987654324) 将单词列表提供给 while 循环@) 或通过管道 (... | foo)。

如果是这样,您的第二个read 命令不会自动切换回从终端读取;它仍在读取重定向到的任何标准输入,并且如果该输入已被消耗(这正是您的 while 循环所做的,正如 chepner 在评论中指出的那样),read 什么也不读取,并返回退出代码 1(这是终止 while 循环开始的原因)。

如果您明确希望第二个read 命令终端获取用户输入,请使用:

read -r -p "Do stuff? [Yn] " ans </dev/tty

注意:

  • 从(有限)文件(或具有有限输出的管道或进程替换)重定向的标准输入是有限资源,一旦所有输入都用完,最终会报告 EOF 条件

    • read 将 EOF 条件转换为退出代码 1,导致 while 循环退出:

      • 具体来说,如果read 无法读取任何 个字符,它会将空字符串(空字符串)分配给指定的变量(或$REPLY如果没有指定),并将退出代码设置为1
        注意:read 可以设置退出代码1,即使它确实读取字符(并将它们存储在指定的变量/$REPLY),即如果输入结束没有分隔符;分隔符默认为\n,否则使用-d显式指定分隔符。
    • 一旦消耗了所有输入,后续read 命令无法再读取任何内容(EOF 条件持续存在,行为如上所述) .

  • 相比之下,来自终端交互式标准输入输入可能是无限的 : 每当请求标准输入时,用户以交互方式输入的任何内容都会提供额外的数据。

    • 交互式多行输入期间模拟EOF条件的方法(即终止输入 loop) 是按 ^D (Control-D):

      • ^D一行的开头一次时,read 不读取任何内容就返回并设置退出代码到1,就像遇到EOF一样。

        • 换句话说:在循环中终止无限交互输入的方法是在提交之后按^D 最后一行输入。
      • 相比之下,在输入行的内部,需要按^D 两次来停止读取并设置退出代码到1,但请注意,到目前为止键入的行已保存到目标变量/$REPLY[1]

    • 由于 stdin 输入流实际上并未关闭后续的read 命令正常工作并继续请求交互用户输入。

    • 警告:如果您在 shell 的提示符 下按 ^D(而不是在 运行时按程序 正在请求输入),您将终止 shell 本身。


附:

问题中有一个偶然错误

  • 第二个read 命令必须将操作数ans(存储输入的变量的名称)放在所有选项之后,以便在语法上工作:read -r -p "Do stuff? [Yn] " ans

[1] 正如William Pursell 在对该问题的评论中指出的那样:^D 导致read(2) 系统调用返回缓冲区中的任何内容;返回的直接值是读取的字符数
0 的计数是 EOF 条件的信号方式,Bash 的 read 将其转换为退出代码 1,从而导致循环终止。
因此,当输入缓冲区为空时,在行首按 ^D 会立即退出循环。
相反,如果已经在该行上输入了字符,那么 first ^D 会导致 read(2) 返回迄今为止输入的许多字符,而 Bash 的 @ 987654358@ reinvokes read(2),因为尚未遇到分隔符(默认为换行符)。
紧随第二个 ^D 然后导致read(2) 返回0,因为没有输入任何字符,导致Bash 的read 设置退出代码1 和退出循环。

【讨论】:

  • 很好。我想知道read 的返回值将是while 终止。我已经得到了答案。 :) 这种循环的逻辑似乎很好,它们在可读性方面真的很糟糕。幕后发生的事情尚不清楚。就像 Chet Ramey 所说,Bourne shell 中存在黑暗的角落,人们都会使用它们。
  • @sjsam:这些黑暗角落之一是 read 返回退出代码 1 用于未以换行符 (\n) 终止的行,尽管读取了值。因此,while 循环不会捕获文件的最后一行,如果它不是换行符终止的。解决方法:while read -r line || [[ -n $line ]]; do ...
  • 谢谢@mklement0!请注意,我在原始问题的读取命令中删除了$word 错字,因为它分散了真正问题的注意力。供参考,原代码为:while IFS= read -r word; do words+=( "$REPLY" ); done;正如您所指出的,在这种情况下,该值将存储在$word,而不是$REPLY
  • 澄清一下,当您说“read 什么都没读”时,这是否意味着它看到的是 EOF 字符?否则,它怎么知道没有更多的输入?
  • @mklement0:确实。你是我翅膀下的风。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多