Tee 过程替换误解

Tee with process substitution misunderstanding

我正在尝试为 LDAP 条目编写一个漂亮的打印机,它只获取根 LDAP 记录一次,然后将输出通过管道传输到 tee,为每个部分调用漂亮的打印机。

为了便于说明,假设我的 group_entry 函数 returns 特定 LDAP DN 的 LDIF。其中的细节并不重要,所以我们总是这样说 returns:

dn: cn=foo,dc=example,dc=com
cn: foo
owner: uid=foo,dc=example,dc=com
owner: uid=bar,dc=example,dc=com
member: uid=foo,dc=example,dc=com
member: uid=baz,dc=example,dc=com
member: uid=quux,dc=example,dc=com
custom: abc123

我可以通过 grep'ing 和 cut'ing 轻松地分别提取所有者和成员。然后,我可以将这些辅助 DN 通过管道传输到另一个 LDAP 搜索查询中,以获取他们的真实姓名。例如,假设我有一个 pretty_print 函数,它在 LDAP 属性名称上进行了参数化,它完成了我刚才提到的所有内容,然后使用 AWK 很好地格式化了所有内容:

$ group_entry | pretty_print owner
Owners:
foo    Mr Foo
bar    Dr Bar

$ group_entry | pretty_print member
Members:
foo    Mr Foo
baz    Bazzy McBazFace
quux   The Artist Formerly Known as Quux

这些单独使用时效果很好,但是当我尝试 tee 将它们组合在一起时,没有任何反应:

$ group_entry | tee >(pretty_print owner) | pretty_print member
Members:
[Sits there waiting for Ctrl+C]

显然我对它的工作原理有一些误解,但我没有理解。我做错了什么?


编辑 为了完整起见,这是我的完整脚本:

#!/usr/bin/env bash

set -eu -o pipefail

LDAPSEARCH="ldapsearch -xLLL"

group_entry() {
  local group=""
  ${LDAPSEARCH} "(&(objectClass=posixGroup)(cn=${group}))"
}

get_attribute() {
  local attr=""
  grep "${attr}:" | cut -d" " -f2
}

get_names() {
  # We strip blank lines out of the LDIF entry, then we always have "dn"
  # followed by "cn" records; we strip off the attribute name and
  # concatenate those lines, then sort. So we get a sorted list of:
  # {{distinguished_name}} {{real_name}}
  xargs -n1 -J% ${LDAPSEARCH} -s base -b % cn \
  | grep -v "^$" \
  | cut -d" " -f2- \
  | paste - - \
  | sort
}

pretty_print() {
  local attr=""
  local -A pretty=([member]="Members" [owner]="Owners")

  get_attribute "${attr}" \
  | get_names \
  | gawk -F'\t' -v title="${pretty[${attr}]}:" '
    BEGIN { print title }
    { print "-", gensub(/^uid=([^,]+),.*$/, "\1", "g", ), "\t",  }
  '
}

# FIXME I don't know why tee with process substitution doesn't work here
group_entry "" | pretty_print owner
group_entry "" | pretty_print member

您描述的行为看起来非常像 C 程序中可能出现的情况,该程序在没有正确处理所有打开的文件描述符的情况下分叉并执行另一个程序(shell 和 xargs 都确实如此) .您可能会遇到进程 p1 没有终止的情况,因为它正在等待在其标准输入上观察 EOF,但它永远不会终止,因为另一个进程 p2 为提供 p1 的标准输入的管道的写端保存一个打开的文件描述符,而 p2 本身正在等待p1 终止或执行其他操作。

尽管如此,我没有发现您的管道在这方面有任何固有的错误,并且我不会使用这个更简单的模型重现挂起...

echo "foo" | tee >(cat) | cat

...在 bash 的 4.2.46 版中。在您的 bash 版本(即使是同一个版本)或 xargs 中可能仍然存在相关错误,但这是推测性的。我不认为你的管道应该像你说的那样挂起,但我不准备开始指责。

无论如何,即使您的管道没有挂起,它也没有您想要的语义,正如@chepner 在评论中指出的那样。 pretty_print member 将在其标准输入上接收 tee 的输出,这将包括 group_entry 的输出和 pretty_print owner。您可以考虑以不同的方式实现它:由于 tee 可以通过两种以上的方式复用输入,因此您可以通过这样做一石二鸟:

group_entry "" | tee >(pretty_print owner) >(pretty_print member)

但这留下了两个 pretty_print 执行的输出将混合在一起的可能性,并且还回显了 group_entry 输出。您可以想像地过滤掉 group_entry 输出,但为了避免混合,您需要确保两个 pretty_print 命令按顺序 运行。这给基于 tee 的方法带来了问题,因为如果任何 tee 的输出阻塞,那么整个管道都会停止。

一种解决方案是将一个或两个 pretty_print 命令的输出重定向到一个文件。或者,如果必须将两个输出都转到 stdout,那么除了捕获 group_entry 输出并将其分别提供给每个 pretty_print 作业之外,我认为没有其他好的选择。您可以将其捕获到一个文件中,但这是不必要的,而且有点混乱。考虑一下:

entry_lines=$(group_entry "")
pretty_print owner  <<<"$entry_lines"
pretty_print member <<<"$entry_lines"

使用命令替换在 shell 变量(包括换行符)中捕获 group_entry 的输出,并使用此处字符串将其重播到每个 pretty_print 进程中。