shell 计算行数或枚举行号以便循环遍历它们--为什么这是反模式?

wnvonmuf  于 2023-01-17  发布在  Shell
关注(0)|答案(1)|浏览(141)

我贴了下面的代码,被骂了,为什么不能接受呢?

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

我们将输入文件中的行数收集到numberOfLines中,然后从1循环到那个数字,每次迭代都从文件中取出下一行sed
我收到的反馈抱怨说,在循环中使用sed重复阅读同一个文件以获取下一行的效率很低,我想我可以使用head -n "$i" "$1" | tail -n 1,但这几乎没有更高的效率,是吗?
有没有更好的方法来做到这一点?为什么我要避免这种特殊的方法?

piah890a

piah890a1#

shell(以及基本上汇编语言之上的所有编程语言)已经知道如何循环遍历文件中的行;它不需要知道需要多少行才能读取下一行--在您的示例中,sed已经执行了这一操作,因此如果shell不能执行,您可以循环sed的输出。
在shell中循环遍历文件中的行的正确方法是使用while read。这有两个复杂的问题--通常,您重置IFS以避免shell不必要地将输入拆分为令牌,使用read -r以避免在原始Bourne shell的read实现中使用反斜杠的一些讨厌的遗留行为。为了向后兼容性而保留了它们。

while IFS='' read -r lineN; do
    # do things with "$lineN"
done <"$1"

除了比sed脚本简单得多之外,它还避免了读取整个文件一次以获得行计数,然后在每次循环迭代中反复读取同一文件的问题。对于典型的现代操作系统,由于缓存,可以避免一些重复阅读(盘驱动器在存储器中保持最近访问的数据的缓冲区,使得再次阅读它实际上将不需要再次从盘取回它),但基本的事实仍然是,从磁盘阅读信息比在可以避免的情况下不读取信息要慢1000倍。特别是对于大文件,该高速缓存最终会填满,因此您最终会一遍又一遍地读取和丢弃相同的字节,增加了大量的CPU开销,甚至更大量的CPU只是在等待磁盘一次又一次地传送您读取的字节时做其他事情。
在shell脚本中,如果可以的话,您还希望避免外部进程的开销。(或者功能上等效但更昂贵的双进程head -n "$i"| tail -n 1)在一个紧密循环中执行数千次将为任何重要的输入文件增加显著的开销。另一方面,如果循环体可以在sed或Awk中完成,因为read的实现方式,这将比原生shell while read循环更高效。这就是为什么while read is also frequently regarded as an antipattern.。并且确保您相当熟悉Unix text processing tools的标准调色板-cutpastenlpr等。在许多情况下,在很多情况下,你应该避免在shell脚本中的代码行上循环,而使用一个外部工具来代替。2基本上只有一个例外;当循环体也大量使用内置shell命令时。
sed脚本中的q是对重复阅读输入文件的非常部分的补救;而且,您经常会看到sed脚本每次都将读取整个输入文件直到末尾的变化,即使它只想从文件中取出最前面的一行。
对于一个小的输入文件,这种影响可以忽略不计,但是仅仅因为输入文件很小时它不会立即造成伤害就继续这种不良做法是不负责任的。只是不要把这种技术教给初学者。
如果你真的需要显示输入文件的行数,比如进度指示器或者类似的东西,至少要确保你不会花很多时间去寻找文件的末尾,也许可以stat这个文件,记录每一行有多少字节。这样您就可以投影剩余的行数(并且显示类似line 1/approximately 10000000的内容,而不是line 1/10345234?)...或者使用类似pv .的外部工具
间接地说,还有一个模糊相关的反模式需要避免;当你一次只处理一行的时候,你不想把整个文件读入内存,在for循环中这样做也会有一些额外的陷阱,所以也不要这样做;参见https://mywiki.wooledge.org/DontReadLinesWithFor
另一个常见的变化是用grep找到你想修改的行,这样你就可以用sed找到它......它已经完全知道如何自己执行正则表达式搜索。(另请参见grep的无用使用)。

# XXX FIXME: wrong
line=$(grep "foo" file)
sed -i "s/$line/thing/" file

正确的方法是更改sed脚本以包含搜索条件:

sed -i '/foo/s/.*/thing/' file

当原始错误脚本中$line的值包含一些需要转义才能实际匹配自身的内容时,这也避免了复杂性(例如,正则表达式中的foo\bar*与文本本身不匹配)。

相关问题