捕获并收集脚本输出,“输入文件是输出文件”错误?

dai*_*isy 2 bash file-descriptors trap error-handling

我需要上传当前脚本的输出,所以我添加了一个trapand set -ex,例如

#!/bin/bash

exec &> /tmp/error.log
trap 'cat /tmp/error.log; curl http://127.0.0.1/error.php?hostname=$(hostname) -F file=@/tmp/error.log' EXIT

set -ex
wtfwtf
Run Code Online (Sandbox Code Playgroud)

当我执行它时,我总是收到这个错误,而且 PHP 脚本没有收到整个文件

%> cat /tmp/error.log
1.sh: line 6: wtfwtf: command not found
cat: /tmp/error.log: input file is output file
Run Code Online (Sandbox Code Playgroud)

到目前为止,唯一的解决方案是将 error.log 复制到一个新文件并上传,例如

#!/bin/bash

exec &> /tmp/error.log
trap 'cp /tmp/error.log 123; curl http://127.0.0.1/error.php?hostname=$(hostname) -F file=@123' EXIT

set -ex
wtfwtf
Run Code Online (Sandbox Code Playgroud)

有没有更好的方法来做到这一点?

Kus*_*nda 6

使用exec,您将脚本的所有输出重定向到特定的日志文件。

在您的陷阱中,您希望使用cat. 由于所有输出也被重定向到该文件,GNUcat注意到它的输入文件和标准输出流(从 shell 继承)是同一件事,并拒绝执行它的任务。

BSDcat不像 GNUcat那样做同样的检查,如果脚本不被中断,它会产生一个无限大的日志文件,其中有几行一遍又一遍地重复。

解决方法是保存原始标准输出文件描述符,像以前一样进行重定向,然后将其恢复到陷阱中。

#!/bin/bash

exec 3>&1                  # make fd 3 copy of original fd 1
exec >/tmp/error.log 2>&1

# in the trap, make fd 1 copy of fd 3 and close fd 3 (i.e. move fd 3 to fd 1)
trap 'exec 1>&3-; cat /tmp/error.log; curl "http://127.0.0.1/error.php?hostname=$(hostname)" -F file=@/tmp/error.log' EXIT

set -ex
wtfwtf
Run Code Online (Sandbox Code Playgroud)

这会在将文件描述符 1(作为 fd 3)重定向到日志文件之前复制它。在陷阱中,我们将此副本移回 fd 1 并执行输出。

请注意,在此示例中,陷阱中的标准错误流仍连接到日志文件。因此,如果curl生成了诊断消息,这将保存在日志文件中,而不是显示在终端上(或原始标准错误流连接到的任何位置)。


考虑到 Stéphane Chazelas 的评论

#!/bin/sh

exit_handler () {
    # 1. Make standard output be the original standard error
    #    (by using fd 3, which is a copy of original fd 2)
    # 2. Do the same with standard error
    # 3. Close fd 3.
    exec >&3 2>&3 3>&-
    cat "$logfile"
    curl "some URL" -F "file=@$logfile"
}

logfile='/var/log/myscript.log'

# Truncate the logfile.
: >"$logfile"

# 1. Make fd 3 a copy of standard error (fd 2)
# 2. Redirect original standard output to the logfile (appending)
# 3. Redirect original standard error to the logfile (will also append)
exec 3>&2 >>"$logfile" 2>&1

# Use shell function for exit trap (for neatness)
trap exit_handler EXIT

set -ex
wtfwtf
Run Code Online (Sandbox Code Playgroud)

他的观点是,无论如何,日志文件仅用于诊断消息,因此将日志文件输出到原始标准错误流更有意义。

他还指出,在世界可写目录中使用固定文件名是危险的,例如/tmp. 这是因为脚本中没有进行检查以确保该文件不存在(例如,某人或某些恶意软件可能已经创建了/tmp/error.log指向/etc/passwd或您的符号链接~/.bashrc)。他对此的解决方案是使用专用的持久化日志文件/var/log代替脚本(该文件是持久化的,但在运行脚本时会清除内容)。

一种变体是用于mktemp在下面创建一个唯一的文件名$TMPDIR(然后在EXIT陷阱中删除该文件,除非curl失败,在这种情况下rm不会执行,因为set -e它有效):

#!/bin/sh

exit_handler () {
    # 1. Make standard output be the original standard error
    #    (by using fd 3, which is a copy of original fd 2)
    # 2. Do the same with standard error
    # 3. Close fd 3.
    exec >&3 2>&3 3>&-
    cat "$logfile"
    curl "some URL" -F "file=@$logfile"
    rm -f "$logfile"
}

logfile=$( mktemp )

# 1. Make fd 3 a copy of standard error (fd 2)
# 2. Redirect original standard output to the logfile (appending)
# 3. Redirect original standard error to the logfile (will also append)
exec 3>&2 >>"$logfile" 2>&1

# Use shell function for exit trap (for neatness)
trap exit_handler EXIT

set -ex
wtfwtf
Run Code Online (Sandbox Code Playgroud)

你的第二个例子有效,但只是因为你没有cat在日志文件上使用,而不是因为复制它。


小吹毛求疵:命令行上的 URL 应该总是至少用双引号引起来,因为它们往往包含 shell 可能解释为特殊字符(例如?)。