让我先介绍我的发现,把我的问题放在最后。(1( 仅适用于zsh
,(2(、(3( 适用于zsh
和bash
。
1. 命令替换的标准
ls | echo $(cat)
ls | { echo $(cat) }
第一个打印cat: -: Input/output error
;而第二个产生ls
的输出。
2. 管道后的链接命令
ls | { head -n1; cat}
ls | { read a; cat}
第一个命令无法正常工作。cat
遇到EOF
,直接退出。但是第二种形式有效:第一行读入a
,cat
得到其余的。
3. 混合标准丁
ls | { python -c 'import sys; print(sys.argv)' $(head -n1) }
ls | { python -c 'import sys; print(sys.argv); print(input())' $(head -n1) }
在第一行的{}
内,该命令用于打印cmdline参数;在第二种形式中,该命令还从stdin
读取一行。
第一个命令可以成功运行,而第二个表单会因为读取EOF
而抛出input()
。
我的问题是:
- (如第1节(有
{}
和没有的表格有什么区别? - (如第2节(
head
和cat
是否可以按顺序读取相同的stdin
?第二种形式如何成功,而第一种形式失败? - (如第 3 节(命令替换中命令的
stdin
如何连接到原始命令的 stdin(此处echo
(。谁先读书?以及如何使stdin
保持打开状态,以便两个命令(python
和head
(可以按顺序读取相同的stdin
?
您没有考虑输入缓冲,它解释了您的大部分观察结果。
head
每次需要数据时都会读取几千字节的输入,这使得它的效率大大提高。因此,它很可能会在任何其他进程有机会之前读取所有 stdin。这在案例 2 中很明显,其中执行顺序可能更清晰。
如果输入来自常规文件,head
可以在终止之前查找到它使用的行的末尾。但是,由于管道是不可寻的,因此它无法做到这一点。如果你使用"here-strings"——<<<
语法,那么stdin将是可搜索的,因为here-strings是使用临时文件实现的。不过,我不知道你是否可以依靠这个事实。
read
不会缓冲输入,至少不会超出当前行(即使这样,也只有在命令行上没有指定其他行结束分隔符的情况下(。它只仔细读取它需要的内容,因为它通常用于其输入来自管道并且无法进行搜索的上下文中。这是非常有用的 - 以至于它工作的事实几乎是看不见的 - 但这也是shell脚本可能非常缓慢的原因之一。
你可以通过向管道发送足够的数据来满足head
的初始读取,可以更清楚地看到这一点。试试这个,例如:
seq 1 10000 | { head -n1; head -n2; }
(我将第二个head
更改为head -n2
,因为第一个head
恰好使stdin
正好位于行尾,因此第二个head
看到一个空行作为第一行。
您需要了解的另一件事是命令替换的作用以及何时执行。命令替换读取命令的整个输出并将其插入命令行。这甚至在命令被识别之前就会发生,更不用说开始执行了。
考虑以下小片段:
$(printf %cc%co e h) hello, world
应该很清楚,命令替换是在 echo 实用程序(或内置(启动之前完全执行的。
你的第一个场景触发了一个奇怪的zsh
,Stéphane Chazelas在 Unix.SE 的这个答案中对此进行了解释。实际上,zsh
在设置管道之前进行命令替换,因此cat
从主 zsh 的标准输入中读取。(Stéphane 解释了为什么会这样以及它如何导致 EIO 错误。虽然我认为它取决于精确的 zsh 配置和选项设置,因为在我默认的 zsh 安装中,它只会锁定我的终端。在某些时候,我必须弄清楚为什么。如果使用大括号,则会在执行命令替换之前设置重定向。