zsh:理解重定向的顺序和带有指针的管道

zsh:理解重定向的顺序和带有指针的管道

在 zsh 中,

echo "hello" 1>&1 1>&1 1>&1 | cat

打印 hello 8 次,同时

echo "hello" 1>&1 1>&1 1>&1 1>&1 | cat

打印 hello 16 次。因此 hello 被打印 2^n 次,其中 n 等于1>&1上面管道中的数量。

我在寻找什么?多个来源表明管道+重定向发生的顺序是:

“在作为命令一部分的重定向运算符指定的任何重定向之前,命令的标准输入、标准输出或两者应被视为由管道分配”

源代码:https://pubs.opengroup.org/onlinepubs/9699919799.2008edition/utilities/V3_chap02.html#tag_18_09_02,https://unix.stackexchange.com/a/672961/456507

粗略的说法是:处理重定向和管道的顺序是“从左到右,在管道之后”。

zsh Libera 聊天 IRC 中的某人建议,在处理重定向之前,“echo 的 fd1 指向 cat 的 fd0”。有人可以扩展这种措辞方式以包括重定向1>&1吗?这很重要,因为我认为如果人们可以用语言表达它,那么人们就可以对其他重定向和管道进行类似的表达并理解正在发生的事情。如果您愿意,可以随意使用一些虚构的 C/C++ 指针术语!

答案1

此行为特定于多操作系统zsh 特有的功能。因此其他文档(例如 POSIX 规范)对您的帮助不大。兹什当多操作系统关闭时,在这方面符合 POSIX 标准,因此 POSIX 行为确实提供了部分故事,但只是部分。

无论有没有多操作系统,理解重定向的最自然的方法就是采用命令式思维方式。命令的重定向是从左到右处理的(管道先行除外),并且每次重定向都会修改进程的状态。这就是 shell 的作用。

  1. 最初,该命令具有与其周围的代码相同的打开的文件描述符。

  2. 如果命令位于管道的右侧,则将管道的读取端连接到文件描述符 0(标准输入)。如果命令位于管道的左侧(包括|&),则将管道的写入侧连接到文件描述符 1(标准输出)。

  3. 按顺序应用重定向。这里我只列出主要的相关案例。请参阅手动的有关重定向运算符的完整列表。N代表零个或多个数字或文件描述符变量M代表一位或多位数字,FILENAME代表文件名(可以是进程替换)。

    • N<&M将文件描述符 M 复制到 N,即 N 连接到 M 所连接的任何位置。先前连接到文件描述符 N 的任何内容都不再重要。
    • N<FILENAME打开 FILENAME 进行输入并将其连接到文件描述符 N。以前连接到文件描述符 N 的任何内容都不再重要。
    • N>&MN>FILENAMEN>>FILENAME与表单类似<,只是N>FILENAME打开文件进行覆盖和N>>FILENAME打开文件进行追加,但以下情况除外。
      如果multios启用(默认情况下)并且此命令中的文件描述符 N 上已经存在重定向(包括管道),则 zsh 不会将文件描述符 N 连接到 M-or-FILENAME,而是创建一个管道 P,分叉一个类似内置的tee进程,从管道读取并将其输出复制到 M-or-FILENAME 以及文件描述符 M 上的内容,并将 N 连接到管道的写入端。也就是说,文件描述符 N 上的第二次重定向大致相当于exec N> >(tee FILENAME >&N)在命令之前执行。 (请注意,这相当于N> >(tee FILENAME >&N)在简单情况下应用重定向,但当同一文件描述符上存在三个或更多重定向时则不然。)
    • N<&-N>&-关闭文件描述符 N。
  4. 如果命令位于 的左侧|&,则执行2>&1重定向。 (这相当于在简单情况下通过管道发送标准错误,但在复杂情况下并不总是如此。)

因此,对于多重重定向1>&1 1>&1 1>&1 …,对于多操作系统,第一个重定向本身不执行任何操作:将文件描述符重定向到其自身是一个无操作(除非文件描述符关闭时会出现错误)。但它仍然算作多操作系统的重定向,因此第二个1>&1创建一个管道,连接到类似 Tee 的进程,该进程复制其输入,两个输出都发送到原始标准输出。第三个1>&1创建另一个类似 tee 的进程,该进程复制其输入,两个输出都转到现在的标准输出,即第一个 tee 的输入,因此数据被复制两次。第四个1>&1复制输入,两个输出都进入第二个 T 形座,从而产生三次重复,即输入的 8 倍。等等。有了多次重定向和管道,管道算作一次重定向,所以第一次1>&1是文件描述符1的第二次重定向并开始复制,这就是echo "hello" 1>&1 1>&1 1>&1 | cat打印hello8次的原因。它相当于

( exec 1> >(tee /dev/fd/1 1>&1);
  exec 1> >(tee /dev/fd/1 1>&1);
  exec 1> >(tee /dev/fd/1 1>&1);
  echo "hello" ) | cat

或者

{ { { { echo hello; } > >(tee /dev/fd/1 1>&1); } > >(tee /dev/fd/1 1>&1); } > >(tee /dev/fd/1 1>&1); } | tr a-z A-Z

练习 1:解释以下命令的行为。

/bin/echo hello 1>&1 1>&1 1>&-

1>&1 1>&1将输出复制到标准输出。然后1>&-关闭标准输出,因此echo无处可写并抱怨。

练习2:解释这两个命令之间的区别。

/bin/echo hello 1>&- 1>&2
/bin/echo hello 1>&1 1>&1 1>&- 1>&2

在第一种情况下,文件描述符 1 上只有一个非关闭重定向,因此 multio 行为不会启动。1>&-关闭 fd 1 并且1>&2是普通重定向,因此该命令hello在 fd 2 上打印,就像/bin/echo 1>&2那样。在第二种情况下,fd 1 上有多个重定向,因此1>&-关闭它后,zsh 对 执行多重定向1>&2。这涉及到分叉一个类似于 tee 的进程,该进程将写入当前连接到 fd 1 的内容,但 fd 1 已关闭,因此失败并显示multio failed for fd 1: bad file descriptor

练习 2b:解释

/bin/echo hello 1>&1 1>&- 1>&2

我认为关闭文件描述符会重置 is-this-the-first-redirection 状态,因此第二个重定向不是多重重定向,这相当于/bin/echo hello 1>&2.

相关内容