将我的bash输出重定向到日志文件

6

我现在正在执行一系列的bash脚本,并希望将我运行的命令的值捕获到输出文件中。

我以为这可以通过将我的命令与| tee -a outputfile.txt结合使用来实现,但这只适用于脚本1。

脚本2..4等没有将输出附加到文件中。

看了看周围,输出也可以通过这种方式重定向 - 1>&1 - 但有时我看到数字1被数字2取代。我想这与消息类型有关。然而,由于我不知道如何调用此输出重定向,所以我无法找到信息。

需要帮助吗? 谢谢 安德烈亚


“输出重定向”是正确的术语:http://www.gnu.org/software/bash/manual/html_node/Redirections.html - Michael Berkowski
你能展示一下如何调用这些脚本吗?管道符(|)和tee没问题,问题肯定在于你调用剩下的脚本的方式。 - Christian Fritz
顺便提一下,“2>&1”语法只是表示标准错误输出应与(正常的)标准输出输出合并。数字“1”和“2”分别代表标准输出和标准错误输出。 - Christian Fritz
5个回答

7

基本技术

有多种方法可用于实现它:

exec >outputfile.txt
command1
command2
command3
command4

这会将整个脚本的标准输出改为日志文件。
通常我喜欢这样做:
{
command1
command2
command3
command4
} > outputfile.txt

这个功能可以对大括号内的所有命令进行I/O重定向。但要小心,必须像对待命令一样处理{},它们不能出现在任何地方。这不会创建子shell——这也是我喜欢它的主要原因。

你也可以用圆括号代替大括号:

(
command1
command2
command3
command4
) > outputfile.txt

在放置括号方面,你可以更加随意,相比之下花括号就不然了,所以:

(command1
 command2
 command3
 command4) > outputfile.txt

