Bash 重定向使用 Exec
如果您经常使用命令行,您一定了解 I/O 重定向,它可以将输入重定向到程序和/或从程序重定向输出。您不经常看到或者可能不熟悉的是在 bash 脚本内部重定向 I/O。我不是在谈论当您的脚本执行另一个命令时使用的重定向,我是在谈论一旦脚本开始执行就重定向脚本的 I/O。
举个例子,假设您想添加一个--log选项到您的脚本中,如果用户指定了该选项,您希望所有输出都转到日志文件而不是 stdout。当然,用户可以直接在命令行上重定向输出,但假设有某种原因让您不喜欢该选项。因此,为了在您的脚本中提供此功能,您可以这样做
#!/bin/bash
echo hello
# Parse command line options.
# Execute the following if --log is seen.
if test -t 1; then
# Stdout is a terminal.
exec >log
else
# Stdout is not a terminal, no logging.
false
fi
echo goodbye
echo error >&2
if 语句使用test来查看文件描述符编号 1 是否连接到终端(1 是 stdout)。如果是,则 exec 命令会重新打开它以写入名为log的文件。不带命令但带有重定向的 exec 命令在当前 shell 的上下文中执行,它是您打开和关闭文件以及复制文件描述符的方式。如果文件描述符编号 1 未连接到终端,则我们不进行任何更改。
如果您运行此命令,您将看到第一个 echo 和最后一个 echo 输出到终端。第一个发生在重定向之前,第二个专门重定向到 stderr(2 是 stderr)。那么,如何也将 stderr 放入日志文件呢?只需对 exec 语句进行一个简单的更改即可
#!/bin/bash
echo hello
if test -t 1; then
# Stdout is a terminal.
exec >log 2>&1
else
# Stdout is not a terminal, no logging.
false
fi
echo goodbye
echo error >&2
在这里,exec 语句在日志文件上重新打开 stdout,然后在 stdout 打开的同一事物上重新打开 stderr(这就是您复制文件描述符的方式,也称为 dup 它们)。请注意,这里的顺序很重要:如果您更改顺序并首先重新打开 stderr(即exec 2>&1 >log),那么它仍然会在终端上,因为您正在 stdout 所在的位置打开它,而此时它仍然是终端。
也许主要是作为练习,让我们尝试做同样的事情,即使输出不是到终端。我们不能像上面那样做,因为在日志文件上重新打开 stdout,当它当前连接到文件重定向或管道时,会破坏用户在调用命令时指定的重定向/管道。
给定以下命令作为示例
bash test.sh | grep good
我们想要做的是操作一下,使其看起来像是执行了以下命令
bash test.sh | tee log | grep good
您的第一个想法可能是您可以将 exec 语句更改为类似这样的内容
exec | tee log & # Won't work
并告诉 exec 在后台管道中重新打开 stdout 到tee,但这行不通(尽管 bash 不会对此抱怨)。这只是将 exec 的输出管道传输到 tee,并且由于 exec 在此实例中不产生任何输出,因此 tee 只是创建一个空文件并退出。
您可能还会认为您可以尝试对文件描述符进行一些 dup 操作,并在后台启动 tee,使其从不同的文件描述符获取输入并写入输出。您可以做到这一点,但问题是无法创建一个新进程,使其标准输入连接到管道,以便我们可以将其插入管道(尽管请参阅本文末尾的附言)。如果我们可以做到这一点,tee 命令的标准输出将很容易,因为默认情况下它会转到主脚本输出的同一位置,因此我们可以关闭主脚本的输出并将其连接到我们的管道(如果我们只是有一种创建它的方法)。
那么我们是否走到了死胡同?啊,不,您想到的是不同的操作系统。解决方案实际上在前一段的最后一句话中描述。我们只需要一种创建管道的方法,对吧?那么让我们使用命名管道。
#!/bin/bash
echo hello
if test -t 1; then
# Stdout is a terminal.
exec >log
else
# Stdout is not a terminal.
npipe=/tmp/$$.tmp
trap "rm -f $npipe" EXIT
mknod $npipe p
tee <$npipe log &
exec 1>&-
exec 1>$npipe
fi
echo goodbye
在这里,如果脚本的 stdout 未连接到终端,我们使用 mknod 创建一个命名管道(一个存在于文件系统中的管道),并设置一个 trap 以在退出时删除它。然后我们在后台启动 tee,从命名管道读取并写入日志文件。请记住,tee 还会将其在其 stdin 上读取的任何内容写入其 stdout。另请记住,tee 的 stdout 也与脚本的 stdout(我们的主脚本,即调用 tee 的脚本)相同,因此 tee 的 stdout 的输出将转到我们 stdout 当前所在的位置(即用户在命令行上指定的重定向或管道)。因此,此时 tee 的输出将转到它需要去的地方:用户指定的重定向/管道。
现在我们只需要让 tee 读取正确的数据。由于 tee 正在从命名管道读取,我们所需要做的就是将我们的 stdout 重定向到命名管道。我们关闭当前的 stdout(使用exec 1>&-),并在命名管道上重新打开它(使用ezec 1>$npipe)。请注意,由于 tee 也写入命令行上指定的重定向/管道,因此我们关闭连接不会破坏任何东西。
现在,如果您运行该命令并将其输出管道传输到 grep(如上所示),您将在终端中看到输出,并且它也会保存在日志文件中。
许多这样的旅程是可能的,让手册页成为您的指南!
附注:还有另一种使用 Bash 4 的 coproc 语句来完成此操作的方法,但这将留到以后再说。