是的,我们看到许多事情,例如:
while read line; do
echo $line | cut -c3
done
或更糟的是:
for line in `cat file`; do
foo=`echo $line | awk '{print $2}'`
echo whatever $foo
done
(别笑,我看过很多)。
通常来自shell脚本初学者。这些只是您在C或python等命令性语言中会做的天真的文字翻译,但这不是您在shell中做事的方式,并且这些示例效率很低,完全不可靠(可能导致安全问题),并且如果您曾经管理过,要修复大多数错误,您的代码将变得难以辨认。
从概念上讲
在C或大多数其他语言中,构造块仅比计算机指令高一级。您告诉处理器该做什么,然后告诉下一步。您可以用手拿住处理器并对其进行微管理:打开该文件,读取很多字节,然后执行此操作,并对其进行操作。
Shell是一种高级语言。有人可能说这甚至不是一门语言。它们位于所有命令行解释器之前。该工作由您运行的那些命令完成,而shell仅用于编排它们。
Unix引入的伟大的事情之一是管道和默认情况下所有命令都处理的默认stdin / stdout / stderr流。
在45年的时间里,我们发现没有比该API更好的方法来利用命令的功能并使它们协作完成任务。这可能是当今人们仍然使用shell的主要原因。
您拥有切割工具和音译工具,您可以简单地执行以下操作:
cut -c4-5 < in | tr a b > out
Shell只是在做管道(打开文件,设置管道,调用命令),当一切准备就绪时,它就在外壳没有做任何事情的情况下流动。这些工具可以按照自己的步调同时进行工作,并有足够的缓冲,以使它们不会相互阻塞,它们既美观又简单。
尽管调用工具需要付出一定的代价(我们将在性能点上进行开发)。这些工具可能用C语言编写了成千上万的指令。必须创建一个过程,必须对该工具进行加载,初始化,清理,销毁和等待过程。
调用cut
就像打开厨房抽屉,拿起刀子,使用,清洗,干燥,再放回抽屉中。当您这样做时:
while read line; do
echo $line | cut -c3
done < file
就像文件的每一行一样,read
从厨房抽屉中拿出工具(这很笨拙,因为它不是为此而设计的),读取一行,清洗读取的工具,然后将其放回抽屉中。然后为echo
和cut
工具安排一次会议,从抽屉中取出它们,调用它们,清洗它们,将它们干燥,然后将它们放回抽屉中,依此类推。
其中一些工具(read
和echo
)内置在大多数shell中,但这在这里几乎没有什么不同,因为echo
它cut
仍然需要在单独的进程中运行。
这就像切洋葱,但要洗刀,然后将其放回每片之间的厨房抽屉中。
在这里,最明显的方法是cut
从抽屉中取出工具,将整个洋葱切成薄片,然后在整个工作完成后将其放回抽屉中。
在外壳程序中,尤其是在处理文本时,IOW会调用尽可能少的实用程序并使它们配合任务,而不是依次运行数千个工具来等待每个工具启动,运行,清理,然后再运行下一个工具。
进一步阅读Bruce的正确答案。Shell中的低级文本处理内部工具(可能除外zsh
)是有限的,麻烦的,并且通常不适合常规文本处理。
性能
如前所述,运行一个命令是有代价的。如果该命令不是内置的,则成本很高,但是即使内置了该命令,代价也很大。
而且,shell还没有设计成可以像这样运行,它们没有成为高性能编程语言的幌子。它们不是,它们只是命令行解释器。因此,在这方面很少进行优化。
而且,shell在单独的进程中运行命令。这些构建块不共享公共内存或状态。当您在C中执行fgets()
或fputs()
时,这就是stdio中的函数。stdio为所有stdio函数保留用于输入和输出的内部缓冲区,以避免过于频繁地执行昂贵的系统调用。
相应的,甚至内置shell实用程序(read
,echo
,printf
)无法做到这一点。read
是要读一行。如果读取的字符超过了换行符,则意味着您运行的下一个命令将丢失该字符。因此read
必须一次读取一个字节的输入(如果输入是常规文件,则某些实现方案会进行优化,因为它们可以读取大块并进行查找,但这仅适用于常规文件,bash
例如只能读取128个字节的块,仍然比文本实用程序少很多)。
在输出端也是如此,echo
不能仅缓冲其输出,它必须立即将其输出,因为您运行的下一个命令将不会共享该缓冲区。
显然,顺序运行命令意味着您必须等待它们,这是一个小的调度程序,它可以使您从外壳到工具再返回进行控制。这也意味着(与在管道中使用长时间运行的工具实例相反),您无法在可用时同时利用多个处理器。
在我的快速测试中,在那个while read
循环和(应该是)等效循环之间,cut -c3 < file
我的测试中的CPU时间比率约为40000(一秒对半天)。但是,即使您仅使用shell内置函数:
while read line; do
echo ${line:2:1}
done
(此处带有bash
),仍然约为1:600(一秒钟对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字符(也是文本实用程序的问题)。
- 以
-
(或+
有时)开头的参数作为选项
- 通常在诸如这些循环使用的各种命令的各种怪癖
expr
,test
...
- 各种shell的(有限的)文本操作运算符,它们以不一致的方式处理多字节字符。
- ...
安全注意事项
当您开始使用shell 变量和command的参数时,您正在输入一个雷区。
如果您忘记引用变量,忘记选项标记的末尾,在具有多字节字符的语言环境中工作(如今已成为常态),那么您肯定会引入一些迟早会成为漏洞的错误。
当您可能想使用循环时。
待定
yes
这么快地写入文件?