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


11

我试图了解Bash如何正确对待以下行:

$(< "$FILE")

根据Bash手册页,这等效于:

$(cat "$FILE")

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

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

有人可以向我解释$FILEstdin到stdout 的内容如何吗?

Answers:


-3

<不是bash命令替换的直接方面。它是一个重定向操作符(如管道),某些外壳程序无需命令即可允许使用此操作(POSIX未指定此行为)。

也许有了更多空间会更清楚:

echo $( < $FILE )

实际上与更安全的POSIX相同

echo $( cat $FILE )

...这也是有效的*

echo $( cat < $FILE )

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

在中bash(但不是POSIX标准),您可以<不使用命令而使用。 bash(and zshand kshnot dash)将解释为cat <,尽管没有调用新的子流程。由于这是Shell固有的,因此它比实际运行external命令要快cat*这就是为什么我说“有效地相同”。


因此,在上一段中,当您说“ bash将解释为cat filename”时,是否表示此行为特定于命令替换?因为如果我自己运行< filename,bash不会解决问题。它不会输出任何内容,并让我返回提示。
斯坦利·于

仍然需要一个命令。@cuonglm从改变我的原文cat < filenamecat filename我是反对的,并且可以恢复。
亚当·卡兹

1
管道是一种文件。Shell运算符|在两个子进程之间创建管道(或者,对于某些Shell,从子进程到Shell的标准输入)。Shell运算符$(…)创建从子流程到Shell本身(而不是其标准输入)的管道。Shell运算符<不涉及管道,它仅打开文件并将文件描述符移动到标准输入。
吉尔(Gilles)'所以

3
< file是不一样的cat < file(除了zsh它像$READNULLCMD < file)。< file是完美的POSIX,仅打开即可file阅读,然后什么也不做(因此立即file关闭)。是$(< file)`< file`的特殊运算符kshzsh并且bash(并且行为未在POSIX中指定)。有关详细信息,请参见我的答案
斯特凡Chazelas

2
换一种说法,将@StéphaneChazelas的评论换句话说:首先近似地,$(cmd1) $(cmd2)通常与相同$(cmd1; cmd2)。但是看情况cmd2< file。如果说$(cmd1; < file),则不读取文件,而使用则读取$(cmd1) $(< file)。因此$(< file)$(command)用命令的普通情况是不正确的< file。   $(< …)是命令替换的一种特殊情况,而不是重定向的常规用法。
斯科特(Scott)

14

$(<file)(也适用于`<file`)是由zsh和复制的Korn shell的特殊运算符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)

然后

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

像使用$(cat < file)不同之处在于

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

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

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

< file
<&3
<<< here...

也就是说,只有输入重定向而不执行命令时,在命令替换之外zsh运行$READNULLCMD(默认为寻呼机),并且当同时存在输入和输出重定向时,$NULLCMDcat默认为),即使$(<&3)没有被识别为特殊运算符,它仍然可以像ksh通过调用分页器执行操作一样工作(该分页器的行为类似,cat因为其标准输出将是管道)。

但是同时ksh$(< a < b)将扩大到的内容a,在zsh,它扩展到的内容ab(或只是b如果multios选项被禁用),$(< a > b)将复制ab并扩大到什么,等等。

bash 具有相似的运算符,但有一些区别:

  • 允许在注释之前但之后不允许注释:

    echo "$(
       # getting the content of file
       < file)"
    

    可以,但是:

    echo "$(< file
       # getting the content of file
    )"
    

    扩展为空。

  • 像in中一样zsh,只有一个文件stdin重定向,尽管没有回退到a $READNULLCMD,所以$(<&3)$(< a < b)请执行重定向,但扩展到无。

  • 由于某些原因,尽管bash不调用cat,它仍然派生出一个通过管道来馈送文件内容的进程,这使其优化程度远低于其他Shell。它像一个作用$(cat < file)在那里cat将是一个内置cat
  • 由于上述原因,之后所做的任何更改都将丢失($(<${file=foo.txt})例如,在上述的中,此$file分配随后将丢失)。

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

请注意,ksh与ksh93相比,基于pdksh的变体有一些变体。有趣的是,mksh(这些pdksh衍生的外壳之一)

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

通过在不使用临时文件或管道的情况下扩展here文档的内容(不带结尾字符)的方式优化了here文档的内容,这与here document的情况不同,这使其成为有效的多行引用语法。

可以移植到所有版本kshzsh并且bash,最好是限制只能$(<file)避免意见,并铭记修改变量内可能会或可能不会被保留做。


$(<)文件名的运算符是否正确?是<$(<)重定向运算符中,还是不是一个独立的运算符,并且必须是整个运算符的一部分 $(<)
蒂姆(Tim)

@Tim,您如何称呼它们并不重要。$(<file)旨在以file类似的方式扩展到的内容$(cat < file)。各个shell的完成方式各不相同,答案中对此进行了详细说明。如果愿意,可以说这是一个特殊的运算符,当看起来像命令替换的内容(语法上)包含看起来像单个stdin重定向的内容(语法上)时被触发,但是又有一些警告和变化,具体取决于此处列出的shell 。
斯特凡Chazelas

@StéphaneChazelas:像往常一样令人着迷;我已经为此加了标签。所以,n<&mn>&m做同样的事情?我不知道,但是我想这并不奇怪。
斯科特(Scott)

@Scott,是的,他们俩都做一个dup(m, n)。我可以看到使用stdio和ksh86的一些证据fdopen(fd, "r" or "w"),所以那时可能很重要。但是在外壳中使用stdio几乎没有意义,因此我不希望您会在任何现代外壳中有所作为。一个区别是>&ndup(n, 1)(的缩写1>&n),而(<&n是的dup(n, 0)缩写0<&n)。
斯特凡Chazelas

对。当然,除了文件描述符复制调用的两个参数形式被称为dup2(); dup()仅接受一个参数,并且与一样open(),使用最低的可用文件描述符。(今天我了解到有一个dup3()功能。)
Scott

8

因为bash它是在内部为您完成的,所以扩展了文件名并将该文件整理为标准输出,就像您要做的那样$(cat < filename)。这是一项bash功能,也许您需要查看bash源代码才能确切了解其工作方式。

这里是处理此功能的函数(来自bash源代码,file 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)最初是从ksh,并加入到bashBash-2.02


1
cat filename如果文件名以短划线开头,则会失败,因为cat接受选项。您可以使用来解决大多数现代系统上的问题cat -- filename
亚当·卡兹

-1

可以将命令替换视为照常运行命令,并在运行命令时将输出转储。

命令的输出可以用作另一个命令的参数,设置变量,甚至可以在for循环中生成参数列表。

foo=$(echo "bar")将变量的值设置$foobar; 命令的输出echo bar

命令替换


1
我认为,从这个问题可以很明显地看出,OP了解命令替换的基本知识。问题是关于的特殊情况$(< file),他不需要有关一般情况的教程。如果您说这$(< file)只是$(command)命令的一般情况< file,那么您说的是亚当·卡茨(Adam Katz)所说的相同的,那么你们都错了。
斯科特(Scott)
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.