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

时间:2010-07-03 23:04:15

标签: bash shell redirect

我知道如何将stdout 重定向到文件:

exec > foo.log
echo test

这会将'test'放入foo.log文件中。

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

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

script | tee foo.log

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

我试过

exec | tee foo.log

但它不起作用。

9 个答案:

答案 0 :(得分:279)

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

答案 1 :(得分:162)

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

./script.sh >/dev/null

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

./script.sh 2>/dev/null

会将foobar输出到终端。显然,事实并非如此 普通用户可能期望的行为。这可以 通过使用两个单独的T形过程来固定,这两个过程都附加在同一个 日志文件:

#!/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行完全分离,请考虑使用两个日志文件,每行可能带有日期戳前缀,以便稍后按时间顺序重组。

答案 2 :(得分:25)

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

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

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

此外,对于使用命名管道的任何人来说,rm $PIPE是安全的,因为这会将管道与VFS断开连接,但使用它的进程仍会保留引用计数,直到它们完成为止。< / p>

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

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

答案 3 :(得分:18)

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

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

答案 4 :(得分:13)

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

在顶部添加此行:

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

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

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

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

答案 5 :(得分:9)

使用接受的答案我的脚本会提前返回(在'exec&gt;&gt;(tee ...)之后'),让我的脚本的其余部分在后台运行。由于我无法按照自己的方式使用该解决方案,因此我找到了另一种解决方案/解决问题:

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

# Rest of my script

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

请注意'exec&amp;&gt;'重定向stdout和stderr,如果我们愿意,我们可以单独重定向它们,或者更改为'exec&gt;'如果我们只想要stdout。

即使在脚本开头将管道从文件系统中删除,它也会继续运行,直到进程完成。我们无法使用rm-line之后的文件名来引用它。

答案 6 :(得分:1)

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

答案 7 :(得分:0)

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

答案 8 :(得分:-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

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