【问题标题】:How can you diff two pipelines in Bash?你如何区分 Bash 中的两个管道?
【发布时间】:2018-03-08 12:05:16
【问题描述】:

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

foo | bar
baz | quux

你想在他们的输出中找到diff。一种解决方案显然是:

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

是否可以在不使用 Bash 中的临时文件的情况下这样做?您可以通过在其中一个管道中进行管道来区分一个临时文件:

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

但是您不能同时将两条管道通过管道传输到 diff 中(至少不是以任何明显的方式)。是否有一些涉及/dev/fd 的巧妙技巧可以在不使用临时文件的情况下做到这一点?

【问题讨论】:

    标签: bash diff pipeline


    【解决方案1】:

    一行包含 2 个 tmp 文件(不是您想要的):

     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),然后使用 dup2quux 的输出重定向到 diff 的输入文件描述符,在 fd 63 上。)

    在没有“神奇”/dev/fd/proc/self/fd 的系统上,bash 可能会使用命名管道来实现进程替换,但与临时文件不同,它至少会自己管理它们,并且不会写入您的数据到文件系统。

    您可以检查 bash 如何使用 echo &lt;(true) 实现进程替换以打印文件名而不是从中读取文件名。它在典型的 Linux 系统上打印 /dev/fd/63。或者要详细了解 bash 使用的系统调用,Linux 系统上的这个命令将跟踪文件和文件描述符系统调用

    strace -f -efile,desc,clone,execve bash -c '/bin/true | diff -u - <(/bin/true)'
    

    没有 bash,您可以创建命名管道。使用- 告诉diff 从STDIN 读取一个输入,并将命名管道用作另一个:

    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
    

    【讨论】:

    • 即使没有 bash,也可以使用临时 fifo 的mkfifo a; cmd &gt;a&amp; cmd2|diff a -; rm a
    • 您可以使用常规管道作为参数之一:pipeline1 | diff -u - &lt;(pipeline2)。然后输出会更清楚地提醒您哪个输入是哪个,通过显示 -- /dev/stdin++ /dev/fd/67 或其他内容,而不是两个编号的 fd。
    • 进程替换 (foo &lt;( pipe )) 不会修改文件系统。 管道是匿名的;它在文件系统中没有名称。 shell 使用pipe 系统调用来创建它,而不是mkfifo。如果您想亲自查看,请使用 strace -f -efile,desc,clone,execve bash -c '/bin/true | diff -u - &lt;(/bin/true)' 跟踪文件和文件描述符系统调用。在 Linux 上,/dev/fd/63/proc 虚拟文件系统的一部分;它自动为每个文件描述符提供条目,并且它不是内容的副本。所以你不能把它称为“临时文件”,除非foo 3&lt;bar.txt 很重要
    • @PeterCordes 好点。我已将您的评论包含在答案中以提高知名度。
    • @PeterCordes 我会将任何编辑留给您:这就是 Stack Overflow 的有趣之处:任何人都可以“修复”答案。
    【解决方案2】:

    在 bash 中,您可以使用子shell 来单独执行命令管道,方法是将管道括在括号中。然后,您可以在它们前面加上

    例如:

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

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

    【讨论】:

    • 比我对同一解决方案(匿名批处理)的修订要详细得多。 +1
    • 这在 Bash 中称为 process substitution
    【解决方案3】:

    到达此页面的某些人可能正在寻找逐行差异,应使用 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,则无法通过进程替换来完成差异。

    这是我写的关于在终端上区分流的blogpost,其中介绍了sd

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2012-07-12
      • 1970-01-01
      • 2013-05-03
      • 2022-10-06
      • 1970-01-01
      • 2020-05-14
      • 1970-01-01
      • 2017-11-25
      相关资源
      最近更新 更多