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


235

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

exec > foo.log
echo test

这会将“ test”放入foo.log文件。

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

即可以从脚本外部简单地完成:

script | tee foo.log

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

我试过了

exec | tee foo.log

但这没用。


3
您的问题措辞不好。当你调用“EXEC> foo.log”,脚本的标准输出文件foo.log。我想你的意思是你想输出去foo.log和tty的,因为要foo.log 将标准输出。
威廉·珀塞尔

我想做的是使用| 在“执行”上。这对我来说是完美的,即“ exec | tee foo.log”,不幸的是您不能在exec调用上使用管道重定向
Vitaly Kushner,2010年

Answers:


297
#!/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)。请注意,这也会导致将颜色代码也写入日志文件,从而降低了可读性。


5
大多数系统上的Tee都已缓冲,因此直到脚本完成后才可能输出。而且,由于此tee在子外壳程序(而不是子进程)中运行,因此无法使用wait将输出同步到调用进程。您想要的是一个类似于bogomips.org/rainbows.git/commit/…

14
@Barry:POSIX指定tee不应缓冲其输出。如果它在大多数系统上都可以缓冲,则在大多数系统上都会损坏。这是实现的问题tee,而不是我的解决方案。
DevSolar '02

3
@Sebastian:exec非常强大,但也非常参与。您可以将当前标准输出“备份”到其他文件描述符,然后稍后将其恢复。Google的“ bash exec教程”,那里有很多高级的东西。
DevSolar

2
@AdamSpiers:我也不知道Barry是干什么的。Bash exec证明不会启动新流程,>(tee ...)是一个名为管道/流程替换的标准,并且&在重定向过程中当然与背景无关...?:-)
DevSolar 2012年

11
我建议传递-itee。否则,信号中断(陷阱)将破坏主脚本中的标准输出。例如,如果您有一个trap 'echo foo' EXIT,然后按ctrl+c,则不会看到“ foo ”。所以我将答案修改为exec &> >(tee -ia file)
JamesThomasMoon1979

173

接受的答案不会将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规范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
alveko 2014年

15
我建议传递-itee。否则,信号中断(陷阱)将破坏脚本中的标准输出。例如,如果您trap 'echo foo' EXIT再按ctrl+c,您将不会看到“ foo ”。所以我将答案修改为exec > >(tee -ia foo.log)
JamesThomasMoon1979

我基于此制作了一些小的“可获取的”脚本。可以用它们在一个脚本像. log. log foo.logsam.nipl.net/sh/log sam.nipl.net/sh/log-a
萨姆·沃特金斯

1
这种方法的问题在于,消息将STDOUT首先成批出现,然后再STDERR出现。它们没有通常所期望的交错。
CMCDragonkai

28

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上使用的解决方案
Mike Baglio Jr.

19

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

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

5
{}
花哨地

是的,我认为是这样,但这不是当前shell stdout的重定向,这是一种作弊,您实际上是在运行子shell并对其进行常规的管道程序重定向。工程思想。我和“ tail -f foo.log&”解决方案并存。会稍等一下,看看是否可能是更好的选择。如果不太可能会解决的话;)
维塔利·库什纳

8
{}在当前Shell环境中执行列表。()在子Shell环境中执行列表。

该死的。谢谢。在那里接受的答案对我没有用,试图安排一个脚本在Windows系统上的MingW下运行。我认为,它抱怨未执行的流程替代。在进行以下更改之后,此答案可以很好地捕获stderr和stdout:```-)| tee foo.log +)2>&1 | tee foo.log
Jon Carter

14

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

在顶部添加以下行:

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

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

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

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


10

使用被接受的答案,我的脚本总是异常地早返回(紧接在'exec>>(tee ...)'之后),而其余脚本仍在后台运行。由于无法以我的方式使用该解决方案,因此我找到了解决该问题的另一种解决方案/解决方案:

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

# Rest of my script

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

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

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


David Z第二个想法类似的答案。看看它的评论。+1 ;-)
olibre 2013年

效果很好。我不了解的$logfile部分tee < ${logfile}.pipe $logfile &。具体来说,我尝试将其更改为捕获完整的扩展命令日志行(从set -x)到文件,同时通过将更改为,(tee | grep -v '^+.*$') < ${logfile}.pipe $logfile &但收到有关的错误消息,从而仅在stdout中显示不带'+'的行$logfile。您能否tee更详细地说明这一行?
克里斯·约翰逊

我对此进行了测试,看来这个答案并没有保留STDERR(它与STDOUT合并了),因此,如果您依靠将流分开来进行错误检测或其他重定向,则应查看Adam的答案。
HeroCC


1

不能说我对基于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的解决方案相似,但是更简单,也许我的限制还没有得到解决。


-1

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

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

如果您的脚本出了点问题,第二个可能会留下一个管道文件,这可能会也可能不是问题(例如,之后您可以rm在父shell中使用它)。


1
tail将在第二个脚本中留下正在运行的进程tee将阻止,或者您将需要使用&运行它,在这种情况下,它将像第一个中那样离开进程。
维塔利·库什纳

@Vitaly:哎呀,忘了背景tee-我已经编辑了。就像我说的那样,这都不是一个完美的解决方案,但是当后台进​​程的父外壳终止时,它们将被杀死,因此您不必担心它们会永远占用资源。
David Z'7

1
Yikes:这些看起来很吸引人,但是tail -f的输出也将到达foo.log。您可以通过在exec之前运行tail -f来解决此问题,但是在父项终止后,tail仍然保持运行状态。您需要明确地杀死它,大概在一个陷阱0
威廉Pursell

是的 如果脚本是后台运行的,则将使进程无处不在。
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.