我也会这样工作(但要用大括号,否则shell将无法找到命令{command1(除非您恰好有一个可执行的文件在周围并...)。 这将创建一个子shell。在括号内完成的任何变量赋值都不会在括号外看到/访问。这有时可能是一个难以解决的问题(但不总是)。 子shell的增量成本基本可以忽略不计;它存在,但您很可能难以衡量。

还有一种冗长的方式:

command1 >>outputfile.txt
command2 >>outputfile.txt
command3 >>outputfile.txt
command4 >>outputfile.txt

如果您想表明自己是新手shell程序员,那么请使用这种技术。如果您希望被视为更高级的shell程序员,则不要使用它。
请注意,上面的所有命令都将标准输出重定向到指定的文件,使标准错误保持原始目的地(通常为终端)。如果要让标准错误也进入同一个文件,请在标准输出的重定向之后添加“2>&1”(意思是将文件描述符2(标准错误)发送到与文件描述符1(标准输出)相同的位置)。
应用技巧:
通过使用“2>&1 >> $_logfile”(如下所述),我得到了我需要的东西,但现在我的echo…也在输出文件中。有没有办法同时在屏幕和文件中打印它们?
啊,所以你不想让所有内容都进入文件……这使事情变得复杂了一些。当然有方法;不一定是直接的。 我可能会使用“exec 3>&1;”将文件描述符3设置为与1(标准输出——或者使用“3>&2”如果我想将回声输出到标准错误)相同的位置,在主重定向之前。然后我会创建一个函数“echoecho() { echo "$*"; echo "$*" >&3; }”,我会使用“echoecho Whatever”来进行回显。当您完成文件描述符3时(如果您不打算退出,则系统会将其关闭),可以使用“exec 3>& -”将其关闭。
关于“exec”的问题:
不;我指的是Bash(shell)内置命令“exec”。它可用于永久执行I / O重定向(对于脚本的其余部分),或者用于替换当前脚本为新程序,例如“exec ls -l”——这可能是一个糟糕的例子。
关于示例的请求:
评论的缺点是它们难以格式化并且大小有限。 这些限制也是优点,但是当答案需要扩展时就必须加以扩展。该时间已经到了。

为了讨论方便,本文将限制使用两个命令而非四个(但这并不影响其普适性)。这两个命令分别是 cmd1cmd2,实际上它们是同一个脚本的不同名称:

#!/bin/bash
for i in {01..10}
do
  echo "$0: stdout $i - $*"
  echo "$0: stderr $i - error message" >&2
done

正如您所看到的,此脚本将消息写入标准输出和标准错误。例如:

$ ./cmd1 trying to work
./cmd1: stdout 1 - trying to work
./cmd1: stderr 1 - error message
./cmd1: stdout 2 - trying to work
./cmd1: stderr 2 - error message

./cmd1: stdout 9 - trying to work
./cmd1: stderr 9 - error message
./cmd1: stdout 10 - trying to work
./cmd1: stderr 10 - error message
$

现在,从Andrea Moro发布的答案中,我们发现:

#!/bin/bash

_logfile="output.txt"

# Delete output file if exist
if [ -f $_logfile ];
then
    rm $_logfile
fi

for file in ./shell/*
do
  $file 2>&1 >> $_logfile
done

我不喜欢以_开头的变量名;我认为没有必要。这会将错误重定向到标准输出的位置(当前位置),然后将标准输出重定向到日志文件。所以,如果子目录shell包含cmd1cmd2,则输出结果为:

$ bash ex1.sh
./shell/cmd1: stderr 1 - error message
./shell/cmd1: stderr 2 - error message

./shell/cmd1: stderr 9 - error message
./shell/cmd1: stderr 10 - error message
./shell/cmd2: stderr 1 - error message
./shell/cmd2: stderr 2 - error message

./shell/cmd2: stderr 9 - error message
./shell/cmd2: stderr 10 - error message
$

要将标准输出和标准错误输出到文件中,您需要使用以下方法之一:

2>>$_logfile >>$_logfile
>>$_logfile 2>&1

通常情况下,I/O 重定向操作是从左往右依次进行的,但如果你使用了管道符 |& ,则它会在处理 I/O 重定向之前控制标准输出(和标准错误)的方向。

将该脚本适配为能够同时向标准输出和日志文件输出信息,有多种方式可供选择。在此我假设 shebang 行为 #!/bin/bash

logfile="output.txt"
rm -f $logfile

for file in ./cmd1 ./cmd2
do
    $file trying to work >> $logfile 2>&1 
done

这个命令会删除日志文件,如果存在的话(但输出信息比之前少了)。所有的标准输出和错误输出都会被记录到日志文件中。我们也可以这样写:

logfile="output.txt"
{
for file in ./cmd1 ./cmd2
do
    $file trying to work
done
} >$logfile 2>&1

或者代码可以使用括号代替花括号,功能上只有很小的差异,不会对该脚本产生实质性影响。 或者,在这种情况下,我们可以使用:

logfile="output.txt"
for file in ./cmd1 ./cmd2
do
    $file trying to work
done >$logfile 2>&1

而且不清楚这个变量是否必要,但我们将保留它。请注意,这两者都使用“覆盖”I/O重定向,因为它们只创建一次日志文件,这意味着没有必要删除它(尽管可能有理由这样做-与其他用户先前运行命令并留下不可写文件相关,但是您应该具有带日期时间戳的日志文件,因此这不是问题)。
显然,如果我们想要像原始标准输出一样回显某些内容,并将其记录到日志文件中,那么我们必须采取一些不同的措施,因为标准错误和标准输出都将进入日志文件。
其中一个选项是:
logfile="output.txt"
rm -f $logfile

for file in ./cmd1 ./cmd2
do
    echo $file $(date +'%Y-%m-%d %H:%M:%S')
    $file trying to work >> $logfile 2>&1 
done

另一种选择是:
exec 3>&1

logfile="output.txt"
for file in ./cmd1 ./cmd2
do
    echo $file $(date +'%Y-%m-%d %H:%M:%S') >&3
    $file trying to work
done >$logfile 2>&1

exec 3>&-

现在文件描述符3将与原始标准输出一起指向相同位置。 在循环内部,标准输出和标准错误都指向日志文件,但echo … >&3会将echo的标准输出发送到文件描述符3。

如果您希望相同的回显输出同时发送到重定向的标准输出和原始标准输出,则可以使用:

exec 3>&1
echoecho()
{
    echo "$*"
    echo "$*" >&3
}

logfile="output.txt"
for file in ./cmd1 ./cmd2
do
    echoecho $file $(date +'%Y-%m-%d %H:%M:%S')
    $file trying to work
done >$logfile 2>&1

exec 3>&-

这个的输出结果是:
$ bash ex3.sh
./cmd1 2014-01-07 14:57:13
./cmd2 2014-01-07 14:57:13
$ cat output.txt
./cmd1 2014-01-07 14:57:13
./cmd1: stdout 1 - trying to work
./cmd1: stderr 1 - error message
./cmd1: stdout 2 - trying to work
./cmd1: stderr 2 - error message

./cmd1: stdout 9 - trying to work
./cmd1: stderr 9 - error message
./cmd1: stdout 10 - trying to work
./cmd1: stderr 10 - error message
./cmd2 2014-01-07 14:57:13
./cmd2: stdout 1 - trying to work
./cmd2: stderr 1 - error message
./cmd2: stdout 2 - trying to work
./cmd2: stderr 2 - error message

./cmd2: stdout 9 - trying to work
./cmd2: stderr 9 - error message
./cmd2: stdout 10 - trying to work
./cmd2: stderr 10 - error message
$

这大致就是我在评论中所说的内容,完整地写出来。


嗨 Jon,我绝不想展示我是一个新手,但不幸的是我确实是。 :) 这是我能找到的唯一方法,所以我一直走这条路线。 - Andrea Moro
我对你说的几句话很好奇:在括号中创建的shell脚本变量在外部看不到。花括号也适用于这个规则吗? - Andrea Moro
1
不,{ ...; } 符号并不会启动一个子 shell,因此在该结构中对变量所做的更改是针对主 shell 中的变量而非子 shell。 - Jonathan Leffler
啊,所以你不想让所有的东西都写入文件...这使事情变得有点复杂。当然有方法;但不一定是直截了当的。我可能会使用 exec 3>&1; 将文件描述符 3 设置为与 1(标准输出)相同的位置 - 或者如果我想要回显到标准错误,则使用 3>&2 - 然后再进行主要重定向。然后我会创建一个函数 echoecho() { echo "$*"; echo "$*" >&3; },并使用 echoecho Whatever 来进行回显。当你完成文件描述符 3 的使用时(如果你不打算退出,系统将关闭它),你可以使用 exec 3>&- 关闭它。 - Jonathan Leffler
不,我指的是Bash(shell)内置命令exec。它可以用于永久进行I/O重定向(对于脚本的其余部分),或者用新程序替换当前脚本,例如exec ls -l . — 这可能是一个不好的示例。 - Jonathan Leffler
显示剩余3条评论

3
假设您有三个脚本文件:sc1.sh,sc2.sh和sc3.sh,并且您想将输出写入名为log.txt的文件中,您可以执行以下操作:
./sc1.sh >> log.txt
./sc2.sh >> log.txt
./sc3.sh >> log.txt

或者,如果你喜欢通过编写另一个Bash脚本来自动化该过程:

#!/bin/bash
n=1
num_scripts=3
while test $n -le $num_scripts
do
    script$n.sh >> log.txt
    n=$[n+1]
done

1

尝试

command >> file

追加到文件中。

command > file

每次清空文件,即从位置1开始写入。

这并不是一个通用的原则。因此我使用tee命令的原因。 - Andrea Moro

1
在一个子shell中运行您的命令(通过将所有命令嵌入一组括号中),然后将该子shell的输出导向tee(command1; command2; command3) | tee outputfile.txt

这是不可行的,因为整个脚本都是用来运行命令的。在一行内调用嵌套命令会影响可读性和代码维护。 - Andrea Moro
左括号可以在一行上;此答案中的三个命令可以分别放在三行上;右括号、管道和 tee 命令可以放在另一行上,就像我的答案中所示。 - Jonathan Leffler
或者只需将 command1...command3 替换为 script1...script3;原理完全相同,就像您在自己的解决方案中展示的那样。 - Emerick Rogul

0

好的,感谢您的贡献和建议。通过组合几个部分,我已经让脚本基本按照我的需求工作,并生成了输出文件。

简而言之,问题在于这些脚本的调用方式;一个外部的 Ruby 脚本启动了 shell 脚本,可能每次都创建了不同的 shell 环境。因此,tee 命令可能没有将输出附加到我的文件中(在每行末尾明确声明),因为它已经存在了。我不确定这是否正确。

所以,我的解决方案是

#!/bin/bash

_logfile="output.txt"

# Delete output file if exist
if [ -f $_logfile ];
then
    rm $_logfile
fi

for file in ./shell/*
do
  $file 2>&1 >> $_logfile
done

然而,正如我所说,我还面临着一个挑战......现在屏幕上完全看不到任何输出,而我很想至少读取我的“echo”命令,以便让我知道脚本执行的进度。

谢谢您在此方面的进一步建议。


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