从 bash 脚本本身将 stdout 的副本重定向到日志文件

Posted

技术标签:

【中文标题】从 bash 脚本本身将 stdout 的副本重定向到日志文件【英文标题】:redirect COPY of stdout to log file from within bash script itself 【发布时间】:2011-03-11 12:42:56 【问题描述】:

我知道如何重定向标准输出到一个文件:

exec > foo.log
echo test

这会将“测试”放入 foo.log 文件中。

现在我想将输出重定向到日志文件并将其保留在标准输出中

即它可以从脚本外部轻松完成:

script | tee foo.log

但我想在脚本本身中声明它

我试过了

exec | tee foo.log

但是没有用。

【问题讨论】:

您的问题措辞不当。当您调用 'exec > foo.log' 时,脚本的标准输出 文件 foo.log。我认为你的意思是你希望输出去 foo.log 和 tty,因为去 foo.log 去 stdout。 我想做的是使用 |在“执行”上。这对我来说是完美的,即“exec | tee foo.log”,不幸的是你不能在 exec 调用中使用管道重定向 相关:How do I redirect the output of an entire shell script within the script itself? 【参考方案1】:
#!/usr/bin/env bash

# Redirect stdout ( > ) into a named pipe ( >() ) running "tee"
exec > >(tee -i logfile.txt)

# Without this, only stdout would be captured - i.e. your
# log file would not contain any error messages.
# SEE (and upvote) the answer by Adam Spiers, which keeps STDERR
# as a separate stream - I did not want to steal from him by simply
# adding his answer to mine.
exec 2>&1

echo "foo"
echo "bar" >&2

请注意,这是bash,而不是sh。如果您使用sh myscript.sh 调用脚本,您将收到与syntax error near unexpected token '>' 类似的错误。

如果您正在使用信号陷阱,您可能需要使用tee -i 选项以避免在出现信号时中断输出。 (感谢 JamesThomasMoon1979 的评论。)


根据写入管道还是终端来更改其输出的工具(例如,ls 使用颜色和列输出)将检测到上述构造意味着它们输出到管道。

有一些选项可以强制着色/列化(例如ls -C --color=always)。请注意,这将导致颜色代码也被写入日志文件,使其不那么可读。

【讨论】:

大多数系统上的 Tee 是缓冲的,因此在脚本完成之前输出可能不会到达。此外,由于此 tee 运行在子 shell 中,而不是子进程中,因此不能使用 wait 将输出同步到调用进程。你想要的是一个类似于bogomips.org/rainbows.git/commit/… 的无缓冲版本的 tee @Barry: POSIX 指定tee 不应缓冲其输出。如果它确实在大多数系统上缓冲,那么它在大多数系统上都会损坏。这是tee 实现的问题,而不是我的解决方案。 @Sebastian:exec 非常强大,但也非常投入。您可以将当前标准输出“备份”到不同的文件描述符,然后再将其恢复。谷歌“bash exec 教程”,那里有很多高级的东西。 @AdamSpiers:我也不确定 Barry 是关于什么的。 Bash的exec记录不启动新进程,>(tee ...)是标准的命名管道/进程替换,重定向中的&当然与后台无关... ?:-) 我建议将-i 传递给tee。否则,信号中断(陷阱)将破坏主脚本中的标准输出。例如,如果您有一个trap 'echo foo' EXIT,然后按ctrl+c,您将看不到“foo”。所以我会将答案修改为exec &> >(tee -ia file)【参考方案2】:

接受的答案不会将 STDERR 保留为单独的文件描述符。这意味着

./script.sh >/dev/null

不会将bar输出到终端,只会输出到日志文件,并且

./script.sh 2>/dev/null

foobar 输出到终端。显然那不是 普通用户可能期望的行为。这可以是 通过使用两个单独的 tee 进程来修复,这两个进程都附加到相同的 日志文件:

#!/bin/bash

# See (and upvote) the comment by JamesThomasMoon1979 
# explaining the use of the -i option to tee.
exec >  >(tee -ia foo.log)
exec 2> >(tee -ia foo.log >&2)

echo "foo"
echo "bar" >&2

(请注意,上述内容最初不会截断日志文件 - 如果您想要这种行为,您应该添加

>foo.log

到脚本的顶部。)

