如果要将系统命令的单行输出读入 Bash shell 变量,则至少有两个选择,如下例所示:
IFS=: read user x1 uid gid x2 home shell <<<$(grep :root: /etc/passwd | head -n1)
和
IFS=: read user x1 uid gid x2 home shell < <(grep :root: /etc/passwd | head -n1)
这两者有什么区别吗?什么更有效或推荐?
请注意,阅读该/etc/passwd
文件只是为了举例。我的问题的焦点是这里是字符串与流程替代。
答案1
首先请注意,使用read
without-r
是为了处理输入,其中\
用于转义字段或行分隔符,而 则不是这样/etc/passwd
。您很少想read
不使用-r
.
现在,对于这两种形式,请注意,它们都不是标准sh
语法。<<<
来自zsh
1991 年。<(...)
来自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 中运行,因此如果cmd1
是read
,read
则只会填充该子 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=1
grep
grep
sed '/:root:/!d;q'