为什么Bash读命令在没有任何输入的情况下返回?

Sas*_*lla 2 unix bash stdin

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

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

read -r ans -p "Do stuff? [Yn] "
echo "ans: |$ans|"
Run Code Online (Sandbox Code Playgroud)

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

$ cat words.txt | foo
ans: ||
Run Code Online (Sandbox Code Playgroud)

由于第一次read调用已经消耗了所有传输到STDIN的东西,为什么第二次read调用返回而没有实际读取任何内容?

mkl*_*nt0 6

从您的症状来看,看起来您已经重定向stdin,while通过输入文件(foo < file)或管道(... | foo)向循环提供单词列表.

如果是这样,您的第二个read命令将不会自动切换回终端读取 ; 它仍在读取stdin被重定向到的任何内容,并且如果已经消耗了该输入(这正是你的while循环所做的,正如chepner在注释中指出的那样),read什么都不读,并返回退出代码1(这就是终止了while循环开始).

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

read -r -p "Do stuff? [Yn] " ans </dev/tty
Run Code Online (Sandbox Code Playgroud)

注意:

  • 从(有限)文件(或有限输出的管道或进程替换)重定向的Stdin是一种有限资源,一旦消耗了所有输入,它最终会报告EOF条件:

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

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

  • 相比之下,来自终端的交互式 stdin输入可能无限的:无论何时用户在请求stdin输入时交互式地输入附加数据.

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

      • ^D按下一次一行的最开始,read返回没有阅读任何东西,并设置退出代码1,就好像EOF遇到了一些.

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

    • 由于stdin输入流实际上没有关闭,后续read命令正常工作并继续请求交互式用户输入.

    • 警告:如果按^D外壳的提示(相对于同时运行的程序请求输入),你将终止Shell本身.


PS:

问题中有一个偶然的错误:

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

[1]正如William Pursell在对该问题的评论中指出的那样:^D导致read(2)系统调用以此时缓冲区中的任何内容返回; 返回的直接值是读取的字符数.
计数0是EOF条件的信号,Bash read将其转换为退出代码1,从而导致循环终止.
因此,^D当输入缓冲区为空时,在行的开头按下,立即退出循环.
相比之下,如果已经在行上键入了字符,那么第一个 返回的^D原因read(2)是到目前为止输入了很多字符,Bash read 重新调用 read(2),因为尚未遇到分隔符(默认为换行符).
紧随其后的第二 ^D进而导致read(2)返回0,因为没有字符输入一样,导致bash的read设置退出代码1和退出循环.