# Linux Shell输入输出流管理
目前对于普通用户来说,接触到最多的Linux Shell
输出的方法有两个,一个是显示到显示器上,一个是将输出重定向到文件中。不过,目前的方式,只能二选一,要么将shell
全部输出到显示器上,要么将shell
输出全部保存到文件中,如果想将一部分输出到显示上一部分记录到文件中,那么该如何实现呢?如何用标准的Linux
输入和输出系统来将脚本输出导向特定位置呢?
# 1)标准文件描述符
Linux
系统将每个对象当作文件处理。Linux
用文件描述符来标识每个文件对象。文件描述符是一个非负整数,可以唯一标识会话中打开的文件。每个进程一次最多可以有九个文件描述符。出于特殊目的,bash shell
保留了前三个文件描述符(0, 1, 2)
。这三个特殊文件描述符作为标准的文件描述符会处理脚本的输入和输出。
文件描述符 | 缩写 | 描述 |
---|---|---|
0 | STDIN | 标准输入 |
1 | STDOUT | 标准输出 |
2 | STDERR | 标准错误 |
STDIN
文件描述符代表shell
的标准输入。对终端界面来说,标准输入是键盘。shell
从 STDIN
文件描述符对应的键盘获得输入,在用户输入时处理每个字符。
许多bash
命令能接受 STDIN
的输入,尤其是没有在命令行上指定文件的话。
当在命令行上只输入 cat
命令时,它会从 STDIN
接受输入。输入一行, cat
命令就会显示出一行。
$ cat
this is a line
this is a line
在使用输入重定向符号( < )
时,Linux
会用重定向指定的文件来替换标准输入文件描述符。它会读取文件并提取数据,就如同它是键盘上键入的。
可以通过 STDIN
重定向符号强制 cat
命令接受来自另一个非 STDIN
文件的输入:
cat < file
this is a line
**STDOUT文件描述符代表shell
的标准输出。**在终端界面上,标准输出就是终端显示器。shell
的所有输出(包括shell
中运行的程序和脚本)会被定向到标准输出中,也就是显示器。
通过输出重定向符号,通常会显示到显示器的所有输出会被shell
重定向到指定的重定向文件。
$ ls -alh > test.log
# cat test.log
# 总用量 12K
# drwxrwxr-x 2 rob rob 4.0K 1月 5 23:28 .
# drwxr-xr-x 6 rob rob 4.0K 1月 5 23:28 ..
# -rw-rw-r-- 1 rob rob 3 1月 5 23:28 1.txt
# -rw-rw-r-- 1 rob rob 0 1月 5 23:28 test.log
也可以用>>
符号将数据追加到某个文件。
$ who >> test.log
# cat test.log
# 总用量 12K
# drwxrwxr-x 2 rob rob 4.0K 1月 5 23:28 .
# drwxr-xr-x 6 rob rob 4.0K 1月 5 23:28 ..
# -rw-rw-r-- 1 rob rob 3 1月 5 23:28 1.txt
# -rw-rw-r-- 1 rob rob 0 1月 5 23:28 test.log
# lx :1 2024-01-02 21:11 (:1)
当命令生成错误消息时,shell
并不能将错误消息重定向到输出重定向文件。
$ ls -alh rob > test.log
# ls: 无法访问 'rob': 没有那个文件或目录
test.log
文件创建成功了,只是里面是空的。shell
对于错误消息的处理是跟普通输出分开的。
STDERR文件描述符用来处理错误消息。STDERR
文件描述符代表shell
的标准错误输出。shell
或shell
中运行的程序和脚本出错时生成的错误消息都会发送到这个位置。
尽管分配给它们的文件描述符值不同,默认情况下, STDERR
文件描述符会和 STDOUT
文件描述符指向同样的地方,譬如显示器,但STDERR
并不会随着 STDOUT
的重定向而发生改变。
要重定向错误输出,只需要在使用重定向符号时定义 STDERR
文件描述符就可以了。STDERR
文件描述符被设成 2
,可以选择只重定向错误消息。
$ ls -al rob 2> test.log
cat test.log
# ls: 无法访问 'rob': 没有那个文件或目录
用这种方法,shell
会只重定向错误消息,正常 STDOUT
输出仍然会发送到默认的 STDOUT
文件描述符,也就是显示器。
要同时重定向错误和正常输出,必须用两个重定向符号。
$ ls -al rob test.log 2> test.log 1>out.log
cat test.log
# ls: 无法访问 'rob': 没有那个文件或目录
cat out.log
# -rw-rw-r-- 1 rob rob 52 1月 5 23:44 test.log
也可以将 STDERR
和 STDOUT
的输出重定向到同一个输出文件。为此bash shell
提供了特殊的重定向符号&>
。
$ ls -al rob test.log &> test.log
# ls: 无法访问 'rob': 没有那个文件或目录
# -rw-rw-r-- 1 rob rob 52 1月 5 23:47 test.log
当使用 &>
符时,命令生成的所有输出都会发送到同一位置,包括数据和错误。为了避免错误信息散落在输出文件中,相较于标准输出,bash shell
自动赋予了错误消息更高的优先级。
# 2)脚本中重定向输出
在脚本中通过重定向STDOUT
和 STDERR
文件描述符就可以在多个位置生成输出了。
# 临时重定向
可以将单独的一行输出重定向到 STDERR
#!/bin/bash
echo error message >&2
echo normal message
执行,
$ ./redirect.sh 2>erro.log
# normal message
$ cat erro.log
# error message
在上面的例子中,会发现error message
这句被记录到erro.log
文件中了,这正是因为在echo
语句后面加了>&2
将输出内容重定向了STDERR
了。在运行脚本时重定向了STDERR
,脚本中所有导向 STDERR
的文本都会被重定向。
# 永久重定向
像上面那样,一句一句的重定向,如果需要输出的内容多的话,就会变的很麻烦了。这时,可以用 exec
命令在shell
脚本运行时重定向某个特定文件描述符。
exec 1>testout
这个语句在shell
脚本中可以将STDOUT
重定向到文件testout
中,这时就不会显示到显示器上了。
#!/bin/bash
exec 1>testout
echo "This is a test of redirecting all output"
echo "from a script to another file."
执行,
$ ./redirect.sh
$ cat testout
# This is a test of redirecting all output
# from a script to another file.
可以在脚本执行过程中重定向 STDOUT
。
#!/bin/bash
exec 2>testerror
echo start of script
echo STDOUT to log file
exec 1>testout
echo log to file
echo log to error file >&2
执行,
$ ./redirect.sh
# start of script
# STDOUT to log file
$ cat testout
# log to file
$ cat testerror
# log to error file
这个脚本示例中值得注意的是,exec 2>testerror
语句将STDERR
重定向到了文件testerror
中,这时后面再将输出重定向到STDERR
中时>&2
,错误信息会被记录到testerror
文件中。
不过上面的操作也有个问题,那就是一旦将STDOUT
重定向到文件testout
中就没办法再恢复输出到屏幕上了。接下来会介绍怎样才能实现重定向的恢复。
# 自定义输出重定向
前面介绍的是3个标准的文件描述符的重定向,除此之外每个进程还支持自定义6个打开的文件描述符。
可以用 exec
命令来给输出分配文件描述符。
#!/bin/bash
exec 3>test3out
echo log to screen
echo log to test3out >&3
echo log back to screen
执行,
$ ./redirect.sh
# log to screen
# log back to screen
$ cat test3out
# log to test3out
以上就是自定义了输出文件描述符3
到文件test3out
中,上面会创建新文件test3out
,也可以不创建新文件,而使用追加的方式,
exec 3>>test13out
可以分配另外一个文件描述符给标准文件描述符,这样就可以恢复已重定向的文件描述符了。
#!/bin/bash
exec 3>&1
exec 1>test13out
echo log to test13out file
exec 1>&3
echo log to screen
执行,
$ ./redirect.sh
# log to screen
$ cat test13out
# log to test13out file
上面的例子,是先将文件描述符3
重定向到STDOUT
,再将STDOUT
重定向到文件test13out
,输出结束后又将文件描述符1
重定向到文件描述符3
也就是STDOUT
上,这样就可以将输出恢复到屏幕上了,有点类似暂存变量的意思。
# 3)重定向输入
默认的STDIN
是用户从键盘输入,exec
命令允许你将 STDIN
重定向到Linux
系统上的文件中。
exec 0< testfile
这个命令会告诉shell
它应该从文件 testfile
中获得输入,而不是 STDIN
。
filename=$1
exec 0< $filename
count=1
while read line
do
echo "Line #$count: $line"
count=$[ $count + 1 ]
done
执行,
$ cat testfile
# error message
# new line
$ ./redirect.sh testfile
# Line #1: error message
# Line #2: new line
read
命令读取用户在键盘上输入的数据。将 STDIN
重定向到文件后,
当 read
命令试图从 STDIN
读入数据时,它会到文件去取数据,而不是键盘。
可以用和重定向输出文件描述符同样的办法重定向输入文件描述符。
#!/bin/bash
exec 6<&0
testfile=$1
exec 0< $testfile
count=1
while read line
do
echo "Line #$count: $line"
count=$[ $count + 1 ]
done
exec 0<&6
read -n1 -p "Are you sure? " answer
echo
case $answer in
Y|y) echo "Good.";;
N|n) echo "Sorry.";;
esac
执行,
$ ./rediect.sh
# Line #1: error message
# Line #2: new line
# Are you sure? Y
# Good.
这里先创建了文件描述符6
作为STDIN
的重定向,又将STDIN
重定向到了testfile
,最后又利用6
恢复了STDIN
,这和前面介绍的STDOUT
的重定向与恢复基本类似。
# 4)支持读写的文件描述符
以打开单个文件描述符来作为输入和输出,即用同一个文件描述符对同一个文件进行读写。用这种方法时,要特别小心,因为是对同一个文件进行数据读写,shell
会维护一个指明在文件中的当前位置的内部指针,任何读或写都会从文件指针上次的位置开始,如果操作不小心会导致文件内容的读写错位。
支持输入输出的文件描述符的创建方式是使用<>
符号,
exec 3<> testfile
支持输入输出文件描述符的一个例子,
#!/bin/bash
exec 3<> testfile
read line <&3
echo "The first line: $line"
echo "line2" >&3
执行,
$ cat testfile
# error message
# new line
$ ./redirect.sh
# The first line: error message
$ cat testfile
# error message
# line2
# ne
这个例子中,read line <&3
定义了从文件描述符3
中读取一行输入,这时文件指针就指向了第二行的开始位置,重定向输入到&3
中时,line2\n
会被记录到testfile
文件第二行开始的位置,并覆盖掉原来的内容。最后就得到了上面展示的结果。
# 5)关闭文件描述符
shell
会在脚本退出时自动关闭创建的新的输入或输出文件描述符。有时需要在脚本结束前手动关闭文件描述符,对应的操作是将它重定向到特殊符号 &-
。
exec 3>&-
尝试使用已关闭的文件描述符时会报错,
# ./redirect.sh: 第 11 行: echo: 写错误: 错误的文件描述符
当然也可以手动关闭标准输入输出文件描述符,关闭后同样无法再使用它们。
在关闭文件描述符后,如果随后在脚本中又打开了同一个输出文件,shell
会用一个新文件来替换已有文件,会覆盖掉原来的内容。
# 6) 列出打开的文件描述符及关闭文件描述符
lsof
命令会列出整个Linux
系统打开的所有文件描述符。因为它会向非系统管理员用户提供Linux
系统的信息,因此这是个功能颇有争议。该命令会产生大量的输出。它会显示当前Linux
系统上打开的每个文件的有关信息。这包括后台运行的所有进程以及登录到系统的任何用户。
lsof
支持大量的参数来筛选,最常用的如-p
指定进程ID
,-d
指定要显示的文件描述符编号,-u
指定用户名或用户ID
,-a
选项用来对其他选项的结果执行布尔 AND
运算。
要想知道进程的当前PID
,可以用特殊环境变量 $$
$ echo $$
# 75485
执行,
$ lsof -a -d 0,1,2 -u rob
# COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME
# sh 75484 rob 0u CHR 136,3 0t0 6 /dev/pts/3
# sh 75484 rob 1u CHR 136,3 0t0 6 /dev/pts/3
# sh 75484 rob 2u CHR 136,3 0t0 6 /dev/pts/3
# bash 75485 rob 0u CHR 136,3 0t0 6 /dev/pts/3
# bash 75485 rob 1u CHR 136,3 0t0 6 /dev/pts/3
# bash 75485 rob 2u CHR 136,3 0t0 6 /dev/pts/3
# lsof 97492 rob 0u CHR 136,3 0t0 6 /dev/pts/3
执行,
$ lsof -a -d 0,1,2 -u rob -p $$
# COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME
# bash 75485 rob 0u CHR 136,3 0t0 6 /dev/pts/3
# bash 75485 rob 1u CHR 136,3 0t0 6 /dev/pts/3
# bash 75485 rob 2u CHR 136,3 0t0 6 /dev/pt
lsof
的默认输出:
列 | 描述 |
---|---|
COMMAND | 正在运行的命令名的前9个字符 |
PID | 进程的PID |
USER | 进程属主的登录名 |
FD | 文件描述符号以及访问类型( r 代表读, w 代表写, u 代表读写) |
TYPE | 文件的类型( CHR 代表字符型, BLK 代表块型, DIR 代表目录, REG 代表常规文件) |
DEVICE | 设备的设备号(主设备号和从设备号) |
SIZE | 如果有的话,表示文件的大小 |
NODE | 本地文件的节点号 |
NAME | 文件名 |
有时候想阻止命令输出,可以将 STDERR/STDOUT
重定向到一个叫作null文件的特殊文件,null
文件跟它的名字很像,文件里什么都没有。shell
输出到null
文件的任何数据都不会保存,全部都被丢掉了。Linux
系统上null
文件的标准位置是/dev/null
。重定向到该文件的数据都会被丢掉,不会显示。
ls -al > /dev/null
**使用这个重定向的情况一般有两种,一种是将脚本放到后台后不希望其再输出,另外一个用来快速清空文件
**
cat /dev/null > testfile
# 7)输出同时发送到显示器和日志文件
将输出同时发送到显示器和日志文件,不用将输出重定向两次,只要用特殊的 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
不过,tee
命令只能记录STDOUT
不能记录STDERR
,如果要显示的同时记录STDERR
,需要自己手动实现,linux
没有提供可以直接使用的命令。