在Bash中如何比较两个流水线?

160

如何在Bash中不使用临时文件来diff两个管道?假设您有两个命令管道:

foo | bar
baz | quux

如果你想找到它们输出中的diff,一个解决方案显然是:

foo | bar > /tmp/a
baz | quux > /tmp/b
diff /tmp/a /tmp/b

在Bash中是否可以不使用临时文件来完成此操作?您可以通过将其中一个管道输入到diff中来消除一个临时文件:

foo | bar > /tmp/a
baz | quux | diff /tmp/a -

但是你不能同时将两个管道都输入到 diff 中(至少没有明显的方式)。有没有一些巧妙的技巧,使用 /dev/fd 在不使用临时文件的情况下完成这个操作?

3个回答

173
一行代码有两个临时文件(不是你想要的),如下所示:
 foo | bar > file1.txt && baz | quux > file2.txt && diff file1.txt file2.txt

使用 bash 的话,你可以尝试以下操作:

 diff <(foo | bar) <(baz | quux)

 foo | bar | diff - <(baz | quux)  # or only use process substitution once

第二个版本将更清晰地提醒您哪个输入是哪个,通过显示 "-- /dev/stdin" vs. "++ /dev/fd/63" 或其他类似的内容,而不是两个编号的 fd。
即使是命名管道在文件系统中也不会出现,至少在 bash 可以使用类似于 "/dev/fd/63" 这样的文件名通过进程替换来实现的操作系统上不会出现。这样可以获得一个文件名,命令可以打开并从中读取,以实际从 bash 执行命令之前设置好的已打开文件描述符进行读取。(即 bash 在 fork 之前使用 pipe(2),然后使用 dup2 将输出从 quux 重定向到 diff 的输入文件描述符上,fd 为 63。)
在没有 "神奇" 的 / dev / fd 或 / proc / self / fd 的系统上,bash 可能会使用命名管道来实现进程替换,但它至少会自己管理它们,不像临时文件,您的数据不会写入文件系统。
您可以使用 echo <(true) 检查 bash 如何实现进程替换,以打印文件名而不是从中读取。在典型的 Linux 系统上,它会打印 /dev/fd/63。或者如果想要更多关于 bash 使用的系统调用的详细信息,在 Linux 系统上,以下命令将跟踪文件和文件描述符系统调用。
strace -f -efile,desc,clone,execve bash -c '/bin/true | diff -u - <(/bin/true)'

没有bash,你可以创建一个命名管道。使用”-”告诉diff从标准输入中读取一个输入,并将命名管道用作另一个输入:
mkfifo file1_pipe.txt
foo|bar > file1_pipe.txt && baz | quux | diff file1_pipe.txt - && rm file1_pipe.txt

请注意,使用tee命令时,只能将一个输出导入多个输入
ls *.txt | tee /dev/tty txtlist.txt 

以上命令将ls *.txt的输出显示到终端并输出到文本文件txtlist.txt。

但是使用进程替换,您可以使用tee将相同的数据馈送到多个管道中:

cat *.txt | tee >(foo | bar > result1.txt)  >(baz | quux > result2.txt) | foobar

5
即使没有Bash,您仍然可以使用临时FIFO(First-in-First-out)管道:mkfifo a; cmd >a& cmd2|diff a -; rm a - unhammer
您可以使用常规管道作为其中一个参数:pipeline1 | diff -u - <(pipeline2)。然后输出将更清晰地提醒您哪个输入是哪个,通过显示 -- /dev/stdin vs. ++ /dev/fd/67 或其他内容,而不是两个编号的文件描述符。 - Peter Cordes
进程替换(foo <( pipe ))不会修改文件系统。管道是匿名的,在文件系统中没有名称。Shell使用pipe系统调用来创建它,而不是mkfifo。如果您想亲自查看,请使用strace -f -efile,desc,clone,execve bash -c '/bin/true | diff -u - <(/bin/true)'跟踪文件和文件描述符系统调用。在Linux上,/dev/fd/63/proc虚拟文件系统的一部分;它自动为每个文件描述符添加条目,并且它不是内容的副本。因此,除非foo 3<bar.txt计数,否则不能将其称为“临时文件”。 - Peter Cordes
为什么不修复您的第一个大段落,而是留下错误并仅发布更正?请注意,丹尼尔·卡西迪(Daniel Cassidy)在发布一年后删除了他的答案,可能是因为它是错误的。 - Peter Cordes
1
@PeterCordes 我会把任何编辑留给你:这就是 Stack Overflow 有趣的地方:任何人都可以“修正”一个答案。 - VonC
显示剩余2条评论

136
在bash中,您可以使用子shell来单独执行命令管道,将管道括在括号内。然后,您可以使用“<”前缀创建匿名命名管道,并将其传递给diff。
例如:

在bash中,您可以使用子shell来单独执行命令管道,将管道括在括号内。然后,您可以使用“<”前缀创建匿名命名管道,并将其传递给diff。

例如:

diff <(foo | bar) <(baz | quux)

匿名命名管道由Bash管理,因此它们会自动创建和销毁(与临时文件不同)。


1
比起我对同一个解决方案的简化版,这个更加详细 -- 匿名批处理 --。+1 - VonC
5
在Bash中,这被称为“过程替代”。 - Franklin Yu

7

有些访问此页面的人可能正在寻找逐行差异,应该使用commgrep -f。需要指出的一件事是,在所有答案的示例中,差异实际上要等到两个流都完成才会开始。可以使用以下命令进行测试:

comm -23 <(seq 100 | sort) <(seq 10 20 && sleep 5 && seq 20 30 | sort)

如果这是个问题,你可以尝试使用 sd(流式差异比对工具),它不需要排序(像comm一样)或者进程替换(就像上面的例子),比grep -f快几个数量级,并支持无限流。
sd中,我提出的测试例子应该这样写:
seq 100 | sd 'seq 10 20 && sleep 5 && seq 20 30'

但是不同之处在于,seq 100 将立即与 seq 10 进行差异比较。请注意,如果其中一个流是 tail -f,则无法使用进程替换进行差异比较。这是我写的一篇关于终端流差异比较的博客文章,介绍了 sd

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接