给定以下 3 个脚本:
printf 'a\nb\nc\n' > file && { head -n 1; cat; } < file
printf 'a\nb\nc\n' | { head -n 1; cat; }
{ head -n 1; cat; } < <(printf 'a\nb\nc\n')
我期望每个的输出是:
a
b
c
但对于其中一些系统,在某些系统上,情况并非如此。例如,在 cygwin 上:
$ printf 'a\nb\nc\n' > file && { head -n 1; cat; } < file
a
b
c
$ printf 'a\nb\nc\n' | { head -n 1; cat; }
a
$ { head -n 1; cat; } < <(printf 'a\nb\nc\n')
a
是什么导致这些脚本的输出不同?
附加信息 - 这显然不仅仅是一个head
问题:
$ printf 'a\nb\nc\n' | { sed '1q'; cat; }
a
$ printf 'a\nb\nc\n' | { awk '1;{exit}'; cat; }
a
$ { sed '1q'; cat; } < <(printf 'a\nb\nc\n')
a
$ { awk '1;{exit}'; cat; } < <(printf 'a\nb\nc\n')
a
shell 中的一种健壮的 POSIX 方式(即不只是调用 awk 或类似的一次来完成所有操作)从输入中读取一些行并将其余行留给不同的命令,无论输入是否来自管道或一份文件?
这个问题的灵感来自于一个答案下的评论根据特定列中的值对整个 .csv 进行排序。
答案1
head
可能读取其整个输入。它必须至少读取它输出的内容(否则逻辑上不可能实现),但它可能会读取更多。
通常head
要求操作系统读取固定大小的数据缓冲(通过调用read
系统调用或类似)。然后,它在该缓冲区中查找换行符,并打印输出,直到达到所需的行数。
全部符合 POSIX 标准head
调用的实现lseek
将输入上的文件位置重置为紧接在已复制到输出的部分末尾之后。然而,只有当文件可查找时,这才是可能的:这包括普通文件,但不包括管道。如果输入是管道,则head
读取的任何内容都会从管道中丢弃并且无法放回。这解释了您在<file
(常规文件)和|
或<()
(管道)之间观察到的差异。
上述标准的相关部分是:
当标准实用程序读取可查找输入文件并在到达文件末尾之前无错误地终止时,该实用程序应确保打开的文件描述中的文件偏移量正确定位在该实用程序处理的最后一个字节之后。对于不可查找的文件,该文件的打开文件描述中的文件偏移量状态是未指定的。符合要求的应用程序不应假设以下三个命令是等效的:
tail -n +2 file (sed -n 1q; cat) < file cat file | (sed -n 1q; cat)
仅当文件可查找时,第二个命令才与第一个命令等效。第三个命令将打开文件描述中的文件偏移量保留为未指定状态。其他实用程序(例如 head、read 和 sh)具有类似的属性。
一些head
实现,例如head
ksh93 的内置实现(在 后启用builtin head
,并在构建时包含它)也尝试在输入不可查找时不要将光标保留在其输出的最后一行上,在 ksh93 的情况下,通过读取一次输入一个字节(就像 shellread
内置函数通常所做的那样)或通过偷看在有这种可能性的系统(不是 Linux)上读取管道的内容之前。但这些都是例外,因为性能会受到严重影响。