了解 Bash 的 Read-a-File 命令替换

Sta*_* Yu 14 shell bash io-redirection command-substitution

我试图了解 Bash 是如何处理以下行的:

$(< "$FILE")
Run Code Online (Sandbox Code Playgroud)

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

$(cat "$FILE")
Run Code Online (Sandbox Code Playgroud)

我可以按照第二行的推理思路。Bash 对 执行变量扩展$FILE,进入命令替换,传递$FILEto的值cat,cat 将内容$FILE输出到标准输出,命令替换通过用内部命令产生的标准输出替换整行来完成,Bash 尝试执行它一个简单的命令。

但是,对于我上面提到的第一行,我理解为:Bash 在 上执行变量替换$FILE,Bash 打开$FILE以读取标准输入,以某种方式将标准输入复制到标准输出,命令替换完成,并且 Bash 尝试执行生成的标准输出。

有人可以向我解释如何$FILE从标准输入到标准输出的内容吗?

Sté*_*las 17

$(<file)(也适用于`<file`) 是由zsh和复制的 Korn shell 的特殊运算符bash。它看起来很像命令替换,但实际上并非如此。

在 POSIX shell 中,一个简单的命令是:

< file var1=value1 > file2 cmd 2> file3 args 3> file4
Run Code Online (Sandbox Code Playgroud)

所有部分都是可选的,您可以仅重定向、仅命令、仅分配或组合。

如果有重定向但没有命令,则执行重定向(因此 a> file将 open 和 truncate file),但没有任何反应。所以

< file
Run Code Online (Sandbox Code Playgroud)

打开file以供阅读,但由于没有命令,因此什么也没有发生。所以file然后关闭,就是这样。如果$(< file)是一个简单的命令替换,那么它将扩展为空。

POSIX 规范$(script),如果script只包含重定向,则产生未指定的结果。这是为了允许 Korn shell 的这种特殊行为。

在 ksh 中(此处使用 测试ksh93u+),如果脚本由一个且仅由一个简单命令组成(尽管前后允许注释)且仅由重定向(无命令,无赋值)组成,并且第一个重定向是标准输入(fd 0) 仅输入 ( <,<<<<<) 重定向,因此:

  • $(< file)
  • $(0< file)
  • $(<&3)$(0>&3)实际上也是同一个运营商)
  • $(< file > foo 2> $(whatever))

但不是:

  • $(> foo < file)
  • 也不 $(0<> file)
  • 也不 $(< file; sleep 1)
  • 也不 $(< file; < file2)

然后

  • 除了第一个重定向之外的所有内容都被忽略(它们被解析掉)
  • 它扩展到文件/heredoc/herestring 的内容(或者任何可以从文件描述符中读取的内容,如果使用类似的东西<&3)减去尾随的换行符。

好像使用$(cat < file)除了那个

  • 读取是由外壳内部完成的,而不是由 cat
  • 不涉及管道或额外的过程
  • 由于上述原因,由于里面的代码不在子shell中运行,任何修改都保留在此之后(如$(<${file=foo.txt})$(<file$((++n)))
  • 读取错误(虽然不是打开文件或复制文件描述符时的错误)被静默忽略。

zsh,它的不同之处在于,当只有一个文件输入重定向特殊行为时,才会触发相同(<file0< file没有<&3<<<here< a < b...)

但是,除了在模拟其他 shell 时,在:

< file
<&3
<<< here...
Run Code Online (Sandbox Code Playgroud)

也就是说,当只有没有命令的输入重定向时,在命令替换之外,zsh运行$READNULLCMD(默认情况下为寻呼机),并且当有输入和输出重定向时,$NULLCMDcat默认情况下),因此即使$(<&3)不被识别为特殊操作符,它仍然会像 in 一样工作,ksh但通过调用寻呼机来完成它(寻呼机的行为就像cat它的标准输出将是一个管道)。

然而,虽然ksh's$(< a < b)会扩展到a, in的内容zsh,它会扩展到aand的内容b(或者只是b如果该multios选项被禁用),$(< a > b)会复制ab并扩展为空等。

