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

251

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

exec > foo.log
echo test

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

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

也就是说,可以轻松地从脚本外部完成:

script | tee foo.log

但是我想在脚本内部声明它

我尝试过了

exec | tee foo.log

但它没有起作用。


4
你的问题措辞不太清楚。 当你调用“exec > foo.log”时,脚本的标准输出就是文件foo.log。我想你的意思是希望将输出同时发送到foo.log和终端/tty上,因为输出到foo.log本身就是输出到标准输出(stdout)。 - William Pursell
我想做的是在“exec”上使用“|”。这对我来说非常完美,例如“exec | tee foo.log”,但不幸的是,您不能在执行调用上使用管道重定向。 - Vitaly Kushner
9个回答

316
#!/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 命令是在子 shell 而不是子进程中运行,因此不能使用 wait 命令将输出与调用进程同步。你需要的是一个类似于 http://bogomips.org/rainbows.git/commit/?id=95417ca711a75612da86a25acd20134efdbc0e67 的非缓冲 Tee 命令版本。 - user246672
14
@Barry: POSIX 规定 tee 命令不应该对其输出进行缓冲。 如果在大多数系统上它进行了缓冲,则在大多数系统上它是有问题的。 这是 tee 实现的问题,而不是我的解决方案的问题。 - DevSolar
3
@Sebastian:exec 命令非常强大,但也非常复杂。你可以将当前的标准输出备份到另一个文件描述符中,然后在以后恢复它。搜索 "bash exec 教程",有许多高级内容可供学习。 - DevSolar
2
那么,是否有一种方法可以恢复输出,或者强制将某些内容输出到真正的原始 STDOUT 中? - abourget
13
我建议在使用 tee 命令时加上 -i 参数。否则,信号中断(trap)会打断主脚本的标准输出流。比如说,如果你设置了一个 trap 'echo foo' EXIT 并按下 ctrl+c,你将看不到 "foo"。 因此,我会修改答案为 exec &> >(tee -ia file) - JamesThomasMoon
显示剩余32条评论

188

接受的答案没有将 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
22
建议在使用 tee 命令时加上 -i 参数,否则脚本中的标准输出可能会被信号中断所打断。例如,如果你使用 trap 'echo foo' EXIT 命令,并且按下 ctrl+c 键,你将看不到 "foo" 输出。因此,我建议将命令修改为 exec > >(tee -ia foo.log) - JamesThomasMoon
我基于此编写了一些小的“可源代码化”的脚本。可以在脚本中像. log. log foo.log这样使用它们:http://sam.nipl.net/sh/log http://sam.nipl.net/sh/log-a - Sam Watkins
3
这种方法的问题在于发送到STDOUT的消息会先作为一批出现,然后是发送到STDERR的消息。它们不像通常期望的那样交错出现。 - CMCDragonkai

29

解决方案适用于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.

21
在您的脚本文件中,将所有命令放在括号中,像这样:

在您的脚本文件中,将所有命令放在括号中,像这样:

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

5
请严谨地翻译,也可以使用大括号({} - glenn jackman
嗯,是的,我考虑过了,但这不是当前shell标准输出的重定向,这有点作弊,实际上你正在运行一个子shell并对其进行常规管道重定向。虽然可以工作,但我对此持保留态度,还有“tail -f foo.log &”解决方案。我会再等一段时间看看是否有更好的解决方案出现。如果没有,可能就会选择这个了 ;) - Vitaly Kushner
8
{ } 执行当前 shell 环境中的列表。( ) 在一个子 shell 环境中执行列表。 - user246672
1
该上面被接受的答案对我没有用,我试图在Windows系统上的MingW下安排运行脚本。我相信它抱怨了未实现的进程替代。经过以下更改后,这个答案完全有效,以捕获stderr和stdout: -) | tee foo.log +) 2>&1 | tee foo.log - Jon Carter
对我来说,这个答案比被接受的那个答案要简单易懂得多,并且不像被接受的那个答案一样在脚本完成后仍然保持重定向输出。 - Ben Farmer

15

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

在顶部添加以下行:

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

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

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

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

看起来你的解决方案只将标准输出发送到一个单独的文件。我如何将标准输出和标准错误发送到一个单独的文件? - mles

13

使用被接受的答案后,我的脚本一直异常早地返回(在“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;-) - oHo
运行良好。我不理解 tee < ${logfile}.pipe $logfile & 中的 $logfile 部分。具体来说,我尝试更改为 (tee | grep -v '^+.*$') < ${logfile}.pipe $logfile & 以捕获完整的扩展命令日志行(从 set -x)到文件,同时仅在标准输出中显示没有前导 '+' 的行,但收到有关 $logfile 的错误消息。您能否更详细地解释一下 tee 行? - Chris Johnson
我测试了一下,似乎这个答案没有保留 STDERR(它与 STDOUT 合并),因此如果您依赖流分开进行错误检测或其他重定向,您应该查看 Adam 的答案。 - HeroCC

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

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

第二种方法如果脚本出现问题,可能会留下一个管道文件,这可能是个问题,也可能不是(例如,您可以在父 shell 中使用 rm 命令删除它)。

2
tail 命令会在后台留下一个正在运行的进程。在第二个脚本中,tee 命令会阻塞,或者你需要在命令后加上 & 符号来使其像第一个脚本一样留下进程。 - Vitaly Kushner
@Vitaly:哎呀,忘了在tee命令后加上后台运行符号 - 我已经编辑过了。就像我说的那样,这两种方法都不是完美的解决方案,但是当它们的父进程终止时,后台进程也会被杀死,所以你不必担心它们会永远占用资源。 - David Z
1
哎呀:这看起来很有吸引力,但 tail -f 的输出也将流向 foo.log。你可以在 exec 前运行 tail -f 来修复它,但是在父进程终止后,tail 仍然会保持运行状态。您需要显式地杀死它,可能是在一个 trap 0 中进行。 - William Pursell
是的。如果脚本在后台运行,它会留下许多进程。 - user246672

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接