"while head -n 1" 奇怪现象

3

一些编程实验(在尝试寻找更短的编码答案时进行)导致了一些有趣的发现:

seq 2 | while head -n 1 ; do : ; done

输出(按 Ctrl-C 或它将永远浪费CPU周期):
1
^C

相同的操作,但使用重定向的输入文件替代管道输入:

seq 2 > two
while head -n 1 ; do : ; done < two

输出结果(按下Control-C):

1
2
^C

问题:

  1. 为什么while循环不能像seq 2 | head -n 1那样停止?

  2. 为什么重定向输入会产生更多的输出,而不是管道输入


以上代码在最近的Lubuntu上使用dashbash进行了测试。 seqhead来自coreutils(版本8.25-2ubuntu2)软件包。

解决需要按(Ctrl-C)的方法:

timeout .1 sh -c "seq 2 > two ; while head -n 1 ; do : ; done < two"

1
2

timeout .1 sh -c "seq 2 | while head -n 1 ; do : ; done"

1


如果没有其他选择,这是区分重定向输入和管道输入的方法。唯一不方便的是 'Control-C'。 - agc
有趣的是,更有趣的是,如果你使用<<重定向,这种效果就会消失。 - Joce NoToPutinsWarInUkraine
1
当在其标准输入上给出一个空文件时,你认为 head -n 1 会做什么? - Charles Duffy
@agc,我指的是前者。head -n 1 /dev/null -- 它的退出状态是什么?当 while 循环的条件部分看到这个退出状态时,你希望它采取什么行动? - Charles Duffy
2
您可能会发现这个答案很有启发性。 - rob mayoff
显示剩余6条评论
2个回答

3

head -n 1,如果在标准输入流上接收到空数据流,则有权并符合规范立即以成功的退出状态退出。

因此:

seq 2 | while head -n 1 ; do : ; done

我可以合法地无限循环,因为head -n 1不需要以非零状态退出并终止循环。(只有在“发生错误”时才需要标准的非零退出状态,而文件的行数少于要求输出的行数不被定义为错误)。

事实上,这是明确的:

当一个文件包含少于number行时,它应该完全复制到标准输出。这不应该是一个错误。


现在,如果您的head实现在第一次调用后(打印第一行内容)退出时将文件指针排队到第二行开头处(这绝对不是必须的),那么第二个循环实例将读取第二行并发出它。然而,这仍然是一个实现细节,取决于编写您的head实现的人选择以下哪种方式之一:
  1. 读取一个非常大的块,但仅发出其中的子集。(更高效的实现。)
  2. 或者逐字符循环以仅消耗单个行。
实现者完全有权根据仅在运行时可用的标准决定要遵循哪些实现。
现在,假设您的head总是尝试一次读取8kb块。那么,它怎么可能会将指针排队到第二行?[* - 除了向后查找(当给定文件时,某些实现会这样做,但标准不要求;感谢Rob Mayhoff在此提供的指针]
如果并发调用seq仅在第一个read发生时已写入和刷新单个行,则可能会发生这种情况。
显然,这是一种非常时间敏感的情况——竞争条件——也取决于未指定的实现细节(例如seq是否在行之间刷新其输出——由于seq未作为POSIX或任何其他标准的一部分指定,因此在平台之间完全不同)。

这是很多背景信息,如果没有其他更好的答案,我稍后会批准。不过,对我来说,这个答案有点“政治化”,因为它涉及到“权利”、“非必需”和标准化的限制等词汇。 - agc
作为语言律师,我属于不后悔的那一类:如果某种行为没有被规范保证,那么它就可以随着任何平台变化、任何软件升级、任何运行时环境修改而消失,而这种行为的改变并不构成任何人的错误。语言律师(广义上包括规范文档)至关重要:它可以帮助你了解哪些行为是可以信任的承诺,哪些行为存在于你使用的库最后一次重构者的心情中,或者根本不存在。 - Charles Duffy
我不会反对...关于“没有任何人犯错的情况下...出现了一个bug”,这让我想起了两个描绘神秘再分配的19世纪形象:'Twas HimGet off the Earth - agc
经再次反思,有些保留。这是一个很好的答案,并且回答了问题,但它在将概括与特定编码示例的分析自由混合时,概括往往会掩盖分析,以至于即使一天后也需要重读。如果特定分析首先进行(关于coreutils v8.25-2ubuntu2),然后是在 POSIX 有意未指定的空白内部可能产生的输出排列的调查之后,效果会更好。 - agc

0

被接受的答案是正确的。head不会为输入(甚至没有输入)返回非零值

但我发现了一些更多的奇怪之处


我找到了一种方法,可以正确地停止它。

seq 10 | while head -c 4 | ifne -n false; do : ; done;

遗憾的是,由于 head 的输出遍布在 while 的主体上,因此您无法对该构造做太多事情。

我发现的一个用途是在每 x 个字节(包括尾部)中插入一个字符。

/> printf '12345678910' | { while head -c 2 | ifne -n false; do printf 'a'; done; }
/> 12a34a56a78a91a0a

你应该使用 sed 's/.\{4\}/&a/g'

这里有一个稍微更有用的命令,它将接收两个字节的输入,"处理"它,然后将其放在某个地方:

printf '12345678910' | { while true; do head -c 2 < /dev/stdin | ifne -n false >> file.txt || break; done;

你应该使用 split --filter

还有一个非常奇怪的用例,当你尝试在 while 循环中使用 /dev/stdin 调用 head 时。

/> printf '12345678910' | { while head -c 2 | ifne -n false; do head -c 3 </dev/stdin | ifne -n false >> every3.txt || break; done > every2.txt; }
/> cat every2.txt
12670
/> cat every3.txt
345891

正如您所看到的,它每2个字节循环一次,然后每3个字节循环一次。 12 345 67 891 0

您应该使用bbe

您可以将其用作某种简陋的进度指示器。

/> printf '12345678910' | while head -c 2 | ifne -n false; do echo "2 bytes travelled" > /dev/stderr ; done > /dev/null;
2 bytes travelled
2 bytes travelled
2 bytes travelled
2 bytes travelled
2 bytes travelled
2 bytes travelled # imperfect because actually only 1 byte travelled here

你应该使用pv

这个结构实际上可以做什么...

¯\_(ツ)_/¯


我觉得这个结构可以做一些其他方式不可能或非常困难的事情... 如果您有任何用例,请告诉我! - WesAtWork
我觉得这个结构有一些其他方式无法实现或者非常困难的功能... 如果你有任何使用案例,请告诉我! - undefined

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