bash 有一个类似的操作符,但有一些不同:

  • 允许评论之前但不允许评论:

    echo "$(
       # getting the content of file
       < file)"
    
    Run Code Online (Sandbox Code Playgroud)

    有效但是:

    echo "$(< file
       # getting the content of file
    )"
    
    Run Code Online (Sandbox Code Playgroud)

    扩展到无。

  • 像 in 一样zsh,只有一个文件 stdin 重定向,虽然没有回退到 a $READNULLCMD,因此$(<&3)$(< a < b)执行重定向但扩展为空。

  • 出于某种原因,虽然bash不调用cat,但它仍然分叉一个进程,通过管道提供文件内容,使其比其他shell 中的优化少得多。它实际上就像一个$(cat < file)wherecat将是一个 builtin cat
  • 由于上述原因,之后所做的任何更改都会丢失($(<${file=foo.txt})例如,在上面提到的 中,该$file分配随后丢失)。

bash, IFS= read -rd '' var < file (也适用于zsh)是将文本文件的内容读入变量的更有效方法。它还具有保留尾随换行符的好处。另请参见$mapfile[file]zsh(中zsh/mapfile模块以及仅适用于常规文件),这也与二进制文件的工作。

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

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

进行了优化,因为 here 文档的内容(没有尾随字符)在没有使用临时文件或管道的情况下扩展,就像 here 文档的情况一样,这使其成为有效的多行引用语法。

要可移植到ksh,zsh和 的所有版本bash,最好限制为仅$(<file)避免注释,并记住对其中所做的变量的修改可能会或可能不会保留。


cuo*_*glm 8

因为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);
}
Run Code Online (Sandbox Code Playgroud)

$(<filename)不完全等同于的注释$(cat filename);如果文件名以破折号开头,后者将失败-

$(<filename)最初来自ksh,并添加到bash来自Bash-2.02


Ada*_*atz -4

<并不直接是bash 命令替换的一个方面。它是一个重定向运算符(如管道),某些 shell 允许在没有命令的情况下使用它(POSIX 未指定此行为)。

也许用更多的空间会更清楚:

echo $( < $FILE )
Run Code Online (Sandbox Code Playgroud)

实际上*与更 POSIX 安全的相同

echo $( cat $FILE )
Run Code Online (Sandbox Code Playgroud)

...这也有效*

echo $( cat < $FILE )
Run Code Online (Sandbox Code Playgroud)

让我们从最后一个版本开始。它cat不带参数运行,这意味着它将从标准输入读取。 $FILE由于 被重定向到标准输入<,因此cat将其内容放入标准输出。$(command)然后,替换将的cat输出推入 的参数中echo

bash(但不是在 POSIX 标准中),您可以<不使用命令来使用。 bash(andzshkshbut not dash) 会将其解释为 if cat <,但不会调用新的子流程。由于这是 shell 的本机功能,因此它比直接运行外部命令要快cat*这就是为什么我说“实际上相同”。

  • `&lt; file` 与 `cat &lt; file` 不同(除了在 `zsh` 中,它类似于 `$READNULLCMD &lt; file`)。`&lt; file` 是完美的 POSIX,只是打开 `file` 进行读取,然后什么也不做(所以 `file` 立即关闭)。`$(&lt; file)` 或 `\`&lt; file\`` 是 `ksh`、`zsh` 和 `bash` 的特殊运算符(POSIX 中未指定其行为)。详情请参阅[我的回答](/a/368663)。 (7认同)
  • 从另一个角度来看@StéphaneChazelas 的评论:首先近似,“$(cmd1) $(cmd2)”通常与“$(cmd1; cmd2)”相同。但看看 `cmd2` 是 `&lt; file` 的情况。如果我们说“$(cmd1; &lt; file)”,则不会读取文件,但是如果使用“$(cmd1) $(&lt; file)”,则会读取该文件。因此,说“$(&lt; file)”只是“$(command)”与“&lt; file”命令的普通情况是不正确的。`$(&lt; …)` 是命令替换的*特殊情况*,而不是重定向的正常用法。 (3认同)