一、 标准文件描述符

Linux将每个对象当作文件处理,包括输入和输出进程。Linux用文件描述符(file descriptor)来标识每个文件对象,文件描述符是一个非负整数,可以唯一标识会话中打开的文件。每个进程一次最多可以有9个文件描述符,bash shell保留了前3个(0,1,2),这三个被称为标准文件描述符。

1. STDIN

STDIN文件描述符代表shell的标准输入,对终端界面来说,标准输入是键盘。许多bash命令能接受STDIN的输入,当在命令行上只输入cat命令时,它会从键盘接受输入。输入一行, cat就显示出一行。

在使用输入重定向符号 < 时,Linux会用重定向指定的文件来替换标准输入文件描述符,因此它会读取文件并提取数据。可以通过<强制cat命令接受文件输入,跟不加<符号效果是一样的。你可以使用这种技术将数据输入到任何能从STDIN接受数据的shell命令中。

2. STDOUT

STDOUT文件描述符代表shell的标准输出,在终端界面上,标准输出就是终端显示器。 shell 的所有输出(包括shell中运行的程序和脚本)默认会被定向到标准输出,也就是显示器中。可以使用 > 符号改变输出位置,通常会指向一个文件。

  • ls -l > test2    #覆盖原数据
  • ls -l >> test2   #追加内容

但是,如果执行的命令有报错,STDOUT并不能重定向报错内容,例如

$ ls -al badfile > test3
ls: cannot access badfile: No such file or directory
$ cat test3
$

当命令生成错误消息时, shell并未将错误消息重定向到指定文件,而是显示在了显示器屏幕上。注意,在显示test3文件的内容时并没有任何错误。 test3文件创建成功了,只是里面是空的。shell对于错误消息的处理是跟普通输出分开的,想要重定向错误信息,你需要换种方法来处理。

3. STDERR

STDERR文件描述符代表shell的标准错误输出,命令和shell脚本出错时生成的错误消息都会发送到这个位置。默认情况下, STDERR和STDOUT都指向显示器,尽管分配给它们的文件描述符值不同。你已经知道如何用重定向符号来重定向STDOUT数据。重定向STDERR数据也没太大差别,只要在使用重定向符号时定义STDERR文件描述符就可以了。有几种办法实现方法:

  • 只重定向错误

STDERR文件描述符为2,只重定向错误消息,将该文件描述符值放在重定向符号前即可(不要有空格)。

ls -al badfile 2> test4

如果有正常输出也有错误输出,这种方法只能重定向错误信息至文件,正常信息依然会输出到显示器。

ls -al test badtest test4 2> test5

  • 重定向错误和数据

如果想重定向错误和正常输出到不同文件,需要用两个重定向符号。

ls -al badtest test4 2> test5 1> test6

也可以将STDERR和STDOUT的输出重定向到同一个输出文件,bash shell 提供了特殊的重定向符号&>。

ls -al badtest test4 &> test7

为了避免错误信息散落在输出文件中,相较于标准输出, bash shell自动赋予了错误消息更高的优先级,方便集中浏览错误信息。

二、 自定义文件描述符

前面提到过,在shell中最多可以有9个打开的文件描述符,3~8的文件描述符均可用作输入或输出重定向。你可以将这些文件描述符中的任意一个分配给文件,然后在脚本中使用它们。

1. 创建输出文件描述符

可以用exec命令来给输出分配文件描述符,分配之后这个重定向就会一直有效,直到你重新分配或者关闭。

$ cat test13
#!/bin/bash
# using an alternative file descriptor

exec 3>test13out
echo "This should display on the monitor"
echo "and this should be stored in the file" >&3
echo "Then this should be back on the monitor"


$ ./test13
This should display on the monitor
Then this should be back on the monitor
$ cat test13out
and this should be stored in the file

当脚本执行echo语句时,输出内容会像预想中那样显示在STDOUT上,但重定向到文件描述符3的那行echo语句的输出却进入了另一个文件。

2. 重定向文件描述符

现在介绍怎么恢复已重定向的文件描述符。你可以分配另外一个文件描述符给标准文件描述符,反之亦然。这意味着你可以将STDOUT的原来位置重定向到另一个文件描述符,然后再利用该文件描述符重定向回STDOUT

$ cat test14
#!/bin/bash
# storing STDOUT, then coming back to it

exec 3>&1  #将文件描述符3重定向到文件描述符1,也就是STDOUT。这意味着任何发送给文件描述符3的输出都将出现在显示器上。
exec 1>test14out  #将文件描述符1重定向到文件,但是,文件描述符3仍然指向原来的位置(显示器)。如果此时将输出数据发送给文件描述符3,它仍然会出现在显示器上,尽管文件描述符1已经被重定向了。
echo "This should store in the output file"
echo "along with this line."

