从 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
将foo
和bar
输出到终端。显然那不是
普通用户可能期望的行为。这可以是
通过使用两个单独的 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" EXIT
exec 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 < $logfile.pipe $logfile &
的$logfile
部分。具体来说,我尝试更改它以将完整的扩展命令日志行(从set -x
)捕获到文件,同时通过更改为(tee | grep -v '^+.*$') < $logfile.pipe $logfile &
仅在标准输出中显示没有前导“+”的行,但收到有关$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脚本,以sudo身份运行时从命令中提取第一个单词