bash:如何将字符串添加到 stderr 行并按确切顺序组合 stdout 和 stderr 并存储在 bash 中的一个变量中?

Posted

技术标签:

【中文标题】bash:如何将字符串添加到 stderr 行并按确切顺序组合 stdout 和 stderr 并存储在 bash 中的一个变量中?【英文标题】:bash: How to prepend a string to stderr lines and combine both stdout and stderr in exact order and store in one variable in bash? 【发布时间】:2016-08-14 21:53:47 【问题描述】:

到目前为止我所做的是:

#!/bin/bash

exec 2> >(sed 's/^/ERROR= /')

var=$(
        sleep 1 ; 
        hostname ; 
        ifconfig | wc -l ; 
        ls /sfsd; 
        ls hasdh;
        mkdir /tmp/asdasasd/asdasd/asdasd;
        ls /tmp ;
) 

echo "$var"

这确实在每个错误行的开头添加了 ERROR=,但首先显示所有错误,然后显示 stdout,(不按执行顺序)。

如果我们跳过将输出存储在变量中并直接执行命令,则输出会按所需的顺序出现。

任何专家意见都将不胜感激。

【问题讨论】:

【参考方案1】:

您的脚本的主要问题是命令替换$(...) 仅捕获子shell 的标准输出;子shell的标准错误仍然只是流向父shell的标准错误。碰巧的是,您已经重定向了父 shell 的标准错误,最终填充了父 shell 的标准输出;但这完全绕过了$(...),它只捕获subshel​​l的标准输出。

你明白我的意思吗?

因此,您可以通过重定向 subshel​​l 的 标准错误以最终填充 its 标准输出的方式来解决此问题,这就是被捕获的内容:

var=$(
    exec 2> >(sed 's/^/ERROR= /')
    sleep 1
    hostname
    ifconfig | wc -l
    ls /sfsd
    ls hasdh
    mkdir /tmp/asdasasd/asdasd/asdasd
    ls /tmp
)

echo "$var"

即便如此,这并不能保证行的正确顺序。问题是sed 与子shell 中的所有其他内容并行运行,因此当它刚刚收到一条错误行并正忙于计划写入标准输出时,子shell 中的后续命令之一可以向前推进并且已经向标准输出写入了更多内容!

您可以通过为每个命令单独启动 sed 来改进它,这样 shell 将等待 sed 完成,然后再继续执行下一个命令:

var=$(
    sleep 1 2> >(sed 's/^/ERROR= /')
    hostname 2> >(sed 's/^/ERROR= /')
     ifconfig | wc -l ;  2> >(sed 's/^/ERROR= /')
    ls /sfsd 2> >(sed 's/^/ERROR= /')
    ls hasdh 2> >(sed 's/^/ERROR= /')
    mkdir /tmp/asdasasd/asdasd/asdasd 2> >(sed 's/^/ERROR= /')
    ls /tmp 2> >(sed 's/^/ERROR= /')
)

echo "$var"

即便如此,sed 将与每个命令同时运行,因此如果这些命令中的任何一个是写入标准输出和标准错误的复杂命令,那么捕获该命令输出的顺序可能不会匹配命令实际编写它的顺序。但这对于您的目的来说应该已经足够了。

您可以通过为简单命令(非管道)情况创建包装函数来稍微提高可读性:

var=$(
    function fix-stderr () 
       "$@" 2> >(sed 's/^/ERROR= /')
    

    fix-stderr sleep 1
    fix-stderr hostname
    fix-stderr eval 'ifconfig | wc -l'   # using eval to get a simple command
    fix-stderr ls /sfsd
    fix-stderr ls hasdh
    fix-stderr mkdir /tmp/asdasasd/asdasd/asdasd
    fix-stderr ls /tmp
)

echo "$var"

【讨论】:

您的回答解释了很多意外发生的事情,谢谢。 在每一行添加2> >(sed 's/^/::ERROR:: /') 的一个问题是,如果我们想要执行另一个脚本文件,我们将执行./test.sh 2> >(sed 's/^/::ERROR:: /'),它仍然会给出随机排序的输出。 @avg598:是的,没错。 “流”的整个 Unix 概念非常强大,但它没有提供任何在不同流之间保持时间顺序的概念。您必须更改您的 ./test.sh 以支持相同的功能。【参考方案2】:

sed 命令与 shell 的其余部分异步运行;一旦它处理来自命令替换中的命令的输入,它的输出就会进入标准错误。但是,这些命令的标准输出会在$var 中捕获,并且在echo 命令运行之前不会显示。

即使您没有捕获输出,也有机会这些命令的标准错误和标准输出不会像您预期的那样出现,因为最终产生的 sed 命令操作系统可能不会按照您的预期安排错误消息,从而延迟错误消息的出现。

当您以常规方式从终端运行命令时,该命令的标准错误和标准输出指向同一个文件:终端本身。因此,对文件的写入会保持它们在程序中出现的顺序。一旦您将一个或另一个通过管道传输到另一个进程,您就会失去对两者如何拼接在一起的所有控制权,如果有的话。在您的情况下,您将标准错误重定向到sed,它将修改后的行写回标准输出。但是您无法控制操作系统安排sed 何时运行以及您的shell 何时运行,因此您无法控制写入行的顺序。

它有助于为每个命令分别重定向标准错误:

tag_error ()  sed 's/^/ERROR= /'; 

hostname 2> >(tag_error)
 ifconfig | wc -l ;  2> >(tag_error)
# etc

但这仍然不能保证来自同一个程序的写入是有序的,就好像它们都写入同一个文件一样。

(ruakh has covered如何将它与捕获标准输出结合起来,所以我现在不费心添加它。看他的回答。)

【讨论】:

你不能;没有标记来指示来自sed 的哪些行以及$var 中的哪些行对应于命令替换中的每个单独命令。相对输出仅在“常规”情况下合并,因为每个进程的 stdout 和 stderr 是完全相同的文件句柄。 我想做的整个前置操作是标记行以识别哪些来自标准输出,哪些来自标准错误,同时保持顺序。还有其他方法吗?【参考方案3】:

一种可能的解决方案是将命令放在一个数组中,然后在循环中执行它们:

declare -a cmds=('sleep 1' 'hostname' 'eval ifconfig | wc -l' 'ls /sfsd' 'ls /tmp' 'ls hasdh')

for i in "$cmds[@]"; do
    $i 2> >(sed -E 's/^/ERROR=/')
done

当发生错误时,它应该按照它在执行中发生的顺序打印。在数组中使用诸如sh script.sh 之类的命令还应该从生成的外部脚本中显示任何stdoutstderr。对于管道命令,可能还需要eval

【讨论】:

但是如果其中一个命令是运行一个同时提供标准错误和标准输出的脚本文件,它将不起作用。 我不确定我是否遵循...如果您在数组中有诸如 sh myscript.sh 之类的命令,它仍然会同时输出 stdoutstderr 好的,让我验证一下,同时检查这个link。这样做的主要目的是在那个链接中。 我建议删除该问题并在此处编辑您的问题,如有任何其他问题。 为每个命令分别重定向标准错误的想法是个好主意;将命令存储在数组中不是。

以上是关于bash:如何将字符串添加到 stderr 行并按确切顺序组合 stdout 和 stderr 并存储在 bash 中的一个变量中?的主要内容,如果未能解决你的问题,请参考以下文章

转换流以将字符串添加到每一行

当方法生成 std​​err 时,如何将信息添加到 STDERR?

如何在bash中重复行并粘贴不同的列?

SQL:选择具有最大值的行并按单列分组

Linux简单学习

如何将 stdout、stderr 重新路由回 /dev/tty