1.5 玩转文件描述符和重定向
文件描述符是与文件输入、输出相关联的整数。它们用来跟踪已打开的文件。最常见的文件描述符是stdin
、stdout
和stderr
。我们可以将某个文件描述符的内容重定向到另一个文件描述符中。下面给出一些对文件描述符进行操作和重定向的例子。
1.5.1 预备知识
我们在编写脚本的时候会频繁使用标准输入(stdin
)、标准输出(stdout
)和标准错误(stderr
)。通过内容过滤将输出重定向到文件是我们从事的基础任务之一。当命令输出文本的时候,这些输出文本有可能是错误信息,也可能是正常的(非错误的)输出信息。单靠查看输出的文本本身,我们没法区分哪些是正常的输出文本,哪些是错误文本。不过,我们可以通过文件描述符来解决这个问题,将那些与特定描述符关联的文本提取出来。
文件描述符是与一个打开的文件或数据流相关联的整数。文件描述符0
、1
以及2
是系统预留的。
0
——stdin
(标准输入)。1
——stdout
(标准输出)。2
——stderr
(标准错误)。
1.5.2 实战演练
用下面的方法可以将输出文本重定向或保存到一个文件中:
- $ echo "This is a sample text 1" > temp.txt
这种方法通过截取文件的方式,将输出文本存储到文件temp.txt中,也就是说在把echo
命令的输出写入文件之前,temp.txt中的内容首先会被清空。
接下来,再看另一个例子:
- $ echo "This is sample text 2" >> temp.txt
这种方法会将文本追加到目标文件中。
>
和>>
并不相同。尽管这两个操作符都可以将文本重定向到文件,但是前者会先清空文件,再写入内容;而后者会将内容追加到现有文件的尾部。
可以用下面的方法查看文件内容:
- $ cat temp.txt
- This is sample text 1
- This is sample text 2
当使用重定向操作符时,重定向的内容不会出现在终端,而是直接被导入文件。重定向操作符默认使用标准输出。如果想使用特定的文件描述符,你必须将描述符置于操作符之前。
>
等同于1>
;对于>>
来说,情况也类似(即>>
等同于1>>
)。
来看看什么是标准错误以及如何对它重定向。当命令输出错误信息时,stderr
信息就会被打印出来。考虑下面的例子:
- $ ls +
- ls: cannot access +: No such file or directory
这里,+
是一个非法参数,因此将返回错误信息。
成功和不成功的命令
当一个命令发生错误并退回时,它会返回一个非0的退出状态;而当命令成功完成后,它会返回数字0。退出状态可以从特殊变量
$?
中获得(在命令执行语句之后立刻运行echo $?
,就可以打印出退出状态)。
下面的命令会将stderr
文本打印到屏幕上,而不是文件中。
- $ ls + > out.txt
- ls: cannot access +: No such file or directory
然而在下面的命令中,stdout
没有任何输出,因为错误已经重定向到out.txt中去了。
- $ ls + 2> out.txt # 正常运行
你可以将stderr
单独重定向到一个文件,将stdout
重定向到另一个文件:
- $ cmd 2>stderr.txt 1>stdout.txt
还可以利用下面的方法将stderr
转换成stdout
,使得stderr
和stdout
都被重定向到同一个文件中:
- $ cmd> output.txt 2>&1
或者采用下列方法:
- $ cmd &> output.txt
有时候,在输出中可能包含一些不必要的信息(比如除错信息)。如果你不想让终端中充斥着有关stderr
的繁枝末节,那么你可以将stderr
的输出重定向到 /dev/null,保证一切都会被清除得干干净净。假设我们有三个文件,分别是a1、a2、a3。但是普通用户对文件a1没有“读?写?执行”权限。如果你需要打印文件名以a起始的所有文件的内容,你可以使用cat
命令。
设置一些测试文件:
- $ echo a1 > a1
- $ cp a1 a2 ; cp a2 a3;
- $ chmod 000 a1 #清除所有权限
尽管可以使用通配符(a*
)显示所有的文件内容,但是系统会显示一个出错信息,因为对文件a1没有可读权限。
- $ cat a*
- cat: a1: Permission denied
- a1
- a1
其中,cat: a1: Permission denied
属于stderr
。我们可以将stderr
信息重定向到一个文件中,而stdout
仍然保持不变。考虑如下代码:
- $ cat a* 2> err.txt #stderr被重定向到err.txt
- a1
- a1
- $ cat err.txt
- cat: a1: Permission denied
观察下面的代码:
- $ some_command 2> /dev/null
在这个示例中,来自stderr
的输出被丢到文件/dev/null中。/dev/null是一个特殊的设备文件,这个文件接收到的任何数据都会被丢弃。因此,null
设备通常也被称为位桶(bit bucket)或黑洞。
当对stderr
或stdout
进行重定向时,重定向的文本将传入文件。因为文本已经被重定向到文件中,也就没剩下什么东西可以通过管道(|
)传给接下来的命令,而这些命令是通过stdin
来接收文本的。
但是有一个巧妙的方法可以一方面将数据重定向到文件,另一方面还可以提供一份重定向数据的副本作为后续命令的stdin
。这一切都可以使用tee
来实现。举个例子:要在终端中打印stdout
,同时将它重定向到一个文件中,那么可以这样使用tee
:
command | tee FILE1 FILE2
在下面的代码中,tee
命令接收到来自stdin
的数据。它将stdout
的一份副本写入文件out.txt,同时将另一份副本作为后续命令的stdin
。命令cat -n
将从stdin
中接收到的每一行数据前加上行号并写入stdout
:
- $ cat a* | tee out.txt | cat -n
- cat: a1: Permission denied
- 1 a1
- 2 a1
查看out.txt的内容:
- $ cat out.txt
- a1
- a1
注意,cat: a1: Permission denied
并没有在文件内容中出现。这是因为这些信息属于stderr
,而tee
只能从stdin
中进行读取。
默认情况下,tee
命令会将文件覆盖,但它提供了一个-a
选项,可以用于追加内容。例如:
- $ cat a* | tee -a out.txt | cat -n.
带有参数的命令可以写成:command FILE1 FILE2
,依次类推,或者简简单单地用command FILE
。
我们可以使用stdin
作为命令参数。只需要将-作为命令的文件名参数即可:
- $ cmd1 | cmd2 | cmd -
例如:
- $ echo who is this | tee -
- who is this
- who is this
或者我们也可以将 /dev/stdin作为输出文件名来使用stdin
。
类似地,使用 /dev/stderr代表标准错误,/dev/stdout代表标准输出。这些特殊的设备文件分别对应stdin
、stderr
和stdout
。
1.5.3 补充内容
从stdin
读取输入的命令能以多种方式接收数据。另外,还可以用cat
和管道来制定我们自己的文件描述符,例如:
- $ cat file | cmd
- $ cmd1 | cmd2
- 将文件重定向到命令
借助重定向,我们可以像使用stdin
那样从文件中读取数据:
- $ cmd < file
- 重定向脚本内部的文本块
有时候,我们需要对文本块(多行文本)像标准输入一样进行重定向。考虑一个特殊情况:源文本就位于shell脚本中。一个实用的例子是向log
文件中写入头部数据,可以按照下面的方法完成:
- #!/bin/bash
- cat <<EOF>log.txt
- LOG FILE HEADER
- This is a test log file
- Function: System statistics
- EOF
在cat <
与下一个EOF
行之间的所有文本行都会被当做stdin
数据。log.txt文件的内容打印如下:
- $ cat log.txt
- LOG FILE HEADER
- This is a test log file
- Function: System statistics
- 自定义文件描述符
文件描述符是用于访问文件的一个抽象指针。存取文件离不开被称为“文件描述符”的特殊数字。0
、1
和2
分别是stdin
、stdout
和stderr
的预留描述符。
我们可以使用exec
命令创建自定义的文件描述符。如果你对用其他编程语言进行文件编程非常熟悉,你可能已经注意到了文件打开模式。通常来说,会使用3种模式。
只读模式。
截断模式。
追加模式。
<
操作符用于从文件中读取至stdin
。>
操作符用于截断模式的文件写入(数据在目标文件内容被截断之后写入)。>>
操作符用于追加模式的文件写入(数据被添加到文件的现有内容中,而且该目标文件中原有的内容不会丢失)。文件描述符可以用以上三种模式中的任意一种来创建。
为读取文件创建一个文件描述符:
- $ exec 3<input.txt # 使用文件描述符3打开并读取文件
我们可以这样使用它:
- $ echo this is a test line > input.txt
- $ exec 3<input.txt
现在你就可以在命令中使用文件描述符3
了。例如:
- $ cat <&3
- this is a test line
如果要再次读取,我们就不能再继续使用文件描述符3
了,而是需要用exec
重新分配文件描述符3
以便用于读取。
创建一个文件描述符用于写入(截断模式):
- $ exec 4>output.txt # 打开文件用于写入
例如:
- $ exec 4>output.txt
- $ echo newline >&4
- $ cat output.txt
- newline
创建一个文件描述符用于写入(追加模式):
- $ exec 5>>input.txt
例如:
- $ exec 5>>input.txt
- $ echo appended line >&5
- $ cat input.txt
- newline
- appended line