bash:如何在stderr行前添加字符串,并以精确的顺序组合stdout和stderr,并将其存储在bash中的一个变



到目前为止我所做的是:

#!/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,(不是按照执行的顺序)。

如果我们跳过将输出存储在变量中而直接执行命令,则输出将按照期望的顺序出现。

脚本的主要问题是命令替换$(...)只捕获子shell的标准输出;子shell的标准错误仍然会传递给父shell的标准错误。碰巧的是,你重定向了父shell的标准错误,最终填充了父shell的标准输出;但这完全绕过了$(...),它只捕获子壳的标准输出。

你明白我的意思吗?所以,你可以通过重定向子shell的标准错误来解决这个问题,最终填充它的标准输出,这就是被捕获的:
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"

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已经介绍了如何将它与捕获标准输出结合起来,所以我现在不再添加它了。

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

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

最新更新