文件描述符是如何工作的?

92

有人能告诉我为什么这不起作用吗?我正在尝试使用文件描述符,但感觉有点迷茫。

#!/bin/bash
echo "This"
echo "is" >&2
echo "a" >&3
echo "test." >&4

前三行代码可以正常运行,但后面两行出现了错误。为什么呢?

4个回答

140

文件描述符0,1和2分别对应标准输入(stdin),标准输出(stdout)和标准错误(stderr)。

文件描述符3、4、...、9用于额外的文件。为了使用它们,您需要先打开它们。例如:

exec 3<> /tmp/foo  #open fd 3.
echo "test" >&3
exec 3>&- #close fd 3.

想了解更多信息,请查看高级Bash脚本指南:第20章I/O重定向


2
这正是我要找的!所以我需要使用exec命令指定一个文件作为临时存储位置,然后在完成后关闭它们?抱歉,我对exec命令有点模糊,我很少使用它。 - Trcx
1
是的,但它不是临时的。即使您的程序完成后,该文件仍将存在。 - dogbane
那样可能很好,我正在尝试将一些脚本移植为与crontab任务调度器兼容,但我遇到了麻烦,因为cron不允许在脚本中进行stdout的管道传输。 - Trcx
2
你不需要指定一个文件。你也可以使文件描述符3指向与文件描述符1相同的文件,exec 3>&1会导致echo hi >&3将“hi”打印到标准输出。 - Felipe Alvarez
1
@clapas,3<> 用于读取和写入。由于我们只向此示例进行写入,因此在此片段中是正确的使用 3>;但是,通常情况下,我们希望从描述符读取我们写入的内容。 - Yzmir Ramirez
显示剩余2条评论

121

这是一个老问题,但有一件事需要澄清

虽然Carl Norum和dogbane的答案是正确的,但前提是更改脚本以使其工作

我想指出的是您无需更改脚本

#!/bin/bash
echo "This"
echo "is" >&2
echo "a" >&3
echo "test." >&4

如果您以不同的方式调用它,它就可以工作:

./fdtest 3>&1 4>&1

意思是将文件描述符3和4重定向到1(标准输出)。

关键在于,如果父进程提供了除1和2(stdout和stderr)之外的描述符,则该脚本想要写入其他描述符是完全可以的。

你的示例实际上非常有趣,因为此脚本可以写入4个不同的文件:

./fdtest >file1.txt 2>file2.txt 3>file3.txt 4>file4.txt

现在你已经有了4个单独文件的输出:

$ for f in file*; do echo $f:; cat $f; done
file1.txt:
This
file2.txt:
is
file3.txt:
a
file4.txt:
test.

更有趣的是,您的程序无需对这些文件具有写权限,因为它实际上并没有打开它们。

例如,当我运行sudo -s以将用户更改为root时,在root下创建目录,并尝试以我的常规用户(在我的情况下为rsp)运行以下命令:

# su rsp -c '../fdtest >file1.txt 2>file2.txt 3>file3.txt 4>file4.txt'

我遇到了一个错误:

bash: file1.txt: Permission denied

但是,如果我在su之外进行重定向:

# su rsp -c '../fdtest' >file1.txt 2>file2.txt 3>file3.txt 4>file4.txt

注意单引号的不同。它有效,我得到:

# ls -alp
total 56
drwxr-xr-x 2 root root 4096 Jun 23 15:05 ./
drwxrwxr-x 3 rsp  rsp  4096 Jun 23 15:01 ../
-rw-r--r-- 1 root root    5 Jun 23 15:05 file1.txt
-rw-r--r-- 1 root root   39 Jun 23 15:05 file2.txt
-rw-r--r-- 1 root root    2 Jun 23 15:05 file3.txt
-rw-r--r-- 1 root root    6 Jun 23 15:05 file4.txt

尽管脚本没有权限创建这些文件,但在由root拥有的目录中,这些文件仍然归root所有 - 这四个文件都是。

另一个例子是使用chroot jail或容器,在其中运行程序,即使以root身份运行,也无法访问那些文件,并将这些描述符重定向到需要的外部位置,而无需实际授予该脚本对整个文件系统或任何其他内容的访问权限。

关键是你发现了一个非常有趣且有用的机制。你不必像其他答案建议的那样在脚本内部打开所有文件。有时候在脚本调用期间重新定向它们是很有用的。

总之,这就是:

echo "This"

实际上等同于:

echo "This" >&1

并运行程序为:

./program >file.txt

就等同于:

./program 1>file.txt

数字1只是一个默认数字,代表标准输出stdout。

但是即使是下面的这个程序:

#!/bin/bash
echo "This"

可能会产生“坏描述符”错误。如何出现这种情况呢?当以以下方式运行时:

./fdtest2 >&-

输出将为:

./fdtest2: line 2: echo: write error: Bad file descriptor

添加>&-(与1>&-相同)表示关闭标准输出。添加2>&-将意味着关闭stderr。

你甚至可以做更复杂的事情。你原来的脚本:

#!/bin/bash
echo "This"
echo "is" >&2
echo "a" >&3
echo "test." >&4

仅运行以下内容时:

