将数据通过管道传输到脚本或函数,确定输出是到达 stdout 还是 stderr?

将数据通过管道传输到脚本或函数,确定输出是到达 stdout 还是 stderr?

我正在尝试创建一个通用函数或脚本,可以将数据通过管道传输到其中,并在输出前面添加显示数据“到达”的 fd(stdout 或 stderr)。我的 Bash 技能充其量只是中等水平,而且我发现自己在试图理解诸如{ foo 2>&1 >&3 3>&- | bar 3>&-; } 3>&1.

任何人都可以建议这是否可能以及实现的方法吗?

本质上我想做这样的事情:

$ { echo "foo" ; echo "bar" 1>&2 ; } | my_output_processor.sh
stdout: foo
stderr: bar 

我读了管道 STDERR 与 STDOUT但这并没有为我解答。我认为答案是https://unix.stackexchange.com/a/440439/307184可能包含一条线索,但似乎只能在运行脚本中工作,而不能在数据管道中工作。

我的 macOS 上的 Bash 版本是 5.0。

编辑:@Glenn 下面的答案有效!我制作了几个小辅助函数来节省一些打字并防止错误,并进行了如下测试:

$ out(){ sed "s/^/out: /" ;} ; err(){ sed "s/^/err: /" 1>&2 ;}
$ { echo "good" ; echo "bad" >&2; } 2> >(err) 1> >(out)
err: bad
out: good

关于重定向顺序的后续问题:为什么2> >(err) 1> >(out)效果很好,而效果1> >(out) 2> >(err)却不佳? (更新:下面由 @RudiC 回答 - 辅助函数已更新,因此它们现在可以按任何顺序工作)

答案1

使用流程替代不是管道:

{ echo "foo" ; echo "bar" >&2; } 2> >(sed "s/^/err: /") > >(sed "s/^/out: /")
err: bar
out: foo

如果您在脚本中执行此操作,请使用以下exec命令在脚本的整个持续时间内设置重定向:

bash -c '
    exec 2> >(sed "s/^/err: /") > >(sed "s/^/out: /")
    echo foo
    echo bar >&2
'
err: bar
out: foo

如果你这样做

exec > >(sed "s/^/out: /") 2> >(sed "s/^/err: /")

那么输出是

out: foo
out: err: bar

我想这就是你所说的“不起作用”的意思。这是因为默认情况下“err”进程替换的输出会转到标准输出,而现在指向“out”进程子进程。你必须这样做才能解决它:

exec 3>&1 > >(sed "s/^/out: /") 2> >(sed "s/^/err: /" >&3)

创建另一个指向默认 stdout 的文件描述符 (3),然后 stderr 重定向打印到 fd3。

答案2

执行时后续问题会自行回答:

err函数也打印到标准输出。显然,第一种形式具有原始的标准输出,而第二种形式已经使用“重定向到 out() 函数”。

答案3

这应该适用于任何标准 shell,而不仅仅是 bash:

{ cmd 2>&1 >&3 | sed 's/^/err: /' >&2; } 3>&1 | sed 's/^/out: /'

为了方便起见,可以将其制成一个函数:

# usage louterr cmd [args ...]
louterr(){ { "$@" 2>&1 >&3 | sed 's/^/err: /' >&2; } 3>&1 | sed 's/^/out: /'; }

cmd(){ echo ERR >&2; echo OUT; }
louterr cmd
out: OUT
err: ERR
louterr louterr louterr cmd
err: err: err: ERR
out: out: out: OUT

或者,使用可配置的“过滤器”:

# usage: louterrx out_filter err_filter cmd [args ...]
louterrx(){
   oflt=$1; eflt=$2; shift; shift
   eval "{ \"\$@\" 2>&1 >&3 | $eflt >&2; } 3>&1 | $oflt"
}

louterrx "sed 's/^/out: /'" "sed 's/^/err: /'" cmd

这样做的缺点是退出状态cmd将被隐藏(与管道的任何右侧一样)。要解决这个问题,您可以使用子 shell 和set -o pipefail(在bashzshksh等中受支持,但尚不支持dash):

louterr()(
   set -o pipefail
   { "$@" 2>&1 >&3 | sed 's/^/err: /' >&2; } 3>&1 | sed 's/^/out: /'
)
cmd(){ echo ERR >&2; echo OUT; return 13; }
louterr cmd; echo $?
out: OUT
err: ERR
13

我不建议> >(...)在这种情况下使用 zsh/bash-ism,因为它依赖于与在不同 shell 中执行扩展和重定向的顺序相关的怪癖(并且受到许多其他问题的影响)怪癖虫子)。例如,在

ls no_such_file >/dev/null 2> >(cat)

cat( 的错误消息)的输出ls将被重定向到/dev/nullinbash但不是 in zsh

~必须使用该cmd > >(...)形式的唯一情况是当cmd是一个函数,其目的是修改当前环境并且不能在子 shell 中运行时。

相关内容