在POSIX Shell中,通常使用while循环处理文本是否被认为是不好的做法?

正如StéphaneChazelas所指出的,不使用shell循环的一些原因是概念,可靠性,易读性,性能和安全性。

此答案解释了可靠性和易读性方面:

while IFS= read -r line <&3; do
  printf '%s\n' "$line"
done 3< "$InputFile"


为了性能,从文件读取时while循环和读取速度非常慢或管道,因为内置的读取外壳一次只能读取一个字符。

概念和安全方面如何?

评论

相关(硬币的另一面):是的,如何这么快地写入文件?

内置的读取外壳程序一次不读取单个字符,而是一次读取一行。 wiki.bash-hackers.org/commands/builtin/read

@ A.Danischewski:这取决于您的外壳。在bash中,它一次读取一个缓冲区大小,例如尝试使用破折号。另请参见unix.stackexchange.com/q/209123/38906

#1 楼

是的,我们看到许多类似的东西:

while read line; do
  echo $line | cut -c3
done


或更糟的是:

for line in `cat file`; do
  foo=`echo $line | awk '{print }'`
  echo whatever $foo
done


(不要笑,我见过很多。)

通常来自shell脚本初学者。这些只是您在C或python之类的命令性语言中会做的幼稚的文字翻译,但这不是您在shell中做事的方式,并且这些示例非常低效,完全不可靠(可能导致安全问题),并且如果您曾经管理过,要修复大多数错误,您的代码将变得难以辨认。

从概念上讲,

在C或大多数其他语言中,构建块仅比计算机指令高一级。您告诉处理器该做什么,然后告诉下一步。用手拿住处理器并对其进行微管理:打开该文件,读取很多字节,然后执行此操作。

Shell是一种高级语言。有人可能说这甚至不是一门语言。它们位于所有命令行解释器之前。这项工作由您运行的那些命令完成,而外壳程序仅用于对它们进行编排。

Unix引入的一大优势是管道和所有命令的默认stdin / stdout / stderr流

在50年来,我们发现没有比该API更好的方法来利用命令的功能并使它们协作完成任务。这可能是当今人们仍在使用炮弹的主要原因。
您拥有切割工具和音译工具,并且只需执行以下操作即可:

cut -c4-5 < in | tr a b > out


Shell只是在做管道(打开文件,设置管道,调用命令),当它们准备就绪时,它就在外壳不做任何事情的情况下流动了。这些工具可以按照自己的步调同时进行工作,并有足够的缓冲,以使它们不会相互阻塞,它们既美观又简单。

尽管调用工具需要付出一定的代价(我们将在性能点上进行开发)。这些工具可能用C语言编写了成千上万的指令。必须创建一个过程,必须对该工具进行加载,初始化,然后清理,销毁该过程并等待。

调用cut是就像打开厨房抽屉一样,拿起刀子,使用它,清洗,干燥,再放回抽屉中。当您执行以下操作时:

while read line; do
  echo $line | cut -c3
done < file


就像文件的每一行一样,从厨房抽屉中获取read工具(非常笨拙,因为它不是为此而设计的) ),阅读一行,清洗阅读工具,然后将其放回抽屉中。然后为echocut工具安排一次会议,从抽屉中取出它们,调用它们,清洗它们,将它们干燥,然后将它们放回抽屉中,依此类推。

其中一些工具(readecho)内置在大多数外壳中,但是在这里几乎没有什么区别,因为echocut仍需要在单独的过程中运行。

就像切洋葱但要洗刀再放回去在每个切片之间的厨房抽屉中。

最明显的方法是从抽屉中取出cut工具,将整个洋葱切成薄片,待整个工作完成后再放回抽屉中。 />
IOW,在shell中,尤其是在处理文本时,您调用尽可能少的实用程序并使它们配合执行任务,而不是依次运行数千个工具来等待每个工具的启动,运行和清理运行下一个。

进一步阅读Bruce的正确答案。 Shell中的低级文本处理内部工具(可能除外zsh)是有限,笨重的,并且通常不适合常规文本处理。

性能

如前所述,运行一个命令会有一定的成本。如果该命令不是内置的,则成本很高,但是即使内置该命令,其成本也很高。

