为什么不能使用 /dev/stderr 作为管道

为什么不能使用 /dev/stderr 作为管道

通常paste打印相邻列中的命名(或等效)文件如下所示:

paste <(printf '%s\n' a b) <(seq 2)

输出:

a   1
b   2

但是当两个文件是/dev/stdin和时/dev/stderr,它的工作方式似乎并不相同。

假设我们有b缺少b盒子输出两行的程序标准输出和两行标准误。出于说明目的,可以使用函数来模拟:

bb() { seq 2 | tee >(sed 's/^/e/' > /dev/stderr) ; }

现在运行annotate-output, (在里面开发脚本封装在Debian/Ubuntu/等。),以表明它有效:

annotate-output bash -c 'bb() { seq 2 | tee >(sed 's/^/e/' > /dev/stderr) ; }; bb'
22:06:17 I: Started bash -c bb() { seq 2 | tee >(sed s/^/e/ > /dev/stderr) ; }; bb
22:06:17 O: 1
22:06:17 E: e1
22:06:17 O: 2
22:06:17 E: e2
22:06:17 I: Finished with exitcode 0

所以它有效。馈送bbpaste

bb | paste /dev/stdin /dev/stderr

输出:

1   e1
e2
^C

它挂着——^C意味着按下控制-C退出。

将 更改|为 a;也不起作用:

bb ; paste /dev/stdin /dev/stderr

输出:

1
2
e1
e2
^C

也挂起——^C意味着按下控制-C退出。

期望的输出:

1    e1
2    e2

可以使用 来完成吗paste?如果没有,为什么不呢?

答案1

为什么不能使用 /dev/stderr 作为管道

问题不在于paste,也不在于/dev/stdin。它与/dev/stderr.

所有命令均使用一个开放输入描述符(0:标准输入)和两个输出(1:标准输出和 2:标准错误)创建。这些通常可以分别使用名称/dev/stdin/dev/stdout/dev/stderr来访问,但请参阅/dev/stdin、/dev/stdout 和 /dev/stderr 的可移植性如何?。许多命令(包括paste)也会将文件名解释-为 STDIN。

当您bb单独运行时,STDOUT 和 STDERR 都是控制台,通常会出现命令输出。这些行经过不同的描述符(如您的 所示annotate-output),但最终到达相同的位置。

当您添加一个|和第二个命令时,创建管道......

bb | paste /dev/stdin /dev/stderr

告诉|shell 将 的输出连接bb到 的输入pastepaste首先尝试读取/dev/stdin,它(通过一些符号链接)解析为它自己的标准输入描述符(外壳刚刚连接起来),以便该行1通过。

但 shell/pipeline 对 STDERR 没有任何作用。 bb仍然将其(e1 e2等)发送到控制台。同时,paste尝试从同一个控制台读取数据,该控制台会挂起(直到您键入某些内容)。

你的链接为什么我无法使用文本编辑器读取 /dev/stdout?在这里仍然相关,因为这些相同的限制适用于/dev/stderr.

如何制作第二条管道

您有一个命令可以生成标准输出和标准错误,并且您希望paste这两行彼此相邻。这意味着两个并发管道,每一列一个。 shell 管道... | ...提供其中之一,您需要自己创建第二个,并使用 .redirect 将 STDERR 重定向到该管道2>filename

mkfifo RHS
bb 2>RHS | paste /dev/stdin RHS

如果这是在脚本中使用,您可能更愿意将该 FIFO 放在临时目录中,并在使用后将其删除。

答案2

annotate-outputpaste之所以能够做到这一点,是因为它正在做一些特殊的事情(即将命令的 stderr 重定向到 fifo),绝对地没办法——只是paste因为不是运行它从中获取输入的命令,并且它无法重定向它们的输入或输出。

但是您可以编写一个包装器,使用与 annotate-output 所使用的完全相同的技巧:

pasteout(){
  f=$(mktemp -u) || return
  mkfifo -m 600 -- "$f" || return
  "$@" 2>"$f" | paste -- - "$f"
  rm -f -- "$f"
}
pasteout bb

