Moh*_*mad 15 shell echo quoting cat command-substitution
回答这个问题让我提出了另一个问题:
我认为下面的脚本做同样的事情,第二个应该快得多,因为第一个使用cat
需要一遍又一遍地打开文件但第二个只打开文件一次,然后只回显一个变量:
(有关正确的代码,请参阅更新部分。)
第一的:
#!/bin/sh
for j in seq 10; do
cat input
done >> output
Run Code Online (Sandbox Code Playgroud)
第二:
#!/bin/sh
i=`cat input`
for j in seq 10; do
echo $i
done >> output
Run Code Online (Sandbox Code Playgroud)
而输入大约是 50 兆字节。
但是当我尝试第二个时,它太慢了,因为回显变量i
是一个巨大的过程。我也遇到了第二个脚本的一些问题,例如输出文件的大小低于预期。
我还检查了手册页echo
并cat
比较了它们:
echo - 显示一行文本
cat - 连接文件并在标准输出上打印
但我没有得到区别。
所以:
i
?(因为在它的手册页中
echo
说它显示“一行文本”,所以我猜它只针对短变量进行了优化,而不是像 那样非常长的变量i
。然而,这只是一个猜测。)echo
?我用seq 10
而不是`seq 10`
错误的。这是编辑后的代码:
第一的:
#!/bin/sh
for j in `seq 10`; do
cat input
done >> output
Run Code Online (Sandbox Code Playgroud)
第二:
#!/bin/sh
i=`cat input`
for j in `seq 10`; do
echo $i
done >> output
Run Code Online (Sandbox Code Playgroud)
(特别感谢roaima。)
然而,这不是问题的关键。即使循环只发生一次,我也会遇到同样的问题:cat
比echo
.
Sté*_*las 24
这里有几件事情需要考虑。
i=`cat input`
Run Code Online (Sandbox Code Playgroud)
可能很昂贵,而且外壳之间有很多变化。
这是一个称为命令替换的功能。这个想法是将命令的整个输出减去尾随换行符i
存储到内存中的变量中。
为此,shell 在子shell 中分叉命令并通过管道或套接字对读取其输出。你在这里看到了很多变化。在此处的 50MiB 文件中,我可以看到例如 bash 比 ksh93 慢 6 倍,但比 zsh 稍快,比yash
.
bash
缓慢的主要原因是它一次从管道中读取 128 个字节(而其他 shell 一次读取 4KiB 或 8KiB)并且受到系统调用开销的惩罚。
zsh
需要做一些后处理来转义 NUL 字节(其他 shell 在 NUL 字节上中断),并yash
通过解析多字节字符进行更繁重的处理。
所有 shell 都需要去除尾随的换行符,这可能或多或少地有效。
有些人可能希望比其他人更优雅地处理 NUL 字节并检查它们的存在。
然后,一旦您在内存中拥有那个大变量,对其进行的任何操作通常都涉及分配更多内存和处理数据。
在这里,您将(打算传递)变量的内容传递给echo
。
幸运的是,echo
它内置在您的 shell 中,否则执行可能会因arg list too long错误而失败。即便如此,构建参数列表数组也可能涉及复制变量的内容。
命令替换方法中的另一个主要问题是您正在调用split+glob 运算符(忘记引用变量)。
为此,shell 需要将字符串视为一串字符(尽管有些 shell 没有并且在这方面有问题),因此在 UTF-8 语言环境中,这意味着解析 UTF-8 序列(如果尚未完成yash
) ,$IFS
在字符串中查找字符。如果$IFS
包含空格、制表符或换行符(默认情况下是这种情况),则算法更加复杂和昂贵。然后,需要分配和复制由该拆分产生的单词。
glob 部分将更加昂贵。如果这些单词中的任何一个包含 glob 字符(*
, ?
, [
),那么 shell 将不得不读取某些目录的内容并进行一些昂贵的模式匹配(bash
例如, 的实现在这方面非常糟糕)。
如果输入包含类似的内容/*/*/*/../../../*/*/*/../../../*/*/*
,那将非常昂贵,因为这意味着列出数千个目录并且可以扩展到数百 MiB。
然后echo
通常会做一些额外的处理。一些实现\x
在它接收的参数中扩展序列,这意味着解析内容以及可能的另一个数据分配和副本。
另一方面,好的,在大多数 shellcat
中不是内置的,这意味着分叉一个进程并执行它(因此加载代码和库),但是在第一次调用之后,该代码和输入文件的内容将缓存在内存中。另一方面,将没有中介。cat
将一次读取大量内容并立即写入而无需处理,并且不需要分配大量内存,只需重用一个缓冲区即可。
这也意味着它更可靠,因为它不会阻塞 NUL 字节并且不会修剪尾随的换行符(并且不会执行 split+glob,尽管您可以通过引用变量来避免这种情况,并且不会展开转义序列,尽管您可以通过使用printf
代替echo
)来避免这种情况。
如果你想进一步优化它,而不是cat
多次调用,只需input
向cat
.
yes input | head -n 100 | xargs cat
Run Code Online (Sandbox Code Playgroud)
将运行 3 个命令而不是 100 个。
为了使变量版本更可靠,您需要使用zsh
(其他外壳无法处理 NUL 字节)并执行以下操作:
zmodload zsh/mapfile
var=$mapfile[input]
repeat 10 print -rn -- "$var"
Run Code Online (Sandbox Code Playgroud)
如果您知道输入不包含 NUL 字节,那么您可以可靠地执行 POSIXly(尽管它可能无法在printf
未内置的情况下工作):
i=$(cat input && echo .) || exit # add an extra .\n to avoid trimming newlines
i=${i%.} # remove that trailing dot (the \n was removed by cmdsubst)
n=10
while [ "$n" -gt 10 ]; do
printf %s "$i"
n=$((n - 1))
done
Run Code Online (Sandbox Code Playgroud)
但这永远不会比cat
在循环中使用更有效(除非输入非常小)。
cuo*_*glm 11
问题不在于cat
and echo
,而在于被遗忘的引用变量$i
。
在类似 Bourne 的 shell 脚本中(除了zsh
),不加引号glob+split
的变量会导致变量上的运算符。
$var
Run Code Online (Sandbox Code Playgroud)
实际上是:
glob(split($var))
Run Code Online (Sandbox Code Playgroud)
因此,在每次循环迭代中,input
(不包括尾随换行符)的全部内容都将被扩展、拆分、通配。整个过程需要shell来分配内存,一次又一次地解析字符串。这就是你表现不佳的原因。
您可以引用该变量来防止,glob+split
但它不会对您有多大帮助,因为当 shell 仍然需要构建大字符串参数并扫描其内容时echo
(echo
用外部替换内置/bin/echo
将使您的参数列表太长或内存不足取决于$i
大小)。大多数echo
实现不符合 POSIX,它会\x
在收到的参数中扩展反斜杠序列。
使用cat
,shell 只需在每次循环迭代中生成一个进程,cat
并执行复制 i/o。系统还可以缓存文件内容,使 cat 进程更快。