而且,shell还没有设计成可以像这样运行,它们没有成为高性能编程语言的幌子。它们不是,它们只是命令行解释器。因此,这方面的优化工作很少。

此外,shell在单独的进程中运行命令。这些构建块不共享公共内存或状态。当您在C语言中执行fgets()fputs()时,这就是stdio中的函数。 stdio为所有stdio函数保留用于输入和输出的内部缓冲区,以避免过于频繁地执行昂贵的系统调用。

相应的甚至内置的shell实用程序(readechoprintf)也无法做到这一点。 read只能读取一行。如果读取的字符超过了换行符,则意味着您运行的下一个命令将丢失该字符。因此,read必须一次读取一个字节(如果输入是常规文件,则某些实现会进行优化,因为它们可以读取大块并向后查找,但这仅适用于常规文件,而bash例如仅读取128个字节的块

在输出端,echo不能只是缓冲其输出,它必须立即将其输出,因为运行的下一个命令将直接输出它。不共享该缓冲区。

显然,顺序运行命令意味着您必须等待它们,这是一个调度程序之舞,它可以控制从Shell到工具再返回。这也意味着(与在管道中使用长时间运行的工具实例不同),您无法在可用时同时利用多个处理器。

while read循环和(应该是)等效的cut -c3 < file之间,在我的快速测试中,我的测试中的CPU时间比率约为40000(一秒对半天)。但是,即使仅使用shell内置函数:

while read line; do
  echo ${line:2:1}
done


(此处带有bash),仍然约为1:600(一秒钟vs 10分钟)。

可靠性/可读性

很难正确编写代码。我举的例子在野外经常看到,但它们有很多错误。

read是一个方便的工具,可以完成许多不同的事情。它可以读取用户的输入,将其分解为单词以存储在不同的变量中。 read line不读取输入行,或者它以非常特殊的方式读取行。实际上,它从输入中读取由$IFS分隔的单词,并在其中可以使用反斜杠来转义分隔符或换行符。

默认值为$IFS,在输入上如:

   foo\/bar \
baz
biz


read line会将"foo/bar baz"存储到$line中,而不是您期望的" foo\/bar \"中。

要阅读一行,您实际上需要:

IFS= read -r line


这不是很直观,但是就是这样,请记住,shell并不是要那样使用的。

echo相同。 echo扩展序列。您不能将其用于任意内容,例如随机文件的内容。您需要在这里改为printf

当然,通常会忘记引用每个人都喜欢的变量。因此,更多信息:

while IFS= read -r line; do
  printf '%s\n' "$line" | cut -c3
done < file


现在,有一些警告:


除了zsh,如果输入包含NUL字符,而至少GNU文本实用程序不会出现问题。
如果最后一个换行符之后有数据,则会在循环中跳过该数据,会重定向stdin,因此您需要注意注意其中的命令不会从stdin中读取。
对于循环中的命令,我们不会关注它们是否成功。通常,错误(磁盘已满,读取错误...)条件的处理较差,通常比使用正确的等效条件还要差。

如果我们要解决上述一些问题,则变为:

while IFS= read -r line <&3; do
  {
    printf '%s\n' "$line" | cut -c3 || exit
  } 3<&-
done 3< file
if [ -n "$line" ]; then
    printf '%s' "$line" | cut -c3 || exit
fi


这变得越来越难以理解。

通过参数将数据传递给命令或在变量中检索其输出还有许多其他问题:


参数大小的限制(一些文本实用程序的实现在那里也有一个限制,尽管达到这些效果通常不会出现问题)
NUL字符(文本实用程序也是一个问题)。
-(有时为+)开头时作为选项的参数
这些循环中通常使用的各种命令的各种怪癖,例如exprtest ...
各种shell的(有限)文本操作运算符
...

安全性考虑因素

当您开始使用shell变量和命令参数时,您正在输入一个雷区。

如果您忘记引用变量,请不要忘记选项标记的结尾,请在语言环境中工作使用多字节字符(这些天的规范),您肯定会引入迟早会成为漏洞的错误。

当您想使用循环时。

待定

评论


清晰(生动),易读且非常有用。再一次谢谢你。实际上,这是我在Internet上任何地方看到的关于Shell脚本与编程之间的根本差异的最好解释。

–通配符
15年10月24日在18:05

这些文章可帮助初学者了解Shell脚本并了解它们之间的细微差别。应该将引用变量添加为$ {VAR:-default_value}以确保您不会得到null。并设置-o名词设置以引用未定义的值时对您大吼大叫。

–unsignedzero
2015年12月4日在17:49

@ A.Danischewski,我认为您没有抓住重点。是的,例如削减是有效的。 cut -f1 <一个非常大的文件是有效的,就像用C编写时一样高效。非常低效且容易出错的是,为一个非常大的每一行调用cut -文件在shell循环中,这就是该答案的重点。这与您关于编写不必要的代码的最后声明是一致的,这使我认为也许我不理解您的评论。

–StéphaneChazelas
16年7月14日在17:17

“在45年的时间里,我们发现没有比该API更好的方法来利用命令的功能并使它们协作完成任务。” -实际上,PowerShell首先通过传递结构化数据而不是字节流解决了可怕的解析问题。 Shell仍不使用它的唯一原因(这个想法已经存在了很长时间,并且当现在标准的列表和字典容器类型成为主流时,它在Java的某个时候已经基本结晶了)是它们的维护者们尚未达成共识。常用的结构化数据格式(。

– ivan_pozdeev
16年11月11日在16:25

@OlivierDulac我觉得这有点幽默。该部分将永远待定。

–muru
18年5月13日在14:25

#2 楼

就概念和易读性而言,shell通常对文件感兴趣。它们的“可寻址单元”是文件,“地址”是文件名。 Shell具有各种测试文件存在性,文件类型,文件名格式(从globbing开始)的方法。 Shell具有很少的用于处理文件内容的原语。 Shell程序员必须调用另一个程序来处理文件内容。

由于文件和文件名的方向,在Shell中进行文本操作确实很慢,如您所注意到的,但是还需要一个不清楚且扭曲的编程风格。

#3 楼

有一些复杂的答案,为我们中间的怪人提供了许多有趣的细节,但这确实非常简单-在shell循环中处理大型文件太慢了。

我认为发问者是在典型的Shell脚本中很有趣,它可以从一些命令行解析,环境设置,检查文件和目录以及更多的初始化开始,然后再进行主要工作:浏览大型的面向行的文本文件。

对于第一个部分(initialization),shell命令通常很慢并不重要-它仅运行几十个命令,也许有几个短循环。
甚至如果我们以低效的方式编写该部分,通常只需不到一秒钟的时间即可完成所有初始化,这很好-它只会发生一次。

但是当我们继续处理大文件时,可能有数千行或数百万行,shell脚本占用很大一部分是不合适的每行只有一秒钟的时间(即使只有几十毫秒),因为这可能总计要花费数小时。

那是我们需要使用其他工具的时候,Unix shell脚本的优点是

而不是使用循环查看每一行,我们需要通过命令管道传递整个文件。
这样,shell只需调用一次即可,而不是数千或数百万次的调用。
确实,这些命令将具有循环逐行处理文件的循环,但是它们不是Shell脚本,因此它们

Unix具有许多出色的内置工具,从简单到复杂,我们可以使用它们来构建管道。我通常从简单的开始,并且仅在必要时使用更复杂的。

我也将尝试使用大多数系统上可用的标准工具,并尝试保持使用的可移植性,尽管这并不总是可行的。而且,如果您最喜欢的语言是Python或Ruby,也许您不会介意确保将其安装在您的软件需要在每个平台上运行的额外努力:-)

简单的工具包括headtailgrepsortcuttrsedjoin(合并2个文件时)和awk单行代码等。
令人惊讶的是,某些人可以使用模式匹配和sed命令来做。
/>
当它变得更加复杂,并且您真的必须对每行应用一些逻辑时,awk是一个不错的选择-单行(有些人将整个awk脚本放在“一行”中,尽管那是不太可读)或使用简短的外部脚本。

由于awk是一种解释型语言(例如您的shell),因此它能够如此高效地进行逐行处理是令人惊讶的,但它的目的是-为此而构建的,而且速度非常快。

然后是Perl和大量其他脚本不仅擅长处理文本文件的语言,而且还提供了许多有用的库。

最后,如果您需要最大的速度和高度的灵活性,那么可以使用旧的C语言(尽管文本处理有点麻烦。)
但是可能很浪费您的时间来为遇到的每个不同的文件处理任务编写新的C程序。
我经常处理CSV文件,所以我写了我可以在许多不同的项目中重复使用C中的几个通用实用程序。实际上,这扩大了我可以从Shell脚本调用的“简单,快速的Unix工具”的范围,因此我可以仅编写脚本来处理大多数项目,这比每次编写和调试定制的C代码要快得多! br />
一些最终提示:


不要忘了用export LANG=C启动主shell脚本,否则许多工具会将纯旧的ASCII文件视为Unicode,这会使它们变慢得多。
如果希望export LC_ALL=C产生一致的排序,也可以考虑设置sort,不管环境如何!
如果需要sort您的数据,那将比其他所有时间花费更多的时间(以及资源​​:CPU,内存,磁盘),因此,请尽量减少sort命令的数量和大小。在一个单一管道中排序它们的文件通常是最有效的-依次运行多个管道以及中间文件,这些文件可读性和调试性更高,但会增加程序花费的时间


评论


经常不必要地使用许多简单工具(特别是提到的工具,例如head,tail,grep,sort,cut,tr,sed等)的管道,特别是如果您在该管道中已经有一个awk实例可以做的话这些简单工具的任务也是如此。要考虑的另一个问题是,在管道中,您不能简单可靠地将状态信息从管道前侧的进程传递到后侧的进程。如果将此类简单程序的流水线用于awk程序,则您只有一个状态空间。

–贾尼斯(Janis)
2015年3月3日在5:54



#4 楼

是的,但是...

StéphaneChazelas的正确答案是基于将每个文本操作委派给特定二进制文件(例如grepawksed等)的shell概念。

由于bash能够自己完成很多事情,所以放下叉子可能会更快(甚至比运行另一个解释器来完成所有工作还容易)。

有关示例,请看一下这篇文章:

https://stackoverflow.com/a/38790442/1765658



https://stackoverflow.com/a/7180078/1765658

测试并比较...

当然

无需考虑用户输入和安全性!

不要在bash下编写Web应用程序!

但是对于许多服务器管理任务,可以使用bash代替Shell,使用内置的bash可能非常有效。

我的意思是:

像bin utils这样的编写工具与系统管理的工作不同。

所以不是同一个人!

系统管理员必须知道shell的地方,他们可以使用他偏爱的工具(并且是最著名的)来编写原型。

如果这个新实用工具(原型)真的有用,其他一些人可以使用一些更合适的语言来开发专用工具。

评论


好的例子。您的方法肯定比lololux方法更有效,但是请注意tensibai的答案(正确执行IMO的方法,即不使用shell循环)比您的方法快几个数量级。如果不使用bash,您的速度会更快。 (在我的系统上的测试中,使用ksh93的速度快3倍)。 bash通常是最慢的shell。甚至zsh在该脚本上的速度也要快两倍。您还遇到了一些不带引号的变量和read用法的问题。因此,您实际上在这里说明了我的很多观点。

–StéphaneChazelas
16年8月5日14:58



@StéphaneChazelas我同意,bash可能是人们今天可以使用的最慢的shell,但无论如何,它使用最广泛。

– F. Hauri
16年8月5日在16:10

@Tensibai,您会发现POSIX sh,Awk,Sed,grep,ed,ex,cut,sort,join ...比Bash或Perl更可靠。

–通配符
16年11月30日在9:48

在与U&L有关的所有系统中,@ Tensibai默认情况下未安装bash,其中大多数系统(Solaris,FreeBSD,HP / UX,AIX,大多数嵌入式Linux系统...)。 bash主要仅在Apple macOS和GNU系统上找到(我想这就是您所说的主要发行版),尽管许多系统也将其作为可选包(例如zsh,tcl,python ...)

–StéphaneChazelas
16年1月1日在16:56

@Stephane好,Cisco nexus确实也使用了bash,checkpoint,f5,当心,bluecoat(我确实称其为嵌入式系统),我在Linux上进行分布范围的确定,但是我几乎不记得没有bash的hp / ux或Aix,甚至是Aix仿真在os400内,我看到过广告。但是无论如何,重点是“比perl更有可能获得bash”,我完全同意保持posix兼容性应该是编写“便携式”代码时的主要目标。

–滕西拜
16年1月1日在17:51