在一个脚本中一次将bash输出stderr和stdout记录到多个文件中

问题描述

假设您有一个bash脚本,并且要打印并将输出(stderr和stdout)保存到日志文件。基于以下答案:@cdarke的https://stackoverflow.com/a/49514467/835098,这就是您的操作方式。

#!/bin/bash

exec > >(tee my.log) 2>&1

echo "Hello"

但是,如果您有一个脚本,其中需要将不同部分转到不同的日志文件,该怎么办?假设您要将典型的configuremakemake test输出分开到各自的日志文件中?天真的方法可能看起来像这样(为了简单起见,configure在这里变成了echo):

#!/bin/bash

# clear logs
rm -f configure.log make.log make_test.log

exec > >(tee configure.log) 2>&1
echo "configure"

exec > >(tee make.log) 2>&1
echo "make"

exec > >(tee make_test.log) 2>&1
echo "make test"

但是在执行此脚本时,您会注意到只有最后一个输出包含了应该包含的内容

$ tail *.log
==> configure.log <==
configure
make
make test

==> make.log <==
make
make test

==> make_test.log <==
make test

还要注意,每个日志文件都以正确的输出开头。我考虑过将一个中间文件复制到最终目的地后,坚持使用一个日志文件并截断​​它。该脚本有效,但我想知道它是否有用:

#!/bin/bash

# clear logs
rm -f configure.log make.log make_test.log tmp.log

exec > >(tee tmp.log) 2>&1
echo "configure"
cp tmp.log configure.log && truncate -s 0 tmp.log

echo "make"
cp tmp.log make.log && truncate -s 0 tmp.log

echo "make test"
cp tmp.log make_test.log  && truncate -s 0 tmp.log

以下是生成的日志文件

$ tail *.log
==> configure.log <==
configure

==> make.log <==
make

==> make_test.log <==
make test

==> tmp.log <==

例如,该方法的缺点是,如果命令成功,则最终的日志文件将可用。实际上,这非常糟糕,也是找到其他解决方案的充分理由。

解决方法

原因是因为您在每次对exec的调用中都复制了stdout文件描述符,从而导致对所有三个文件的所有三个echo调用的输出。即在设置了初始exec之后,当您这样做

exec > >(tee make.log) 2>&1
echo "make"

不仅最近的echo'd语句进入了make.log,而且也进入了configure.log,因为您先前的exec确实为标准输出的内容设置了对该日志文件的写入。现在,第一个日志文件包含来自两个echo'd语句的行。在您的下一种情况下也会再次复制。

为避免此问题,请为每种唯一情况设置不同的文件描述符,即

exec 3> >(tee configure.log) 2>&1
echo "configure" >&3

exec 4> >(tee make.log) 2>&1
echo "make" >&4

exec 5> >(tee make_test.log) 2>&1
echo "make test" >&5

# Releasing the file descriptors back to shell
exec 3>&-
exec 4>&-
exec 5>&-

或者如果您使用的bash版本> 4.1,则可以让shell为您分配文件描述符,这样您就不必显式选择数字了。

exec {fd1}> >(tee configure.log) 2>&1
echo "configure" >&"$fd1"

exec {fd2}> >(tee make.log) 2>&1
echo "make" >&"$fd2"

exec {fd3}> >(tee make_test.log) 2>&1
echo "make test" >&"$fd3"

执行exec {fd1}>时,bash会自动使用文件描述符(通常大于10)填充fd1变量。现在,您可以像以前一样在通话中使用$fd1tee。和以前一样,您可以通过将其关闭为

来释放使用的描述符
exec >&"$fd1"-
exec >&"$fd2"-
exec >&"$fd3"-

如果您还希望stderr也转到相应的日志文件,请将重定向2>&1更改为打开的相应文件描述符,即

2>&3        
2>&"$fd1"