在Bash中,我如何打开一个可写的文件描述符并进行外部重定向?

13

我想使用bash为编写额外的诊断消息打开一个新描述符。我不希望使用stderr,因为stderr应该只包含由bash调用的程序的输出。我也希望用户能够重定向我的自定义描述符。

我尝试了以下方法:

exec 3>/dev/tty
echo foo1
echo foo2 >&2
echo foo3 >&3

但是当我尝试重定向文件描述符3时,输出仍然写入终端。

$ ./test.sh >/dev/null 2>/dev/null 3>/dev/null
foo3

一个有用的网站 链接 - Mike
你可以尝试使用命名管道 - Dennis Williamson
5个回答

11

首先父级 shell 将文件描述符 3 设置为 /dev/null
然后你的程序将文件描述符 3 设置为 /dev/tty
因此你遇到的症状并不令人惊讶。

编辑:你可以检查文件描述符 3 是否已经设置:

if [[ ! -e /proc/$$/fd/3 ]]
then
    exec 3>/dev/tty
fi

你删除的解决方案几乎可以工作。只是 -t 只能检测 fd 是否连接到终端,而不能检测其是否存在。+1 说明。 - Kelvin
1
我刚想到了/proc测试,然后你就发了它。或许伟大的思想会有相同的想法。不幸的是,这并不具备可移植性。在Linux中运行得很好,但在Mac上不行。如果可以的话,我会再点一个赞的。 - Kelvin
噢,好吧,值得一试。我没有使用 Mac 的权限。感谢您的友善评论。 - cdarke
1
@x-yuri 我认为你找到了另一个解决方案。这个命令在Linux和Mac上可以运行:bash -c 'if [[ ! -e /dev/fd/3 ]]; then echo "fd3 non-existent"; fi'。然而,在Solaris上它不起作用,不过我也不太关心。 - Kelvin

8

简单来说:如果父Shell没有重定向fd 3,那么test.sh将会把fd 3重定向到/dev/tty

if ! { exec 0>&3; } 1>/dev/null 2>&1; then
   exec 3>/dev/tty
fi
echo foo1
echo foo2 >&2
echo foo3 >&3

尽管我很想给gregc接受答案的机会,但他已经将近4周没有回来清理他的答案了。这个答案虽然可能是从gregc那里“借鉴”来的,但它是最简单的方法。 - Kelvin

2

以下是一种仅使用(Bash)shell的方法来检查文件描述符是否已设置。

(
# cf. "How to check if file descriptor exists?", 
# http://www.linuxmisc.com/12-unix-shell/b451b17da3906edb.htm

exec 3<<<hello

# open file descriptors get inherited by child processes, 
# so we can use a subshell to test for existence of fd 3
(exec 0>&3) 1>/dev/null 2>&1 && 
    { echo bash: fd exists; fdexists=true; } || 
    { echo bash: fd does NOT exists; fdexists=false; }

perl -e 'open(TMPOUT, ">&3") or die' 1>/dev/null 2>&1 && 
    echo perl: fd exists || echo perl: fd does NOT exist

${fdexists} && cat <&3
)

+1 这看起来是正确的。我会再测试一下。但你应该将perl部分分离出来,因为它看起来像是脚本的一部分。仔细阅读后,我发现你只是用它进行比较。此外,我不确定exec 3<<<hello的目的是什么;将其放在fdexists测试之前会使测试每次都为真。 - Kelvin
我确认这个想法是正确的,但需要进行清理。如果您能够采用我的问题中的脚本并使用您的解决方案进行修改,那么我将接受您的答案。 - Kelvin
perl 部分是另一种解决方案。exec 3<<<hello 这里是为了测试。取消注释这行代码的效果与从外部重定向文件描述符相同。 - x-yuri

1

更新

这是可以做到的。请参考kaluy的答案,这是最简单的方法。

原始答案

似乎答案是“你不能”。在脚本中创建的任何描述符都不适用于调用脚本的shell。

我想出了如何使用ruby来实现它,如果有人感兴趣,请参见使用perl的更新。

begin
  out = IO.new(3, 'w')
rescue Errno::EBADF, ArgumentError
  out = File.open('/dev/tty', 'w')
end
p out.fileno
out.puts "hello world"

请注意,这在守护程序中显然不起作用 - 它未连接到终端。
更新
如果 Ruby 不是您的菜,您可以从 Ruby 脚本中简单地调用一个 bash 脚本。您需要 open4 gem/library 来可靠地传输输出:
require 'open4'

# ... insert begin/rescue/end block from above

Open4.spawn('./out.sh', :out => out)

更新2

这里有一种方法,使用了一点perl和大部分bash。您必须确保perl在您的系统上正常工作,因为缺少perl可执行文件也会返回非零退出代码。

perl -e 'open(TMPOUT, ">&3") or die' 2>/dev/null
if [[ $? != 0 ]]; then
  echo "fd 3 wasn't open"
  exec 3>/dev/tty
else
  echo "fd 3 was open"
fi
echo foo1
echo foo2 >&2
echo foo3 >&3

1

@Kelvin:这是你要求修改的脚本(加上一些测试)。

echo '
#!/bin/bash

# If test.sh is redirecting fd 3 to somewhere, fd 3 gets redirected to /dev/null;
# otherwise fd 3 gets redirected to /dev/tty.
#{ exec 0>&3; } 1>/dev/null 2>&1 && exec 3>&- || exec 3>/dev/tty
{ exec 0>&3; } 1>/dev/null 2>&1 && exec 3>/dev/null || exec 3>/dev/tty

echo foo1
echo foo2 >&2
echo foo3 >&3

' > test.sh

chmod +x test.sh

./test.sh
./test.sh 1>/dev/null
./test.sh 2>/dev/null
./test.sh 3>/dev/null
./test.sh 1>/dev/null 2>/dev/null
./test.sh 1>/dev/null 2>/dev/null 3>&-
./test.sh 1>/dev/null 2>/dev/null 3>/dev/null
./test.sh 1>/dev/null 2>/dev/null 3>/dev/tty

# fd 3 is opened for reading the Here String 'hello'
# test.sh should see that fd 3 has already been set by the environment
# man bash | less -Ip 'here string'
exec 3<<<hello
cat <&3
# If fd 3 is not explicitly closed, test.sh will determine fd 3 to be set.
#exec 3>&- 
./test.sh

exec 3<<<hello
./test.sh 3>&-

最好的做法是通过编辑您的原始答案将其放置在那里。 - Sid Holland
另外,您应该使用您原来的 SO 账户,而不是创建一个新账户。 - Kelvin
抱歉,这个不起作用。如果我运行脚本,将fd 3重定向到一个文件,什么也没有被写入。 - Kelvin

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