重定向复制的标准输出到日志文件从bash脚本本身

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

exec > foo.log
echo test

这将把“test”放到foo.log文件中。

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

也就是说,它可以在脚本之外简单地完成:

script | tee foo.log

但我想在脚本中声明它

我试着

exec | tee foo.log

但这并没有起作用。

164529 次浏览

这两种方法都不是完美的解决方案,但你可以尝试以下几件事:

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它)。

在你的脚本文件中,把所有的命令都放在括号里,就像这样:

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

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

#!/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)。注意,这将导致颜色代码也写入日志文件,使其可读

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

公认的答案当然是bash的最佳选择。我在Busybox环境中工作,没有访问bash,它不理解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

使用已接受的答案,我的脚本总是异常早地返回(就在'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行之后使用文件名引用它。

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

./script.sh >/dev/null

将不输出bar到终端,只输出到日志文件,和

./script.sh 2>/dev/null

将同时向终端输出foobar。显然不是这样的 一个正常用户可能会期待的行为。这可以是 通过使用两个独立的三通过程都附加到相同的固定 日志文件:< / p >

#!/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

到脚本的顶部。)

tee(1)的POSIX.1-2008规范要求输出是无缓冲的,即甚至不是行缓冲的,所以在这种情况下,STDOUT和STDERR可能会在foo.log的同一行上结束;然而,这也可能发生在终端上,所以日志文件将是终端上所看到的可以的忠实反映,如果不是它的精确镜像的话。如果希望STDOUT行与STDERR行干净地分开,可以考虑使用两个日志文件,可能在每行上都有日期戳前缀,以便以后按时间顺序重新组合。

使bash脚本日志转换为syslog的简单方法。脚本输出可以通过/var/log/syslog和stderr获得。Syslog将添加有用的元数据,包括时间戳。

在顶部添加这一行:

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

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

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

这需要moreutils(对于ts命令,它添加时间戳)。

不能说我对任何基于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的类似,但是更简单,也许我的解决方案有我还没有遇到的局限性。