了解 Bash 的读取文件命令替换

了解 Bash 的读取文件命令替换

我试图了解 Bash 究竟如何处理以下行:

$(< "$FILE")

根据 Bash 手册页,这相当于:

$(cat "$FILE")

我可以遵循第二行的推理思路。 Bash 在 上执行变量扩展,输入命令替换,传递to$FILE的值,cat 将 的内容输出到标准输出,命令替换通过用内部命令产生的标准输出替换整行来完成,Bash 尝试像这样执行它一个简单的命令。$FILEcat$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,它是相同的,只是只有当只有一个文件输入重定向时才会触发特殊行为(<file0< file、否<&3<<<here... < a < b

但是,除了模拟其他 shell 时,即< file 当只有一个没有命令的输入重定向时,在命令替换之外,zsh运行$READNULLCMD(默认情况下为寻呼机),并且当存在更多重定向或除< file( <&3, <<<text, <a <b, >file, <a >b. ..)$NULLCMDcat默认情况下),因此即使$(<&3)不被识别为该特殊运算符,它仍然会像ksh通过调用cat来执行它一样工作。

然而 while ksh's将扩展为, in$(< a < b)的内容,它扩展为 的内容azsha b(或者只是在禁用b该选项的情况下),将复制并扩展为空,等等。multios$(< a > b)ab

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 。catcat

  • 由于上述原因,其中所做的任何更改都会随后丢失($(<${file=foo.txt})例如,在上面提到的 中,该$file分配随后会丢失)。

bash, IFS= read -rd '' var < file (也适用于zsh)是阅读内容的更有效方法文本文件到变量中。它还具有保留尾随换行符的优点。另请参阅(在$mapfile[file]模块中且仅适用于常规文件),它也适用于二进制文件。zshzsh/mapfile

请注意,与 ksh93 相比,基于 pdksh 的变体ksh有一些变化。有趣的是,在mksh(那些 pdksh 派生的 shell 之一)中,

var=$(<<'EOF'
That's multi-line
test with *all* sorts of "special"
characters
EOF
)

进行了优化,因为此处文档的内容(没有尾随换行符)在不使用临时文件或管道的情况下进行扩展,而此处文档的情况则如此,这使其成为有效的多行引用语法。

为了可移植到kshzsh和的所有版本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,并添加到bashfrom 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(andzshkshbut not dash) 会将其解释为 if cat <,但不会调用新的子流程。由于这是 shell 本机的,因此它比直接运行外部命令更快cat*这就是为什么我说“实际上相同”。

相关内容