头吃额外的字符

ant*_*_rh 15 pipe utilities shell-script text-processing head

预计以下 shell 命令仅打印输入流的奇数行:

echo -e "aaa\nbbb\nccc\nddd\n" | (while true; do head -n 1; head -n 1 >/dev/null; done)
Run Code Online (Sandbox Code Playgroud)

但它只是打印第一行:aaa

-c( --bytes) 选项一起使用时不会发生同样的情况:

echo 12345678901234567890 | (while true; do head -c 5; head -c 5 >/dev/null; done)
Run Code Online (Sandbox Code Playgroud)

此命令1234512345按预期输出。但这仅适用于该实用程序的coreutils实现head。该busybox的执行还是吃多余的字符,所以输出正好12345

我想这种特定的实现方式是为了优化目的。您无法知道该行在哪里结束,因此您不知道需要阅读多少个字符。不消耗输入流中额外字符的唯一方法是逐字节读取流。但是一次从流中读取一个字节可能会很慢。所以我想head将输入流读取到一个足够大的缓冲区,然后计算该缓冲区中的行数。

对于--bytes使用选项的情况,情况并非如此。在这种情况下,您知道需要读取多少字节。所以你可以准确地读取这个字节数,而不是更多。该corelibs实现使用这个机会,但是busybox的一个没有,它仍然比读取所需到缓冲区的字节以上。这样做可能是为了简化实现。

所以问题。head实用程序从输入流中消耗比要求的更多字符是否正确?Unix 实用程序是否有某种标准?如果有,它是否指定了这种行为?

聚苯乙烯

您必须按Ctrl+C以停止上述命令。Unix 实用程序不会在读取超出EOF. 如果不想按,可以使用更复杂的命令:

echo 12345678901234567890 | (while true; do head -c 5; head -c 5 | [ `wc -c` -eq 0 ] && break >/dev/null; done)
Run Code Online (Sandbox Code Playgroud)

为简单起见,我没有使用它。

Ste*_*itt 30

head 实用程序从输入流中消耗比要求的更多字符是否正确?

是的,这是允许的(见下文)。

Unix 实用程序是否有某种标准?

是的,POSIX 第 3 卷,Shell & Utilities

如果有,它是否指定了这种行为?

它确实在介绍中:

当标准实用程序读取可查找的输入文件并在它到达文件结尾之前无错误地终止时,该实用程序应确保打开文件描述中的文件偏移正确定位在该实用程序处理的最后一个字节之后。对于不可查找的文件,该文件的打开文件描述中的文件偏移状态是未指定的。

head标准实用程序之一,因此符合 POSIX 的实现必须实现上述行为。

GNUhead 确实尝试将文件描述符保留在正确的位置,但是不可能在管道上查找,因此在您的测试中它无法恢复该位置。你可以看到这个使用strace

$ echo -e "aaa\nbbb\nccc\nddd\n" | strace head -n 1
...
read(0, "aaa\nbbb\nccc\nddd\n\n", 8192) = 17
lseek(0, -13, SEEK_CUR)                 = -1 ESPIPE (Illegal seek)
...
Run Code Online (Sandbox Code Playgroud)

所述read返回17个字节(所有可用的输入),head处理这些四个然后尝试移回的13个字节,但它不能。(您还可以在此处看到 GNUhead使用 8 KiB 缓冲区。)

当您告诉head计数字节(这是非标准的)时,它知道要读取多少字节,因此它可以(如果以这种方式实现)相应地限制其读取。这就是您的head -c 5测试有效的原因:GNUhead只读取五个字节,因此不需要寻求恢复文件描述符的位置。

如果您将文档写入文件,然后使用它,您将获得您所追求的行为:

$ echo -e "aaa\nbbb\nccc\nddd\n" > file
$ < file (while true; do head -n 1; head -n 1 >/dev/null; done)
aaa
ccc
Run Code Online (Sandbox Code Playgroud)

  • 请注意,`head -c 5` 将读取 5 个字节还是完整的缓冲区取决于实现(另请注意,`head -c` 不是标准的),你不能依赖它。您需要 `dd bs=1 count=5` 来保证不超过 5 个字节将被读取。 (3认同)
  • 可以使用 `line`(现在从 POSIX/XPG 中删除,但在许多系统上仍然可用)或 `read`(`IFS= read -r line`)实用程序代替,它们一次读取一个字节以避免该问题。 (2认同)

ilk*_*chu 6

来自 POSIX

工具应在其输入文件复制到标准输出,在指定点结束的每个文件的输出。

它没有说明head 必须从输入中读取多少内容。要求它逐字节读取是愚蠢的,因为在大多数情况下它会非常慢。

然而,这是在read内置/实用程序中解决的:我可以read从管道中一次一个字节地找到所有外壳,并且可以将标准文本解释为意味着必须这样做,以便能够仅读取一行:

实用程序应当从标准输入读取一个单一的逻辑线到一个或多个壳的变量。

如果read, 在 shell 脚本中使用,一个常见的用例是这样的:

read someline
if something ; then 
    someprogram ...
fi
Run Code Online (Sandbox Code Playgroud)

这里,of 的标准输入someprogram与shell的标准输入相同,但可以预期的是,它someprogram会读取 所消耗的第一个输入行之后的所有内容,read而不是 缓冲读取后剩余的任何内容read。另一方面,head在您的示例中使用as 更为罕见。


如果您真的想删除所有其他行,最好(更快)使用一些可以一次性处理整个输入的工具,例如

$ seq 1 10 | sed -ne '1~2p'   # GNU sed
$ seq 1 10 | sed -e 'n;d'     # works in GNU sed and the BSD sed on macOS

$ seq 1 10 | awk 'NR % 2' 
$ seq 1 10 | perl -ne 'print if $. % 2'
Run Code Online (Sandbox Code Playgroud)

  • 请注意,除非您使用 `-r`,否则 `read` 可能会读取多行(如果没有 `IFS=`,它还会去除前导和尾随空格和制表符(默认值为 `$IFS`))。 (2认同)