exec 1>&3  #将文件描述符1重定向到文件描述符3的当前位置(显示器),这意味着现在文件描述符1又指向了它原来的位置:显示器。
echo "Now things should be back to normal"

$ ./test14
Now things should be back to normal
$ cat test14out
This should store in the output file
along with this line.

3. 关闭文件描述符

如果你创建了新的输入或输出文件描述符, shell会在脚本退出时自动关闭它们。然而在有些情况下,你需要在脚本结束前手动关闭文件描述符。要关闭文件描述符,需要将它重定向到特殊符号&-。

exec 3>&-

一旦关闭了文件描述符,就不能在脚本中向它写入任何数据,否则shell会报错。

$ cat badtest
#!/bin/bash
# testing closing file descriptors
exec 3> test17file
echo "This is a test line of data" >&3
exec 3>&-
echo "This won't work" >&3

$ ./badtest
./badtest: 3: Bad file descriptor

4. 列出打开的文件描述符

lsof命令会列出整个Linux系统打开的所有文件描述符。要想以普通用户账户来运行它,必须通过全路径名来引用:/usr/sbin/lsof。

有大量的命令行选项和参数可以用来帮助过滤lsof的输出。最常用的有-p-d,前者允许指定 PID,后者允许指定要显示的文件描述符编号。

要想知道进程的当前PID,可以用特殊环境变量$$-a选项用来对其他两个选项的结果执行AND运算。

/usr/sbin/lsof -a -p $$ -d 0,1,2

COMMAND PID USER FD TYPE DEVICE SIZE NODE NAME
bash 3344 rich 0u CHR 136,0 2 /dev/pts/0
bash 3344 rich 1u CHR 136,0 2 /dev/pts/0
bash 3344 rich 2u CHR 136,0 2 /dev/pts/0

5. 阻止命令输出

有时候,你可能不想显示脚本的输出。可以将STDERR重定向到一个叫作null文件的特殊文件。Linux系统上null文件的标准位置是/dev/null,重定向到该位置的任何数据都会被丢掉,不会显示。

ls -al > /dev/null
cat /dev/null

这是避免输出错误消息,也无需保存它们的一个常用方法。

ls -al badfile test16 2> /dev/null
-rwxr--r-- 1 rich rich 135 Oct 29 19:57 test16*

也可以将/dev/null作为输入文件,由于/dev/null文件不含有任何内容,通常用来快速清除现有文件中的数据,而不用先删除文件再重新创建。

$ cat testfile
This is the first line.
This is the second line.
This is the third line.
$ cat /dev/null > testfile
$ cat testfile

文件testfile仍然存在系统上,但现在它是空文件。这是清除日志文件的一个常用方法,因为日志文件必须时刻准备等待应用程序操作。

6. 记录消息

想要将输出同时发送到显示器和日志文件,不用将输出重定向两次,只要用特殊的tee命令就行。tee命令相当于管道的一个T型接头。它将从STDIN过来的数据同时发往两处:STDOUT和tee命令指定的文件名。

$ date | tee testfile
Sun Oct 19 18:56:21 EDT 2014
$ cat testfile
Sun Oct 19 18:56:21 EDT 2014

默认情况下, tee命令会在每次使用时覆盖输出文件内容。如果你想将数据追加到文件中,必须用-a选项。

$ date | tee -a testfile
Sun Oct 19 18:58:05 EDT 2014

$ cat testfile
Sun Oct 19 18:56:21 EDT 2014
Sun Oct 19 18:58:05 EDT 2014

三、 实例

文件重定向常见于脚本需要读入文件和输出文件时。这个样例脚本两件事都做了。它读取.csv 格式的数据文件,输出SQL INSERT语句来将数据插入数据库

tran.sh

#!/bin/bash
# read file and create INSERT statements for MySQL
outfile='members.sql'
IFS=','
cat $1 | while read lname fname address city state zip
do
cat >> $outfile << EOF
INSERT INTO members (lname,fname,address,city,state,zip) VALUES
('$lname', '$fname', '$address', '$city', '$state', '$zip');
EOF
done

members.csv

Blum,Richard,123 Main St.,Chicago,IL,60601
Blum,Barbara,123 Main St.,Chicago,IL,60601
Bresnahan,Christine,456 Oak Ave.,Columbus,OH,43201
Bresnahan,Timothy,456 Oak Ave.,Columbus,OH,43201

运行脚本时,显示器上不会出现任何输出, 但是在members.sql输出文件中,你会看到如下输出内容。

chmod +x tran.sh
./tran.sh < members.csv
cat members.sql

更多推荐

shell脚本编程笔记(六)—— 输出处理