如何捕获有序的STDOUT / STDERR并添加时间戳/前缀?


25

我已经探索了几乎所有 可用的 类似 问题,但均无济于事。

让我详细描述问题:

我运行一些无人值守的脚本,这些脚本可以产生标准输出和标准错误行,我想捕获它们 按终端仿真器显示的精确顺序,然后向它们添加诸如“ STDERR:”和“ STDOUT:”的前缀。

我尝试对它们使用管道,甚至基于epoll的方法,都无济于事。我认为解决方案是使用pty,尽管我对此并不熟练。我也偷看了Gnome VTE的源代码,但是效率不高。

理想情况下,我会使用Go而不是Bash来完成此操作,但我一直无法做到。似乎管道由于缓冲会自动禁止保持正确的行顺序。

有人能够做类似的事情吗?还是不可能?我认为,如果终端仿真器可以做到这一点,那不是-可能是通过创建一个以不同方式处理PTY的小型C程序吗?

理想情况下,我将使用异步输入读取这两个流(STDOUT和STDERR),然后根据需要重新打印它们,但是输入顺序至关重要!

注意:我知道stderred,但是它不适用于Bash脚本,并且不能轻易地添加前缀(因为它基本上包装了许多syscall)。

更新:添加以下两个要点

(可以在我提供的示例脚本中添加亚秒级随机延迟以证明结果一致)

更新:正如@Gilles指出的那样,该问题的解决方案也将解决其他问题。但是我得出的结论是,不可能在这里和那里做所要求的事情。当使用2>&1这两种流时,应该在pty / pipe级别正确合并,但是要分别并以正确的顺序使用流,实际上应该使用stderred的方法,即involes syscall挂钩,并且在许多方面都可以视为脏污

如果有人可以反驳以上内容,我将急于更新此问题。


1
这不是你想要的吗?stackoverflow.com/questions/21564/...
SLM

@slm可能不是,因为OP需要在不同的流之前添加不同的字符串。
彼得2014年

您能否分享为什么订单如此重要?也许还有其他方法可以解决您的问题……
peterph 2014年

@peterph这是一个先决条件,如果我无法获得一致的输出,我宁愿将其发送到/ dev / null而不是读取它并被它弄糊涂:) 2>&1例如保留顺序,但不允许这种类型我在这个问题中提出的定制概念
Deim0s 2014年

Answers:


12

您可以使用协同处理。简单包装器,将给定命令的两个输出馈送到两个sed实例(一个实例stderr,另一个实例用于stdout),该实例进行标记。

#!/bin/bash
exec 3>&1
coproc SEDo ( sed "s/^/STDOUT: /" >&3 )
exec 4>&2-
coproc SEDe ( sed "s/^/STDERR: /" >&4 )
eval $@ 2>&${SEDe[1]} 1>&${SEDo[1]}
eval exec "${SEDo[1]}>&-"
eval exec "${SEDe[1]}>&-"

注意几件事:

  1. 对于许多人(包括我)来说,这是一个魔咒-出于某种原因(请参阅下面的链接答案)。

  2. 无法保证它不会偶尔交换几行-这都取决于协同流程的调度。实际上,几乎可以保证在某个时间点它会。也就是说,如果保持严格的顺序相同的,你必须处理来自数据stderrstdin在同一个进程,否则内核调度可以(而且会)做它的一个烂摊子。

    如果我正确理解了该问题,则意味着您需要指示Shell将两个流重定向到一个进程(可以通过AFAIK完成)。当该过程开始决定首先要采取的措施时,麻烦就开始了-它必须轮询两个数据源,并在某个时刻进入处理一个流并且数据在完成之前到达两个流的状态。这正是它崩溃的地方。这也意味着,包装输出系统调用就像stderred可能是获得所需结果的唯一方法(即使这样,一旦在多处理器系统上出现多线程,您可能会遇到问题)。

至于协进程,请务必阅读Stéphane在如何在Bash中使用命令coproc的出色答案深入了解。


感谢@peterph的回答,但是我正在寻找保留订单的方法。注意:我认为您的解释器应该是bash,因为您使用了进程替换(我./test1.sh: 3: ./test1.sh: Syntax error: "(" unexpected通过复制/粘贴脚本来获得)
Deim0s 2014年

很可能是这样,我bash与它一起运行/bin/sh(不确定为什么在那儿有它)。
彼得2014年

我对问题进行了一些更新,该问题可能在何处发生流混淆。
彼得2014年

1
eval $@是越野车。使用"$@",如果你想运行参数作为一个确切的命令行-添加一层eval解释抛出一堆难以预测(和潜在的恶意,如果你传递的文件名或您无法控制的其他内容自变量)行为,甚至无法用引号引起来(将带有空格的名称拆分成多个单词,即使以前用引号将它们括起来也扩展了glob等)。
查尔斯·达菲

1
同样,在足够现代的bash中,您不需要 eval关闭变量中命名的文件描述符。exec {SEDo[1]}>&-将按原样运行(是的,在故意使用$之前缺少a {)。
查尔斯·达菲

5

方法1。使用文件描述符和awk

使用标题为:这样的UNIX问答中的解决方案的这样的事情怎么样:是否存在Unix实用程序来将时间戳添加到文本行之前?这样的SO Q&A标题为:将STDOUT和STDERR传递到Shell脚本中的两个不同进程?

该方法

步骤1,我们在Bash中创建2个函数,将在调用时执行时间戳消息:

$ msgOut () {  awk '{ print strftime("STDOUT: %Y-%m-%d %H:%M:%S"), $0; fflush(); }'; }
$ msgErr () {  awk '{ print strftime("STDERR: %Y-%m-%d %H:%M:%S"), $0; fflush(); }'; }

第2步,您将使用上述功能来获取所需的消息传递:

$ { { { ...command/script... } 2>&3; } 2>&3 | msgErr; } 3>&1 1>&2 | msgOut

在这里,我构思了一个示例,该示例将写入aSTDOUT,休眠10秒钟,然后将输出写入STDERR。当我们将此命令序列放入上面的构造中时,我们将按照您指定的方式发送消息。

$ { { echo a; sleep 10; echo >&2 b; } 2>&3 | \
    msgErr; } 3>&1 1>&2 | msgOut
STDERR: 2014-09-26 09:22:12 a
STDOUT: 2014-09-26 09:22:22 b

方法2。使用注释输出

有一个叫做工具annotate-output这部分devscripts包,会做你想要的。唯一的限制是它必须为您运行脚本。

如果我们将上面的示例命令序列放入如下所示的脚本中mycmds.bash

$ cat mycmds.bash 
#!/bin/bash

echo a
sleep 10
echo >&2 b

然后我们可以像这样运行它:

$ annotate-output ./mycmds.bash 
09:48:00 I: Started ./mycmds.bash
09:48:00 O: a
09:48:10 E: b
09:48:10 I: Finished with exitcode 0

可以控制时间戳部分的输出格式,但不能超出此范围。但这与您要寻找的输出类似,因此可能适合您。


1
不幸的是,这也不能解决可能交换某些行的问题。
彼得2014年

究竟。我认为这个问题的答案是“不可能的”。与stderred您发生的事件无法轻松确定线的边界(尝试这样做可能会让人头疼)。我想看看是否有人可以帮助我解决这个问题,但显然每个人都想放弃作为该问题基础的单一约束(顺序
Deim0s

方法1的步骤2需要前面的另一个{才能正常工作。
奥斯汀·汉森
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.