但请注意,它很容易出现死锁。例如,如果bb产生的标准输出多于管道所能容纳的数量加上最初读取的额外数量,paste但不会产生任何错误输出,paste则将被阻止等待 fifo 上的输入,并且不会清空bb正在将其标准输出提供给的管道,导致bb管道的 write() 也挂起。

答案3

整条生产线有几个问题需要我们分析,那就是:

seq 2 | tee >(sed 's/^/e/' > /dev/stderr) | paste /dev/stdin /dev/stderr

标准错误

首先是最后一个命令。仅有的标准输出可以通过管道:

$ seq2 | paste -
1
2

$ seq2 | paste - -
1 2

没有什么可读的stderr

$ seq 2 | paste - /dev/stderr 
1   ^C

您需要^C它,因为它会阻塞,没有任何内容可供读取stderr
即使您创建一些输出,stderr它也不会通过管道传输:

$ { seq 2; seq 3 4 >/dev/stderr; } | paste - /dev/stderr
1   3
4

和以前一样,1被打印并且paste块等待stderr
另外 2 个数字直接进入控制台并(独立)打印。

stderr您可以在管道的最后一个命令中提供一些输入:

$ { seq 2; seq 3 4 >/dev/stderr; } | paste - /dev/stderr 2</dev/null
1
2
3
4

2>/dev/null顺便说一下,这与避免阻塞命令中使用的第二个文件描述符完全相同paste。但打印的值直接来自seq 3 4重定向到控制台,而不是来自paste.这也有同样的作用:

$ { seq 2; seq 3 4 >/dev/tty; } | paste - /dev/stderr 2</dev/null
1   
2   
3
4

这并不会阻止:

$ seq 2 | tee >(sed 's/^/e/' > /dev/stderr) | 
  paste /dev/stdin /dev/stderr 2</dev/null
1   
2   
e1
e2

命令

其次, 的输出tee不必是“按顺序”的。`tee` 和 `bash` 进程替换顺序

而且,事实上:进程替换的输出不必是“按顺序”的: 进程替换输出乱序

$ echo one; echo two > >(cat); echo three;
one
three
two

事实上,在某些示例中,如果您尝试多次,您可能会得到不同的订单。通过进程替换同时运行的独立进程的非确定性输出

$ printf '%s\n' {0..1000} | tee >(head -n2) >(sort -grk1,1 | head -n3) >/dev/null
1000
999
998
0
1

所以,不,不能通过过程替换和粘贴来完成。
您需要给执行一些命令:

$ seq 2 | { while read a; do printf "%s %s\n" "$a" "e$a" ; done; }
1 e1
2 e2

BB

所以,你的 bb 函数(基本上)包含:

| tee >(sed 's/^/e/')

可以用以下方法进行测试:

$ printf '%s\n' {0..1000} | tee >(sort -grk1,1 | head -n3 >&2) | head -n 2
0
1
291
290
289

应该按这个顺序打印 0, 1, 1000, 999, 998,但很多时候却没有。
即:是本质上不稳定。

稳定的实数解。

对于 bb 来说,唯一安全的解决方案是避免任何进程替换。
并且,利用{…}捕获 stdout 和 stderr 的优势,例如:

$ bash -c '{ echo test-str >/dev/stderr; }' 2>/dev/null

无输出,去掉2确认。

这适用于 bb:

$ bb() { seq 5 | tee /dev/stderr | sed 's/^/e/'; }

并使用 fifo 进行粘贴:

$ mkfifo out2
$ bb 2>out2  | paste out2 -
1   e1
2   e2
3   e3
4   e4
5   e5

您需要设置一个陷阱来删除 fifo 文件,并在创建之前测试 fifo 文件是否存在。

似乎可以在我测试的所有 shell 上移植(与 Almquist 语法兼容)。尚未完全测试,请其他用户确认,可能会有一些未知的惊喜。

相关内容