在 Bash 中将命令的输出读取到变量中更有效或推荐的方法是什么?

在 Bash 中将命令的输出读取到变量中更有效或推荐的方法是什么?

如果要将系统命令的单行输出读入 Bash shell 变量,则至少有两个选择,如下例所示:

  1. IFS=: read user x1 uid gid x2 home shell <<<$(grep :root: /etc/passwd | head -n1)

  1. IFS=: read user x1 uid gid x2 home shell < <(grep :root: /etc/passwd | head -n1)

这两者有什么区别吗?什么更有效或推荐?


请注意,阅读该/etc/passwd文件只是为了举例。我的问题的焦点是这里是字符串流程替代

答案1

首先请注意,使用readwithout-r是为了处理输入,其中\用于转义字段或行分隔符,而 则不是这样/etc/passwd。您很少想read不使用-r.

现在,对于这两种形式,请注意,它们都不是标准sh语法。<<<来自zsh1991 年。<(...)来自ksh大约 1985 年,尽管ksh最初不支持从/到它的重定向。

$(...)也来自 ksh,但已由 POSIX 标准化(因为它取代了`...`Bourne shell 的设计不良),因此sh现在可以跨实现移植。

$(code)解释子 shell 中的代码,并将输出重定向到管道,同时父 shell 从管道的另一端读取该输出并将其存储在内存中。然后,一旦该命令完成,该输出将删除尾随换行符(并删除 中的 NUL 字符bash),从而构成 的扩展$(...)

如果它$(...)没有被引用并且在列表上下文中,那么它会受到 split+glob 的影响(仅在 zsh 中拆分)。之后<<<,它不是列表上下文,但仍然旧版本bash仍会执行分割部分(不是全局),然后用空格将各部分连接起来。因此,如果使用bash,您可能还希望$(...)在用作 的目标时引用<<<

cmd <<< word在 zsh 和旧版本的 bash 中,shell 将word后跟换行符存储到临时文件中,然后将其作为将要执行的进程的标准输入,并执行cmd之前删除的临时文件。cmd<< EOF与 70 年代的 Bourne shell 中发生的情况相同。实际上,它与以下完全相同:

cmd << EOF
word
EOF

在 5.1 中,bash 从使用临时文件切换到使用管道,只要单词可以整个放入管道缓冲区(如果不是为了避免死锁,则回退到使用临时文件)并使cmd's stdin成为外壳已预先用 播种的管道word

因此cmd1 <<< "$(cmd2)"涉及一个或两个管道,将整个输出存储cmd2在内存中,再次将其存储在另一个管道或临时文件中,并破坏 NUL 和换行符。

cmd1 < <(cmd2)功能相当于cmd2 | cmd1.cmd2的输出连接到管道的写入端。然后<(...)扩展到标识另一端的路径,< that-path为您提供该另一端的文件描述符。因此,无需 shell 对数据执行任何操作,即可cmd2直接进行对话。cmd1

您会在 shell 中看到这种构造,bash特别是因为在 中bash,与 AT&T ksh 或 zsh 相反,在:

cmd2 | cmd1

cmd1在子 shell 中运行,因此如果cmd1readread则只会填充该子 shell 的变量。

所以在这里,你会想要:

IFS=: read -r user x1 uid gid x2 home shell rest_if_any_ignored < <(
  grep :root: /etc/passwd)

head与 一样是多余的,-r无论如何read只会读取一行²。我添加了一个rest_if_any_ignored以供将来校对,以防将来添加新字段/etc/passwd,从而导致$shell包含/bin/sh:that-field其他内容。

可移植(在sh)中,你不能这样做:

grep :root: /etc/passwd |
  IFS=: read -r user x1 uid gid x2 home shell rest_if_any_ignored 

因为 POSIX 未指定是否read在子 shell 中运行(如bash/ dash...)或不在子 shell 中运行(如zsh/ ksh)。

但是您可以这样做:

IFS=: read -r user x1 uid gid x2 home shell rest_if_any_ignored << EOF
$(grep :root: /etc/passwd | head -n1)
EOF

(这里恢复head以避免整个grep输出存储在内存和临时文件/管道中)。

即使效率不高,这也是标准的(尽管正如 @muru 所指出的,与在分叉进程中运行外部实用程序的成本相比,如此小的输入的差异可能可以忽略不计)。

如果性能在这里很重要,则可以通过使用 shell 的内置功能来完成grep工作来提高性能。然而,特别是在 中bash,您只能对非常小的输入执行此操作,因为 shell 不是为此类任务设计的,并且在这方面比grep.

while
  IFS=: read <&3 -r user x1 uid gid name home shell rest_if_any_ignored
do
  if [ "$name" = root ]; then
    do-something-with "$user" "$home"...
    break
  fi
done 3< /etc/passwd

¹ 除非设置了lastpipe选项bash并且 shell 与脚本中一样是非交互式的

² 另请参阅GNU 实现的-m1或选项,它会告诉自己在第一个匹配后停止搜索。或便携式等效项:--max-count=1grepgrepsed '/:root:/!d;q'

答案2

这里的字符串,bash 读取命令替换 ( $(grep :root: /etc/passwd | head -n1)) 的整个输出以创建此处字符串的内容,然后使用 告诉它再次读取它read

另一方面,与流程替代,bash 设置一个管道,然后读取输出一次


您正在运行 bash(以及其他两个外部命令)来读取一行内容。到那时,效率早已被淘汰。


当我们还在做的时候,GNUgrep有一个-m选项:

-m 编号
--max-count=编号

在第一个之后停止编号选定的线路。

相关内容