前段时间,我发布了一些有关脚本编写问题的答案。有人指出我不应该使用以下命令:
for x in $(cat file); do something; done
但取而代之的是:
while read f; do something; done < file
猫的无用使用文章本想解释整个问题,但唯一的解释是:
反引号是非常危险的,除非您知道反引号的结果将小于或等于 shell 可以接受的命令行长度。 (实际上,这是一个内核限制。limits.h 中的常量 ARG_MAX 应该告诉您您自己的系统可以占用多少。POSIX 要求 ARG_MAX 至少为 4,096 字节。)
如果我正确地理解了这一点,如果我在命令中使用非常大的文件的输出(它应该超过limits.h文件中定义的ARG_MAX),bash(?)应该崩溃。所以我用命令检查了 ARG_MAX:
> grep ARG_MAX /usr/src/kernels/$(uname -r)/include/uapi/linux/limits.h
#define ARG_MAX 131072 /* # bytes of args + environ for exec() */
然后我创建了包含没有空格的文本的文件:
> ls -l
-rw-r--r--. 1 root root 100000000 Aug 21 15:37 in_file
然后我运行:
for i in $(cat in_file); do echo $i; done
啊啊,并没有发生什么可怕的事情。
那么我应该怎么做来检查整个“不要将猫与循环一起使用”是否/如何危险?
答案1
这取决于file
要包含的内容。如果它意味着包含 IFS 分隔的 shell 全局列表,例如(假设默认值为$IFS
):
/var/log/*.log /var/adm/*~
/some/dir/*.txt
那么这for i in $(cat file)
就是要走的路。这就是未加引号的作用:在删除其尾随换行符$(cat file)
的输出上应用 split+glob 运算符。cat file
因此,它将循环遍历这些 glob 扩展所产生的每个文件名(除非 glob 与任何文件不匹配,否则会将 glob 保留在那里但未扩展)。
如果您想循环 的每个分隔行file
,您可以这样做:
while IFS= read -r line <&3; do
{
something with "$line"
} 3<&-
done 3< file
通过for
循环,您可以使用以下命令循环遍历每个非空行:
IFS=' ' # 仅在换行符上分割(实际上是换行符序列和 # 忽略前导和尾随,因为换行符是 #IFS 空白字符) set -o noglob # 禁用全局split+glob 运算符的一部分: 对于 $(cat 文件) 中的行;做 带有“$line”的东西 完毕
然而:
while read line; do
something with "$line"
done < file
没什么意义。那是file
以非常复杂的方式阅读内容其中$IFS
和反斜杠字符经过特殊处理。
在任何情况下,您引用的文本所指的 ARG_MAX 限制是在execve()
系统调用上(关于参数和环境变量的累积大小),因此仅适用于正在使用可能的文件系统上的命令执行的情况应用于命令替换的 split+glob 运算符的非常长的扩展(该文本在多个帐户上具有误导性和错误性)。
例如,它适用于:
cat -- $(cat file) # with shell implementations where cat is not builtin
但不在:
for i in $(cat file)
不execve()
涉及系统调用的地方。
比较:
bash-4.4$ echo '/*/*/*/*' > file
bash-4.4$ true $(cat file)
bash-4.4$ n=0; for f in $(cat file); do ((n++)); done; echo "$n"
523696
bash-4.4$ /bin/true $(cat file)
bash: /bin/true: Argument list too long
bash
使用 的true
内置命令或循环可以for
,但执行时不行/bin/true
。请注意,只有 9 个字节大,但由于shell 正在扩展 glob,因此file
扩展$(cat file)
了几兆字节。/*/*/*/*
更多阅读:
答案2
@切普纳解释了评论中的差异:
for i in $(cat in_file)
不会迭代文件的行,它会迭代由于文件内容经过分词和路径名扩展而产生的单词。
为了对性能和资源使用的影响,我使用 1M 行(约 19M)的输入对这两种情况做了一个小型基准测试,并使用以下方法测量时间和内存使用情况/usr/bin/time -v
:
测试1.sh:
#!/bin/bash
while read x
do
echo $x > /dev/null
done < input
结果:
Command being timed: "./test1.sh"
User time (seconds): 12.41
System time (seconds): 2.03
Percent of CPU this job got: 110%
Elapsed (wall clock) time (h:mm:ss or m:ss): 0:13.07
Maximum resident set size (kbytes): 3088
测试2.sh:
#!/bin/bash
for x in $(cat input)
do
echo $x > /dev/null
done
结果:
Command being timed: "./test2.sh"
User time (seconds): 17.19
System time (seconds): 3.13
Percent of CPU this job got: 109%
Elapsed (wall clock) time (h:mm:ss or m:ss): 0:18.51
Maximum resident set size (kbytes): 336356
我已将两个测试的完整输出上传到帕斯特宾。使用 bash 使用for i in $(cat ...)
会占用更多内存并且运行速度也会变慢。但是,结果可能会有所不同,具体取决于您是否在其他 shell 上运行这些相同的测试。
答案3
while
循环可能会出现问题,最明显的是它们默认使用标准输入(因此ssh -n
),因此如果您需要其他东西的标准输入,则while
循环将失败
$ find . -name "*.pm" | while read f; do aspell check $f; done
$
不执行任何操作,而是aspell
需要一个终端,该终端被 Perl 模块名称列表占据;循环for
更合适(假设文件名不会被 POSIX 分词规则分割):
$ for f in $(find . -name \*.pm); do aspell check $f; done
...
while
因为它不像默认情况下那样使用标准输入。
此外,while
很容易出现静默数据丢失(并且for
对于相同的输入,其行为有所不同):
$ echo -n mmm silent data loss | while read line; do echo $line; done
$ for i in $(echo -n mmm silent data loss); do echo $i; done
mmm
silent
data
loss
$
while
因此,根据上下文,可以提出危险且不应使用的论点。