计算行数或枚举行号,以便我可以遍历它们 - 为什么这是一种反模式?

tri*_*eee 1 bash shell anti-patterns

我发布了以下代码并被骂了。为什么这是不可接受的?

numberOfLines=$(wc -l <"$1")
for ((i=1; $i<=$numberOfLines; ++$i)); do
  lineN=$(sed -n "$i!d;p;q" "$1")
  # ... do things with "$lineN"
done
Run Code Online (Sandbox Code Playgroud)

我们将输入文件中的行数收集到 中numberOfLines,然后从 1 循环到该数字,sed在每次迭代中从文件中提取下一行。

我收到的反馈抱怨说,sed在循环内部重复读取同一个文件以获取下一行效率低下。我想我可以使用,head -n "$i" "$1" | tail -n 1但这几乎没有效率,是吗?

有一个更好的方法吗?为什么我要避免这种特殊方法?

tri*_*eee 11

shell(以及基本上所有高于汇编语言的编程语言)已经知道如何遍历文件中的行;它不需要知道将有多少行来获取下一行——令人惊讶的是,在你的例子中,sed已经这样做了,所以如果 shell 不能这样做,你可以sed改为循环遍历输出。

在 shell 中循环文件中的行的正确方法是使用while read. 有一些复杂情况 - 通常,您重置IFS以避免外壳不必要地将输入拆分为标记,并且您使用read -r原始 Bourne shell 的 实现中的反斜杠来避免一些讨厌的遗留行为read,这些行为已被保留以实现向后兼容性。

while IFS='' read -r lineN; do
    # do things with "$lineN"
done <"$1"
Run Code Online (Sandbox Code Playgroud)

除了比您的sed脚本简单得多之外,这还避免了您一次读取整个文件以获取行数,然后在每次循环迭代中一次又一次地读取同一个文件的问题。使用典型的现代磁盘驱动程序,可以通过缓存来避免一些重复读取,但基本事实仍然是,从磁盘读取信息的速度比不这样做时可以避免它慢 1000 倍。特别是对于一个大文件,缓存最终会填满,所以你最终会一遍又一遍地读入和丢弃相同的字节,增加大量的 CPU 开销,甚至更多的 CPU 只是在做其他事情时等待磁盘传送您读取的字节。

在 shell 脚本中,如果可以,您还希望避免外部进程的开销。在紧密循环中调用sed(或功能等效但更昂贵的两个进程head -n "$i"| tail -n 1)数千次将为任何非平凡的输入文件增加大量开销。(另一方面,如果你的循环体可以在 egsed或 Awk 中完成,那么这将比本地 shellwhile read循环更有效,因为实现的方式read。这就是为什么while read也经常被认为是一个反模式。

qsed脚本是一个很偏的补救措施; 经常,您会看到sed脚本每次都会读取整个输入文件直到最后的变化,即使它只想从文件中提取第一行之一。

对于一个小的输入文件,影响可以忽略不计,但是仅仅因为当输入文件很小时它不会立即有害而使这种不良做法永久化是不负责任的。只是不要将这种技术教给初学者。在所有。

如果您确实需要显示输入文件中的行数,请至少确保您不会为了获得该数字而花费大量时间寻找到最后。也许stat文件并跟踪每行上有多少字节,因此您可以投影剩余的行数(而不是line 1/10345234显示类似的内容line 1/approximately 10000000?)...或使用像pv.

切线地,您也想避免一个模糊相关的反模式;当您一次只处理一行时,您希望避免将整个文件读入内存。在for循环中这样做也有一些额外的问题,所以也不要这样做;见https://mywiki.wooledge.org/DontReadLinesWithFor

  • 稍微扩展一下_显着的开销_。通过执行 OP 中提供的操作,您可以一遍又一遍地执行各种操作。这包括打开文件、读取文件直至感兴趣的行、关闭文件。这确保了原始程序的复杂度是 O(N^2) ,而在这个答案中它只是 O(N) (你只打开、读取和关闭文件)。对于大文件以及存储在基于网络的文件系统上的文件,原始过程将变得非常慢。 (3认同)