POSIX.1-2008 specification of tee(1) 要求输出是无缓冲的,即甚至不是行缓冲的,因此在这种情况下,STDOUT 和 STDERR 可能最终在 foo.log 的同一行上;但是,这也可能发生在终端上,因此日志文件将忠实反映终端上 可以看到的内容,如果不是它的精确镜像的话。如果您希望 STDOUT 行与 STDERR 行完全分离,请考虑使用两个日志文件,每行可能带有日期戳前缀,以便以后按时间顺序重新组装。

【讨论】:

出于某种原因,在我的情况下,当从 c 程序 system() 调用执行脚本时,即使在主脚本退出后,两个 tee 子进程仍然存在。所以我不得不添加这样的陷阱:exec > >(tee -a $LOG)trap "kill -9 $! 2>/dev/null" EXITexec 2> >(tee -a $LOG >&2)trap "kill -9 $! 2>/dev/null" EXIT 我建议将-i 传递给tee。否则,信号中断(陷阱)将破坏脚本中的标准输出。例如,如果您trap 'echo foo' EXIT 然后按ctrl+c,您将看不到“foo”。所以我会将答案修改为exec > >(tee -ia foo.log) 我在此基础上制作了一些“可溯源”的小脚本。可以在 . log. log foo.log 之类的脚本中使用它们:sam.nipl.net/sh/log sam.nipl.net/sh/log-a 这种方法的问题是发往STDOUT的消息首先作为一个批次出现,然后发往STDERR的消息出现。它们没有像通常预期的那样交错。【参考方案3】:

busybox、macOS bash 和非 bash shell 的解决方案

公认的答案无疑是 bash 的最佳选择。我在无法访问 bash 的 Busybox 环境中工作,并且它不理解 exec > >(tee log.txt) 语法。它也没有正确执行exec >$PIPE,试图创建一个与命名管道同名的普通文件,但失败并挂起。

希望这对没有 bash 的其他人有用。

此外,对于使用命名管道的任何人,rm $PIPE 是安全的,因为这会取消管道与 VFS 的链接,但使用它的进程仍然会在其上维护一个引用计数,直到它们完成。

请注意,使用 $* 不一定安全。

#!/bin/sh

if [ "$SELF_LOGGING" != "1" ]
then
    # The parent process will enter this branch and set up logging

    # Create a named piped for logging the child's output
    PIPE=tmp.fifo
    mkfifo $PIPE

    # Launch the child process with stdout redirected to the named pipe
    SELF_LOGGING=1 sh $0 $* >$PIPE &

    # Save PID of child process
    PID=$!

    # Launch tee in a separate process
    tee logfile <$PIPE &

    # Unlink $PIPE because the parent process no longer needs it
    rm $PIPE    

    # Wait for child process, which is running the rest of this script
    wait $PID

    # Return the error code from the child process
    exit $?
fi

# The rest of the script goes here

【讨论】:

这是迄今为止我见过的唯一适用于 mac 的解决方案【参考方案4】:

在您的脚本文件中,将所有命令放在括号内,如下所示:

(
echo start
ls -l
echo end
) | tee foo.log

【讨论】:

