调查 Bash coproc 的一些意外行为
最近,在复习 Bash 的 coproc 功能时,我偶然看到一个关于 陷阱 的参考,它描述了我认为非常意外的行为。这篇文章描述了我对这个陷阱的 快速 调查,并提出了一个解决方法(尽管我真的不建议使用它)。
我在 BashHackers wiki 上,标题为 避免最终管道子 shell 下找到了这个 陷阱 。wiki 上给出的例子是
#DOESN'T WORK
$ coproc ls
[1] 23232
$ while IFS= read -ru ${COPROC[0]} line; do printf '%s\n' "$line"; done
bash: read: line: invalid file descriptor specification
[1]+ Done coproc COPROC ls
这试图做的是运行ls在后台(作为协进程)运行,然后从协进程的标准输出中读取并打印来自ls命令的文件名输出。但是,正如您所看到的,read循环不打印任何内容,并产生一个错误,指出正在使用无效的文件描述符。wiki 上的示例还与使用ksh(Korn Shell),它产生了人们可能期望的输出
# ksh93 or mksh/pdksh derivatives
ls |& # start a coprocess
while IFS= read -rp file; do print -r -- "$file"; done # read its output
a.pdf
b.pdf
...
正如您可能已经收集到的,Bash 的行为对我来说似乎是意外的,而 Korn shell 的行为是我期望发生的。为了弄清楚这一点,我通过添加一个read在循环之前(这设法在错误消息之前为我提供了一个文件名)
$ cat coproc1.sh
coproc ls *.pdf
IFS= read -ru ${COPROC[0]} line; printf '%s\n' "$line"
while IFS= read -ru ${COPROC[0]} line
do
printf '%s\n' "$line"
done
$ bash coproc1.sh
a.pdf
coproc1.sh: line 4: read: line: invalid file descriptor specification
如果您添加更多read在循环之前的行,您通常可以在收到错误之前获得要打印的其他文件名。因此,看起来好像来自协进程的管道在脚本完成读取其输出之前被关闭了。这仍然让我感到意外,所以我查看了管道系统调用的手册页 pipe(2),然后查看了管道概述手册页 pipe(7),我在那里找到了以下段落
如果所有引用管道写入端的文件描述符都已关闭,则尝试从管道读取 (read(2)) 将看到文件结尾(read(2) 将返回 0)。如果所有引用管道读取端的文件描述符都已关闭,则写入 (write(2)) 将导致为调用进程生成 SIGPIPE 信号。如果调用进程忽略此信号,则 write(2) 将失败并显示错误 EPIPE。
这时我终于恍然大悟:这个ls命令正在写入其所有输出并退出(即关闭管道),在 Bash 脚本有时间读取管道的所有内容之前,这会导致读取在某个时候失败(取决于脚本运行的速度)。
这段话也给了我一个关于如何解决这个 问题 的想法:如果我复制由coproc命令返回的文件描述符,那么read就不会遇到上面提到的 所有引用管道写入端的文件描述符都已关闭 的情况(从而导致后续读取失败)
$ cat coproc2.sh
coproc ls *.pdf
exec 5<&${COPROC[0]} 6>&${COPROC[1]}
fd=5
IFS= read -ru $fd line; printf '%s\n' "$line"
while IFS= read -ru $fd line
do
printf '%s\n' "$line"
done
exec 5<&- 6>&-
$ bash coproc2.sh
a.pdf
b.pdf
c.pdf
d.pdf
e.pdf
f.pdf
g.pdf
现在列出了所有文件,并且没有产生错误。
记住,复制文件句柄是使用 Bash 中的 exec 重定向 完成的。第一个exec将协进程的文件描述符复制到文件描述符 5 和 6。最后一个exec关闭文件描述符 5 和 6。
提示:从 Bash 脚本中找出哪些文件是打开的
请注意,在复制文件句柄时,通常很高兴看到哪些文件在哪些文件描述符上是打开的。您可以通过在您想要查看打开的文件描述符的点将以下命令添加到您的 Bash 脚本中来轻松地做到这一点
$ ls -la /proc/$$/fd
dr-x------ 2 mitch users 0 Aug 16 13:01 .
dr-xr-xr-x 9 mitch users 0 Aug 16 13:01 ..
lr-x------ 1 mitch users 64 Aug 16 13:01 255 -> .../script.sh
lr-x------ 1 mitch users 64 Aug 16 13:01 5 -> pipe:[73893]
l-wx------ 1 mitch users 64 Aug 16 13:01 6 -> pipe:[73894]
l-wx------ 1 mitch users 64 Aug 16 13:01 60 -> pipe:[73894]
lr-x------ 1 mitch users 64 Aug 16 13:01 63 -> pipe:[73893]
我第一次尝试这种方法仅复制了管道读取端的文件描述符 (${COPROC[0]}),因为我只是从管道读取而不是写入它,这似乎就足够了,但仍然失败了。复制两个文件描述符使其能够完成而没有错误。
这里的主要目标不是为这种 Bash 行为提出一个解决方法,因为这可能不是一个真正稳健的解决方法。人们可以想象,即使这种方法在足够快的系统上运行也可能会失败,在这种系统中,协进程在exec命令有机会复制文件描述符之前完成并退出。因此,诚然,我并没有真正想出多少我每天都可以使用的东西,但我已经满足了我的好奇心,即为什么会发生这种意外行为。