./fdtest

打印:

This
is
./fdtest: line 4: 3: Bad file descriptor
./fdtest: line 5: 4: Bad file descriptor

但您可以通过运行以下命令使描述符3和4正常工作,但让第1个失败:

./fdtest 3>&1 4>&1 1>&-

输出结果为:

./fdtest: line 2: echo: write error: Bad file descriptor
is
a
test.

如果您想使描述符1和2都失败,则应像这样运行:

./fdtest 3>&1 4>&1 1>&- 2>&-

你将获得:

a
test.

为什么?什么都没有失败吗? 有失败,但没有stderr(文件描述符号2)你没有看到错误信息!

我认为以这种方式进行实验非常有用,可以让人对描述符及其重定向的工作原理有所了解。

你的脚本确实是一个非常有趣的例子 - 我认为它根本没有问题,只是你使用方法不正确! :)


1
非常有趣的回复..谢谢 - Gunith D
实际上,文件描述符1是标准输出(stdout);标准输入(stdin)是文件描述符0。 - programmerjake
@programmerjake 抱歉,打错字了。感谢你指出来。 - rsp
为什么 echo "is" 会将 stdout 转到 stderr >&2? - solfish
这是一个很棒的回复。谢谢! 有没有一种方法可以检查文件是否存在?例如,如果您在不使用“3>&1 4>&1”的情况下运行./fdtest,那么fdtest是否有办法确定文件描述符3和4不存在? - MattCochrane
1
尽管这个回答在很多方面都很出色,但它在一个重要的方面上有误: "将3>file.txt放在单引号外面的魔法" 的原因是 su 正在以另一个用户身份运行程序,而 root 将其重定向到一个目录中的文件,该目录由你的 ls -la 显示也是由 root 拥有的。这里没有违反权限的技巧,只是 "将你的重定向放在一个字符串内,以便由用户切换子程序(在你的情况下为 su)执行时将作为该子程序执行的用户进行检查"。从语义上讲,重定向应用于它们声明之前的命令。 - Ajax

20

这是因为这些文件描述符没有指向任何东西!通常的默认文件描述符是标准输入0,标准输出1和标准错误流2。由于您的脚本没有打开其他文件,因此没有其他有效的文件描述符。您可以使用 exec 在bash中打开一个文件。这是您示例的修改版:

#!/bin/bash
exec 3> out1     # open file 'out1' for writing, assign to fd 3
exec 4> out2     # open file 'out2' for writing, assign to fd 4

echo "This"      # output to fd 1 (stdout)
echo "is" >&2    # output to fd 2 (stderr)
echo "a" >&3     # output to fd 3
echo "test." >&4 # output to fd 4

现在我们将运行它:

$ ls
script
$ ./script 
This
is
$ ls
out1    out2    script
$ cat out*
a
test.
$

正如您所见,额外的输出被发送到请求的文件中。


有没有办法让它将输出写入终端?我想能够在终端中看到所有内容,但也想将输出发送到我想要的地方。例如:./script 2>out.2 3>out.3 4>out.4 - Trcx
@Trcx,如果你想要写入终端,请使用stdoutstderr。你为什么需要或想要使用其他文件呢? - Carl Norum
我正在尝试使脚本与crontab兼容,需要将多个文件写出,但是由于缺乏stdout支持,crontab不允许从脚本中写入文件。然而,我可以使用crontab将脚本的输出写入文件。我想过让脚本写入各种输出,然后让crontab将所有内容分离到适当的文件中。我只是在寻找一种聪明的方法来写入文件,而不使用stdout。但是感谢你们,我发现我又想多了(再次:P)。感谢您的帮助! - Trcx

4
为了补充 rsp 的回答 在评论中回答问题,以下是来自@MattClimbs的答案。
您可以尝试提前重定向文件描述符并测试其是否打开。如果失败,则将所需的编号文件描述符打开到类似于/dev/null的位置。我经常在脚本中这样做,并利用附加的文件描述符传递返回更多详细信息或响应,而不仅仅是return #

script.sh

#!/bin/bash
2>/dev/null >&3 || exec 3>/dev/null
2>/dev/null >&4 || exec 4>/dev/null

echo "This"
echo "is" >&2
echo "a" >&3
echo "test." >&4

标准错误被重定向到/dev/null,以丢弃可能的bash: #: Bad file descriptor响应,并使用||来处理以下命令exec #>/dev/null,当前一个命令以非零状态退出时执行。如果文件描述符已经打开,则两个测试将返回零状态,exec ...命令将不会被执行。

没有任何重定向调用脚本产生:

# ./script.sh
This
is

在这种情况下,atest的重定向被发送到/dev/null
使用定义了重定向的脚本调用会产生以下结果:
# ./script.sh 3>temp.txt 4>>temp.txt
This
is
# cat temp.txt
a
test.

第一个重定向3>temp.txt会覆盖文件temp.txt,而4>>temp.txt则是将内容追加到文件中。

最后,如果你想要将默认文件重定向到脚本内部而不是/dev/null,或者改变脚本的执行方式并将那些额外的文件描述符重定向到任何你想要的地方,都是可以实现的。


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