学究式,也可以使用大括号 () 嗯,是的,我考虑过,但这不是当前 shell 标准输出的重定向,它是一种欺骗,你实际上运行一个子 shell 并对其进行常规的 piper 重定向。作品思想。我对此和“tail -f foo.log &”解决方案意见不一。会稍等一下,看看是否可能是一个更好的表面。如果不能解决;) 在当前 shell 环境中执行一个列表。 ( ) 在子 shell 环境中执行列表。 该死的。谢谢你。那里接受的答案对我不起作用,试图安排一个脚本在 Windows 系统上的 MingW 下运行。我相信,它抱怨未实现的流程替换。在进行以下更改后,此答案可以很好地捕获标准错误和标准输出:```-) |三通 foo.log +) 2>&1 | tee foo.log 对我来说,这个答案比接受的答案更简单、更容易理解,并且在脚本完成后也不会像接受的答案那样继续重定向输出!【参考方案5】:

将 bash 脚本记录到 syslog 的简单方法。脚本输出可通过/var/log/syslog 和stderr 获得。 syslog 将添加有用的元数据,包括时间戳

在顶部添加这一行:

exec &> >(logger -t myscript -s)

或者,将日志发送到单独的文件:

exec &> >(ts |tee -a /tmp/myscript.output >&2 )

这需要moreutils(用于添加时间戳的ts 命令)。

【讨论】:

您的解决方案似乎只将标准输出发送到单独的文件。如何将 stdout 和 stderr 发送到单独的文件?【参考方案6】:

使用接受的答案,我的脚本一直异常早地返回(在 'exec > >(tee ...)' 之后),让我的脚本的其余部分在后台运行。由于我无法让该解决方案按我的方式工作,因此我找到了另一个解决方案/解决该问题:

# Logging setup
logfile=mylogfile
mkfifo $logfile.pipe
tee < $logfile.pipe $logfile &
exec &> $logfile.pipe
rm $logfile.pipe

# Rest of my script

这使得脚本的输出从进程通过管道进入“tee”的子后台进程,该进程将所有内容记录到光盘和脚本的原始标准输出。

请注意,'exec &>' 会同时重定向 stdout 和 stderr,如果我们愿意,我们可以分别重定向它们,或者如果我们只想要 stdout,则更改为 'exec >'。

即使您在脚本开始时从文件系统中删除了管道,它仍将继续运行,直到进程完成。我们只是不能使用 rm 行后面的文件名来引用它。

【讨论】:

与second idea from David Z 类似的答案。看看它的cmets。 +1 ;-) 效果很好。我不理解tee &lt; $logfile.pipe $logfile &amp;$logfile 部分。具体来说,我尝试更改它以将完整的扩展命令日志行(从set -x)捕获到文件,同时通过更改为(tee | grep -v '^+.*$') &lt; $logfile.pipe $logfile &amp; 仅在标准输出中显示没有前导“+”的行,但收到​​有关$logfile 的错误消息。你能更详细地解释一下tee 行吗? 我对此进行了测试,似乎这个答案并没有保留 STDERR(它与 STDOUT 合并),所以如果你依赖流分开来进行错误检测或其他重定向,你应该看看亚当的回答。【参考方案7】:

Bash 4 有一个coproc 命令,它为命令建立一个命名管道并允许您通过它进行通信。

【讨论】:

【参考方案8】:

不能说我对任何基于 exec 的解决方案都很满意。我更喜欢直接使用 tee,所以我在请求时使用 tee 使脚本自己调用:

# my script: 

check_tee_output()

    # copy (append) stdout and stderr to log file if TEE is unset or true
    if [[ -z $TEE || "$TEE" == true ]]; then 
        echo '-------------------------------------------' >> log.txt
        echo '***' $(date) $0 $@ >> log.txt
        TEE=false $0 $@ 2>&1 | tee --append log.txt
        exit $?
    fi 


check_tee_output $@

rest of my script

这允许你这样做:

your_script.sh args           # tee 
TEE=true your_script.sh args  # tee 
TEE=false your_script.sh args # don't tee
export TEE=false
your_script.sh args           # tee

您可以自定义它,例如将 tee=false 设为默认值,让 TEE 保存日志文件,等等。我猜这个解决方案类似于 jbarlow 的,但更简单,也许我的有一些我还没有遇到的限制。

【讨论】:

【参考方案9】:

这些都不是完美的解决方案,但您可以尝试以下几件事:

exec >foo.log
tail -f foo.log &
# rest of your script

PIPE=tmp.fifo
mkfifo $PIPE
exec >$PIPE
tee foo.log <$PIPE &
# rest of your script
rm $PIPE

如果你的脚本出现问题,第二个会留下一个管道文件,这可能是也可能不是问题(即,也许你可以在之后在父 shell 中 rm 它)。

【讨论】:

tail 将在第二个脚本中留下一个正在运行的进程 tee 将阻塞,或者您需要使用 & 运行它,在这种情况下它将像第一个脚本一样离开进程。 @Vitaly:哎呀,忘了背景tee - 我已经编辑过了。正如我所说,两者都不是一个完美的解决方案,但是当它们的父 shell 终止时,后台进程会被杀死,所以你不必担心它们会永远占用资源。 哎呀:这些看起来很吸引人,但 tail -f 的输出也将进入 foo.log。您可以通过在 exec 之前运行 tail -f 来解决这个问题,但是在父级终止后 tail 仍然运行。您需要明确地杀死它,可能在陷阱 0 中。 是的。如果脚本是后台的,它会留下所有进程。

以上是关于从 bash 脚本本身将 stdout 的副本重定向到日志文件的主要内容,如果未能解决你的问题,请参考以下文章

如何从脚本本身中获取 Bash 脚本的源目录?

如何从脚本本身中获取 Bash 脚本的源目录?

如何将实时stdout流拆分成几个文件?

简化bash脚本,以sudo身份运行时从命令中提取第一个单词

C++ 启动脚本(bash、python ...)并使用 stdin/stdout 进行数据传输 [linux]

text 从脚本本身获取Bash脚本的源目录