我试图了解 Bash 究竟如何处理以下行:
$(< "$FILE")
根据 Bash 手册页,这相当于:
$(cat "$FILE")
我可以遵循第二行的推理思路。 Bash 在 上执行变量扩展,输入命令替换,传递to$FILE
的值,cat 将 的内容输出到标准输出,命令替换通过用内部命令产生的标准输出替换整行来完成,Bash 尝试像这样执行它一个简单的命令。$FILE
cat
$FILE
然而,对于我上面提到的第一行,我将其理解为:Bash对 执行变量替换$FILE
,Bash打开$FILE
以在标准输入上读取,以某种方式将标准输入复制到标准输出,命令替换完成,Bash 尝试执行生成的标准输出。
有人可以向我解释一下内容是如何$FILE
从标准输入到标准输出的吗?
答案1
$(<file)
(在 ksh93 中也与`<file`
以及 一起使用${<file;}
)是 Korn shell 的一个特殊操作符,由zsh
和复制bash
。它看起来很像命令替换,但实际上不是。
在 POSIX shell 中,一个简单的命令是:
< file var1=value1 > file2 cmd 2> file3 args 3> file4
所有部分都是可选的,您可以仅重定向、仅命令、仅分配或组合。
如果有重定向但没有命令,则会执行重定向(因此 a> file
将打开并截断file
),但随后什么也不会发生。所以
< file
打开file
以供阅读,但由于没有命令,所以什么也没有发生。然后file
就关闭了,就是这样。如果$(< file)
是一个简单的命令替换,那么它就会扩展为空。
在里面POSIX规范, 在$(script)
, 如果script
仅包含重定向,则产生未指定的结果。这是为了允许 Korn shell 的特殊行为。
在 ksh 中(这里测试了ksh93u+
),如果脚本包含一个且仅一个简单的命令(尽管前后允许注释)仅包含重定向(无命令,无赋值),并且如果第一个重定向是 stdin (fd 0) 仅输入(<
、<<
或<<<
)重定向,则:
$(< file)
$(0< file)
$(<&3)
($(0>&3)
实际上也因为这实际上是同一个运算符)$(< file > foo 2> $(whatever))
但不是:
$(> foo < file)
- 也不
$(0<> file)
- 也不
$(< file; sleep 1)
- 也不
$(< file; < file2)
然后
- 除了第一个重定向之外的所有重定向都被忽略(它们被解析掉)
- 它扩展到 file/heredoc/herestring 的内容(或者如果使用类似的东西可以从文件描述符中读取的任何内容
<&3
)减去尾随换行符。
就好像使用$(cat < file)
except that
- 读取是由 shell 内部完成的,而不是由
cat
- 不涉及管道或额外过程
- 由于上述原因,由于内部代码不在子 shell 中运行,因此此后保留任何修改(如
$(<${file=foo.txt})
或$(<file$((++n)))
) - 读取错误(尽管不是打开文件或复制文件描述符时出现的错误)会被默默地忽略。
在 中zsh
,它是相同的,只是只有当只有一个文件输入重定向时才会触发特殊行为(<file
或0< file
、否<&3
、<<<here
... < a < b
)
但是,除了模拟其他 shell 时,即< file
当只有一个没有命令的输入重定向时,在命令替换之外,zsh
运行$READNULLCMD
(默认情况下为寻呼机),并且当存在更多重定向或除< file
( <&3
, <<<text
, <a <b
, >file
, <a >b
. ..)$NULLCMD
(cat
默认情况下),因此即使$(<&3)
不被识别为该特殊运算符,它仍然会像ksh
通过调用cat
来执行它一样工作。
然而 while ksh
's将扩展为, in$(< a < b)
的内容,它扩展为 的内容a
zsh
a
和b
(或者只是在禁用b
该选项的情况下),将复制并扩展为空,等等。multios
$(< a > b)
a
b
bash
有一个类似的运算符,但有一些区别:
允许在以下内容之前发表评论,但不允许在以下内容之后发表评论:
echo "$( # getting the content of file < file)"
有效,但是:
echo "$(< file # getting the content of file )"
膨胀到什么都没有。
与 中一样
zsh
,只有一个文件 stdin 重定向,尽管没有回退到 a$READNULLCMD
,因此$(<&3)
,$(< a < b)
执行重定向但扩展为空。由于某种原因,虽然
bash
不调用cat
,但它仍然分叉一个进程,通过管道提供文件内容,使其比其他 shell 的优化要少得多。它实际上就像一个内置的$(cat < file)
where 。cat
cat
由于上述原因,其中所做的任何更改都会随后丢失(
$(<${file=foo.txt})
例如,在上面提到的 中,该$file
分配随后会丢失)。
在bash
, IFS= read -rd '' var < file
(也适用于zsh
)是阅读内容的更有效方法文本文件到变量中。它还具有保留尾随换行符的优点。另请参阅(在$mapfile[file]
模块中且仅适用于常规文件),它也适用于二进制文件。zsh
zsh/mapfile
请注意,与 ksh93 相比,基于 pdksh 的变体ksh
有一些变化。有趣的是,在mksh
(那些 pdksh 派生的 shell 之一)中,
var=$(<<'EOF'
That's multi-line
test with *all* sorts of "special"
characters
EOF
)
进行了优化,因为此处文档的内容(没有尾随换行符)在不使用临时文件或管道的情况下进行扩展,而此处文档的情况则如此,这使其成为有效的多行引用语法。
为了可移植到ksh
、zsh
和的所有版本bash
,最好仅限于$(<file)
避免注释,并记住对变量所做的修改可能会也可能不会被保留。
答案2
因为bash
它是在内部为您执行的,扩展了文件名并将文件转换为标准输出,就像您要做的那样$(cat < filename)
。这是一个 bash 功能,也许您需要查看bash
源代码才能确切知道它是如何工作的。
这里是处理此功能的函数(来自bash
源代码,文件builtins/evalstring.c
):
/* Handle a $( < file ) command substitution. This expands the filename,
returning errors as appropriate, then just cats the file to the standard
output. */
static int
cat_file (r)
REDIRECT *r;
{
char *fn;
int fd, rval;
if (r->instruction != r_input_direction)
return -1;
/* Get the filename. */
if (posixly_correct && !interactive_shell)
disallow_filename_globbing++;
fn = redirection_expand (r->redirectee.filename);
if (posixly_correct && !interactive_shell)
disallow_filename_globbing--;
if (fn == 0)
{
redirection_error (r, AMBIGUOUS_REDIRECT);
return -1;
}
fd = open(fn, O_RDONLY);
if (fd < 0)
{
file_error (fn);
free (fn);
return -1;
}
rval = zcatfd (fd, 1, fn);
free (fn);
close (fd);
return (rval);
}
$(<filename)
不完全等同于的注释$(cat filename)
;如果文件名以破折号开头,后者将失败-
。
$(<filename)
最初是 from ksh
,并添加到bash
from Bash-2.02
。
答案3
这是一个 bash 3.2 片段,显示了差异,并进行了解释:
- 使用 strace 跟踪进程并显示 execve 调用
strace -f -e trace=execve
- 运行 bash 从字符串读取命令
bash -c
- 有或没有/bin/cat
- 将并排模式下的输出区分为 80 列以适合此处
diff -y -W 80
您可以execve(/bin/cat...)
在差异右侧看到额外的内容:
$ echo $BASH_VERSION
3.2.25(1)-release
$ echo "hi" >/tmp/f
$ strace -f -e trace=execve /bin/bash -c 'echo $(</tmp/f)' >/tmp/no_cat 2>&1
$ strace -f -e trace=execve /bin/bash -c 'echo $(/bin/cat </tmp/f)' >/tmp/wi_cat 2>&1
$ diff -y -W 80 /tmp/no_cat /tmp/wi_cat
execve("/bin/bash", ["/bin/bash", "-c | execve("/bin/bash", ["/bin/bash", "-c
Process 24253 attached (waiting for p | Process 24256 attached (waiting for p
Process 24253 resumed (parent 24252 r | Process 24256 resumed (parent 24255 r
Process 24253 detached | Process 24257 attached (waiting for p
> Process 24257 resumed (parent 24256 r
> Process 24256 suspended
> [pid 24257] execve("/bin/cat", ["/bin
> Process 24256 resumed
> Process 24257 detached
> [pid 24256] --- SIGCHLD (Child exited
> Process 24256 detached
--- SIGCHLD (Child exited) @ 0 (0) -- --- SIGCHLD (Child exited) @ 0 (0) --
hi hi
答案4
这<
不是直接的一个方面bash 命令替换。它是一个重定向运算符(如管道),某些 shell 允许在没有命令的情况下使用它(POSIX 未指定此行为)。
也许用更多的空间会更清楚:
echo $( < $FILE )
这是有效地* 与更POSIX安全的相同
echo $( cat $FILE )
...这也有效*
echo $( cat < $FILE )
让我们从最后一个版本开始。它cat
不带参数运行,这意味着它将从标准输入读取。 $FILE
由于 被重定向到标准输入<
,因此cat
将其内容放入标准输出。$(command)
然后,替换将 的输出推入cat
的参数中echo
。
在bash
(但不是在 POSIX 标准中),您可以<
不使用命令来使用。 bash
(andzsh
和ksh
but not dash
) 会将其解释为 if cat <
,但不会调用新的子流程。由于这是 shell 本机的,因此它比直接运行外部命令更快cat
。 *这就是为什么我说“实际上相同”。