“(head; tail) < file”是如何工作的?

32

(来源于 https://dev59.com/G2oy5IYBdhLWcg3wUcj_#8624829)

(head; tail) < file 是如何工作的?注意,cat file | (head;tail) 不起作用。

此外,为什么 (head; wc -l) < file 会给出 wc 的输出结果为 0 呢?

注意:我了解 head 和 tail 的工作原理,只是不知道这些特定调用中涉及的细节。


3
你为什么说cat file | (head;tail)不行?这个命令在我这里可以正常使用。另外,你为什么说(head; wc -l) < file输出结果为0呢?这个命令在我这里也可以正常使用。你使用的是什么系统和Bash版本?是否有特定的文件无法使用?这些文件的行数是少于10行、10-20行还是多于20行呢? - Brian Campbell
只是为了增加混乱:Mac OS X 10.7.5,bash 4.2.37(2)。我使用文件重定向看到前面和后面的10行,但使用管道只看到前面的10行。在*两种情况下,'wc -l'都返回0。 - chepner
1
Ubuntu 12.10,bash 4.2.37(1)在这里:重定向和管道都有效,并且wc -l给出的是文件中的行数-10,我猜这是预期的?我不知道这个语法,非常酷。 - Linus Thiel
我在 Mac OS 上尝试使用 bash。 - zellyn
Xfce 4.8,bash 4.2.25,管道有时会起作用。$ curl -s http://www.wikipedia.org/ | ( head -n 2; echo "#--#"; tail -n 1; ) 运行良好,但 $ yes | head -12 | cat -n | ( head -n 2; echo "#--#"; tail -n 1; ) 只显示头部输出。这可能是答案中提到的头部缓冲区问题,因为 $ yes | head -5000 | cat -n | ( head -n 2; echo "#--#"; tail -n 1; ) 运行得很好。 - Stephen
4个回答

20

OS X

如果你使用 OS X,你可以查看head 的源代码tail 的源代码,以了解一些情况。对于 tail,您需要查看 forward.c

实际上,head 并没有做什么特殊的事情。它只是使用 stdio 库读取其输入,因此它会一次读取一个缓冲区,并可能读取过多。这意味着对于小文件,cat file | (head; tail) 不起作用,因为 head 的缓冲区使其读取最后 10 行的一些(或全部)内容。

另一方面,tail 检查其输入文件的类型。如果它是一个普通文件,tail 将寻找到末尾并向后读取,直到找到足够的行数进行输出。这就是为什么 (head; tail) < file 可以在任何普通文件上工作,而不管其大小。

Linux

您也可以查看 Linux 上 headtail 的源代码,但最简单的方法是使用 strace,像这样:

(strace -o /tmp/head.trace head; strace -o /tmp/tail.trace tail) < file

看一下/tmp/head.trace。你会发现head命令尝试通过从标准输入(文件描述符0)读取来填充缓冲区(在我的测试中为8192字节)。根据file的大小,它可能会或可能不会填充缓冲区。无论如何,让我们假设它在第一次读取中读取了10行。然后,它使用lseek将文件描述符回退到第10行的末尾,实际上是“未读取”任何多余的字节。这很有效,因为文件描述符是打开在普通的可寻址文件上的。因此,(head; tail) < file可以适用于任何可寻址文件,但它不会使cat file | (head; tail)工作。

另一方面,tail在我的测试中不会向OS X上那样定位到结尾并向后读取。至少它不会一直读回文件的开头。

以下是我的测试。创建一个小的12行输入文件:

yes | head -12 | cat -n > /tmp/file

然后,在Linux上尝试使用(head; tail) < /tmp/file。 我在GNU coreutils 5.97中得到了以下结果:

     1  y
     2  y
     3  y
     4  y
     5  y
     6  y
     7  y
     8  y
     9  y
    10  y
    11  y
    12  y

但在OS X上,我得到了这个:

     1  y
     2  y
     3  y
     4  y
     5  y
     6  y
     7  y
     8  y
     9  y
    10  y
     3  y
     4  y
     5  y
     6  y
     7  y
     8  y
     9  y
    10  y
    11  y
    12  y

1
谢谢,答案太棒了!我和同事得出结论是它涉及到缓冲(通过执行seq 10000来生成流,并注意到在第4096个字符处切换),但您的答案更全面,并且“检查是否为常规文件”解释了从文件重定向和管道之间的差异。 - zellyn

12
这里的括号创建了一个“子shell”,这是另一个解释器运行内部命令的实例,有趣的是,子shell充当单一的stdin/stdout组合;在这种情况下,它首先将stdin连接到head,后者会回显前10行并关闭管道,然后子shell将其stdin连接到tail,后者会消耗剩余的内容并将最后10行写回stdout,但子shell会接收两个输出,并将它们作为自己的stdout写入,这就是为什么看起来是混合的。
值得一提的是,相同的效果可以通过命令分组来实现,例如{ head; tail; } < file,这更便宜,因为它不会创建另一个bash实例。

4
如果文件足够大,所有这些命令都应该按预期工作。head命令将消耗一定量的输入(不仅仅是它所需的缓冲区),如果剩余的输入量不足以供tail命令使用,则无法正常工作。
另一个问题是管道导致两侧并行执行,因此生产方可能会导致消费方的head命令每次运行时读取不同的数量。
比较以下命令的多次运行:
for i in `seq 1 10`; do echo "foo"; done | (head -n1; wc -l)

每次使用wc命令时,文件的数量应该不同。

当使用<输入时,似乎不存在这种并行性(可能是因为bash读取整个输入然后将其传递给head命令)。


我在每次运行该命令时都看到相同的输出。然而,通过将数字10更改为更高的值,我可以触发不同的行为。 - zellyn

-2

head命令显示文件的前10行(默认值)。tail命令显示文件的最后10行(默认值)。 假设文件只有3行,这些命令也可以显示这些行。 但是如果您有超过10行,则这两个命令将仅显示默认的10行。 可以使用-n、n、+n选项更改默认行数。(请参阅man页面)


我的问题不是关于head和tail如何工作,而是关于特定调用的微妙之处。 - zellyn

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