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

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

我已经探索得差不多了全部 可用的 相似的 问题,无济于事。

我来详细描述一下这个问题:

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

我尝试过使用管道甚至基于 epoll 的方法,但没有成功。我认为解决方案是在 pty 用法中,尽管我不是这方面的大师。我还查看了源代码Gnome 的 VTE,但这并没有多大成效。

理想情况下我会使用而不是 Bash 来完成这个任务,但我一直没能做到。由于缓冲,管道似乎自动禁止保持正确的行顺序。

有人能够做类似的事情吗?或者这是不可能的?我认为,如果终端仿真器可以做到这一点,那么它就不行——也许可以通过创建一个小 C 程序来以不同的方式处理 PTY?

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

笔记:我知道斯德雷德但它不适用于 Bash 脚本,并且无法轻松编辑以添加前缀(因为它基本上包装了大量系统调用)。

更新:添加了以下两个要点

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

更新:这个问题的解决方案也将解决这另一个问题,正如@Gilles 指出的。然而我得出的结论是不可能去做这里那里要求的事情。当使用2>&1两个流时,它们在 pty/pipe 级别正确合并,但要单独并按正确的顺序使用流,确实应该使用以下方法斯德雷德涉及系统调用挂钩,可以看作肮脏的在许多方面。

如果有人可以反驳上述观点,我会很乐意更新这个问题。

答案1

您可能会使用协进程。简单的包装器,将给定命令的两个输出提供给两个sed实例(一个用于stderr另一个 for 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 将两个流重定向到一个进程(据我所知可以完成)。当该进程开始决定首先对什么采取行动时,麻烦就开始了——它必须轮询两个数据源,并在某个时刻进入处理一个流并且数据在完成之前到达两个流的状态。这正是它崩溃的地方。这也意味着,像这样包装输出系统调用stderred可能是实现您期望的结果的唯一方法(即使这样,一旦多处理器系统上的东西变成多线程,您也可能会遇到问题)。

至于协进程,请务必阅读 Stéphane 在如何在 Bash 中使用 coproc 命令?以获得深入的洞察力。

答案2

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

使用此 SO Q&A 中的解决方案进行类似的操作怎么样?是否有一个 Unix 实用程序可以将时间戳添加到文本行之前?这个 SO Q&A 的标题是:在 shell 脚本中将 STDOUT 和 STDERR 通过管道传输到两个不同的进程?

该方法

第 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。使用注释输出

有一个名为 that 的工具,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

可以控制时间戳部分的输出格式,但不能控制更多部分。但它的输出与您正在寻找的输出类似,因此它可能符合要求。

答案3

鉴于您的脚本本质上将其输出分成两个单独的管道,因此即使只有一个附加到这些管道的程序在物理上也不可能100%确保没有行被交换 - 它必须在这些管道上执行交替的非阻塞读取,在某些时候,它必须决定接下来要读取哪个管道。也就是说,我怀疑即使是终端模拟器也不能完全保留行的顺序。

相关内容