将 stderr 和 stdout 分别重定向到 bash 脚本中的函数

将 stderr 和 stdout 分别重定向到 bash 脚本中的函数

我正在尝试包装脚本将 cronjob 输出记录到日志中。

我有几个目标:

  • 必须使用不同的优先级和前缀标签将 stderr 和 stdout 记录到日志
  • 还必须将包装命令的 stderr 输出到包装器脚本的 stderr(由 cron 捕获并通过电子邮件发送警报)
  • 必须始终保持线路顺序

到目前为止我似乎有两个问题:

  1. 我不确定如何将 stderr 和 stdout 分别重定向到我的日志记录功能。到目前为止我所尝试的一切都无法重定向除 NULL 之外的任何内容。在我看来,重定向并不是我的强项之一。(已解决,见评论)
  2. 我当前的方法{ $_EXEC $_ARGS 2>&1 1>&7 7>&- | journaldlog error; } 7>&1 1>&2 | journaldlog info似乎运行日志记录功能两次,一次用于 stderr,一次用于 stdout。这不会保留线路顺序。我宁愿它在每个输出行运行一次该函数。

我应该放弃在 bash 中执行此操作并尝试 Perl 吗?我确信我的 Perl 经验最终会回来的。

我尝试了很多不同的方法。其中大部分是在网上找到的,特别是在堆栈交换上。老实说,我不记得我还尝试过什么......此时只是一片模糊。而且 bash 文档也没有多大帮助。

答案1

你可以只使用流程替代直接在重定向中:

gen > >(one) 2> >(two)

这将给出 的标准输出gen作为 的标准输入one和 的标准误差gen作为 的输入two。这些可以是可执行的命令或函数;这是我使用的功能:

one() {
    while read line
    do
        echo $'\e[31m'"$line"$'\e[22m'
    done
}

two() {
    while read line
    do
        echo $'\e[32m'"$line"$'\e[22m'
        echo "$line" >&2
    done
}

它们分别以红色和绿色输出输入线,并且two还写入普通标准错误。您可以在循环内做任何您想做的事情,或者将输入发送到另一个程序或您需要的任何内容。


注意一旦你在这一点上,就没有真正的“保留线路顺序”之类的东西了- 它们是两个独立的流和独立的进程,调度程序很可能会运行一个进程一段时间,然后再运行另一个进程,数据保存在内核管道缓冲区中直到被读取。我一直在使用一个函数将奇数输出到 stdout,甚至输出到 stderr 进行测试,并且通常会从每个函数中连续运行十几个或更多。

可以得到更接近终端中显示的顺序,但可能不是 Bash 中的顺序。交流程序使用pipeselect可以重建一个排序(尽管仍然不能保证与显示的相同,出于同样的原因:您的流程可能暂时不会被安排,一旦出现积压,就无法知道最先发生的是什么1)。如果顺序非常重要,您将需要另一种方法,并且可能需要基本可执行文件的协作。如果这是一个硬性要求,您可能需要重新考虑。

1可能还有特定于平台的方法来控制管道缓冲,但它们也不太可能为您提供足够的帮助。在 Linux 上,您可以将缓冲区大小缩小到系统页面大小,但不能更低。我不知道有什么可以让你立即强制写入(或阻止直到它们被读取)。无论如何,它们很可能以 stdio 流的方式在源端进行缓冲,然后您将永远看不到排序。

答案2

这是我用来将 stderr 和 stdout 分别重定向到函数的方法:

tag() { awk '$0="['$1'] "$0; fflush();' ; }

{
    {
        echo std1; sleep 0.1;
        echo error2 >&2; sleep 0.1;
        echo error3 >&2; sleep 0.1;
        echo std4; sleep 0.1;
    } 2>&1 1>&3 | tag STDERR 1>&2 ;
} 3>&1 | tag STDOUT 3>&- ;

这应该给出以下输出:

[STDOUT] std1
[STDERR] error2
[STDERR] error3
[STDOUT] std4

它适用于我的 bash 和 ksh (93u+)。

请注意,我在命令之间添加了“sleep 0.1”,以便在这种情况下保留行顺序。

答案3

这是一种捕获方式标准输出标准错误到不同的命令管道。

#!/bin/bash
#
exec 3>&1 4>&2

loggerForStdout() {
    # FD 3 is the original stdout
    local x
    while IFS= read -r x; do printf "STDOUT\t%s\n" "$x"; done >&3
}
loggerForStderr() {
    # FD 4 is the original stderr
    local x
    while IFS= read -r x; do printf "STDERR\t%s\n" "$x"; done >&4
}


( (
    # This is where your command would be put
    # e.g. stdbuf -oL -eL rsync -a /from /to
    #
    echo this is stdout; echo this is stderr >&2

) | loggerForStdout ) 2>&1 | loggerForStderr

我不完全确定即使使用stdbuf -oL -eL.

如果您有替代记录器,您可以省略前三行。它们的存在只是为了使代码成为完整且独立的示例。

相关内容