# 简介

之前关于标准输入,输出重定向的文章,我提到开始接触流,并且在文中讲解时使用也是以流来具体化重定向的作用,但是讲解的非常不严谨。本文相当于对重定向深入讲解,甚至可以自定义重定向。

# 标准文件描述符

Linux 系统将每个对象当作文件处理(有句话就是一切皆文件),包括输入和输出,用文件描述符标识每个文件对象。文件描述符是一个非负整数,可以唯一表示会话中打开的文件。出于特殊目的, bash shell 在每个进程,只保留了 3 个文件标识符 (0,1,2)

  • STDIN -- 标准输。对于终端界面,标准输入是键盘shellSTDIN 对应的键盘获得输入。使用输入重定向符号 <Linux 会使用重定向指定的文件来替换标准文件描述符,会读取文件并提取数据,如同是在键盘上键入的。
  • STDOUT -- 标准输。同标准输入类似,shell 所有输出都会被定向到标准输出中,也就是显示器。
  • STDERR -- 错误消息对于错误消息,也是重定向到显示器的,但是错误消息和一般标准输出是分开的。比如: ls -l badfile > test ,如果没有 badfile 就会报错,但是错误信息并没有给 test

我们之前知道如何重定向标准输入和标准输出,现在讲解如何重定向错误信息。

  • 只重定向错误:将 STDERR 文件描述符的值 (2) 紧紧放在重定向符号前, ls -al badfile 2> test 。但是这种方法,如果该命令的输出既包含错误信息,也包含标准输出,那么标准输出还是会输出到屏幕中。
  • 重定向错误和数据:我们肯定不希望重定向的数据和错误信息在同一文件中(难道你希望错误日志里面还给你保存几句莫名其妙的打印语句吗),所以必须使用两个重定向符号,需要在符号面前放上各自文件描述符值: ls -al test test2 test3 badtest 2> testSTDERR 1> testSTDOUT (其中 test* 文件都存在, badtest 不存在)。 当然,也可以将数据和错误重定向到同一文件: ll n_File bad_File new_File &> STD_ALLbash shell 自动赋予了错误消息更高的优先级,方便集中浏览错误信息,也就是说, ll A B CB 文件不存在,那么输出时会优先输出关于 B 的错误信息。

# 脚本中的标准重定向输出和输入

对于三个标准文件描述符,一定要记住,使用 exec 命令时数字放在箭头左边。当数字出现在右边,要用 & 符号。

# 临时重定向

在脚本中生成错误消息,可以将单独的一行输出重定向到 STDERR ,不同的是,在脚本中,格式为 >&2echo "This is error" >&2 ,这样,该行就会指向 STDERR 的位置,也就类似于终端上的错误信息一样。

这种操作可以用于检测脚本运行时传入的选型或者参数是否正确,如果不正确就可以通过 >&2 来生成错误信息。

cat test
#!/bin/bash
echo "This is error" >&2
echo "This is normal"
# 运行脚本,并将脚本的错误信息重定向到 result1 文件中
./test 2> result1
This is normal
cat result1
This is an error

# 永久重定向

上面的临时重定向,比如我一个脚本有很多行生成错误信息并需要重定向,而每一行都是用 >&2 太麻烦了,那么就可以使用 exec 命令来进行永久重定向。

exec 命令告诉 shell 在脚本中执行期间重定向某个特定文件描述符。

#!/bin/bash
exec 1> STD_output_file
....
# 那么脚本中该命令之下的所有标准输出都重定向到了 STD_output_file 中
# 同理,改变标准输入,这对于从待处理文件中读取数据有很大帮助
exec 0< STD_input_file

# 自定义重定向

Linux 系统本来每个进程都有 9 个文件描述符,3 个标准,剩下 6 个都可以用于自定义,这 6 个可以任意作为输入还是输出

exec 6> testout
echo "This is a data">&6
# 如果使用 >> 就是追加模式

如果你想恢复一个被重定向的文件描述符

exec 3>&1   #3 指向 1
exec 1>testout
.....
exec 1>&3

创建读写文件描述符

exec 4<> testfile
# 输入输出时,文件指针是共享的。同时注意 >> 的追加模式与该特性的使用

关闭文件描述符时,一般创建了新的输入输出文件描述符,脚本退出时, shell 就会自动关闭它们。手动关闭: exec 3>&- 。一旦关闭,就不能在写入 / 读取数据,否则就会报错。

# lsof 命令

网上给的知识有些凌乱,甚至有些错误,所以我会总结几篇文章,最后给一下参考。

lsof--list open files ,列出当前系统已经打开的所有文件,一般 lsof 命令位于 /usr/bin/losf 或者是 /usr/sbin/lsof 。因为终端运行时,会有很多文件被打开使用,如果直接使用 lsof 会出现很多结果,我们在使用时一定要灵活使用相关的选项来控制输出。

选项描述
-a对给的选项进行运算
-p<pid>列出指定进程号所打开的文件
-d <文件号>列出占用该文件号的文件
+d <目录>列出目录下被打开的文件
-c <进程名>列出指定进程所打开的文件

根据上面的部分选项,我们使用一下该命令

lsof -a -p $$ -d 0,1,2
COMMAND   PID USER   FD   TYPE DEVICE SIZE/OFF NODE NAME
bash    29075 cyan    0u   CHR  136,0      0t0    3 /dev/pts/0
bash    29075 cyan    1u   CHR  136,0      0t0    3 /dev/pts/0
bash    29075 cyan    2u   CHR  136,0      0t0    3 /dev/pts/0
# 变量 $$ 表示当前 shell 的 pid
# COMMAND: 正在运行的命令名的前 9 个字符
# FD: 文件描述符号以及访问类型 (r-- 读,w-- 写,u-- 读写)
# TYPE: 文件的类型 (CHR-- 字符型,BLK-- 块型,DIR-- 目录,REG-- 常规文件)
# NAME:文件描述符所使用的文件的完整路径名

现在编写一个简单的脚本 test ,本文主要是讲解文件描述符。

#!/bin/bash
exec 4> four_data
while [ $var -eq 10] #死循环
do
	var=10
done

使用 & 将该脚本置入后台运行: ./test&

cyan@cyan-virtual-machine:~/Templates$ ./test&
[1] 88214
cyan@cyan-virtual-machine:~/Templates$ ps
    PID TTY          TIME CMD
  88084 pts/3    00:00:00 bash
  88214 pts/3    00:00:19 test
  88218 pts/3    00:00:00 ps

再使用 lsof 来查看使用了哪些文件描述符

cyan@cyan-virtual-machine:~/Templates$ lsof -a -p 88214 -d 4
COMMAND   PID USER   FD   TYPE DEVICE SIZE/OFF   NODE NAME
test    88214 cyan    4w   REG    8,5        0 935362 /home/cyan/Templates/four_data

-p 88214 是寻找进程为 pid=88214 的进程打开的文件, -d 4 找到使用 4 文件描述符的文件, -a-p 88214-d 4 进行与运算。

所以,你也可以尝试以下命令

lsof -p pid
# 查看这个进程的文件到底占用了哪些描述符

完成案例讲解,将 test 的进程杀死

cyan@cyan-virtual-machine:~/Templates$ kill 88214
cyan@cyan-virtual-machine:~/Templates$ ps
    PID TTY          TIME CMD
  88084 pts/3    00:00:00 bash
  88243 pts/3    00:00:00 ps
[1]+  Terminated              ./test

lsof 命令很强大,这里我们只是用来查看文件描述符,如果你想要深入了解,可以参考以下文章

Linux 查看端口占用情况:https://www.runoob.com/w3cnote/linux-check-port-usage.html

lsof 命令详解:https://www.cnblogs.com/klausage/p/14995042.html (其实并不是很详细)

lsof 入门:https://www.qieseo.com/162896.html

# 阻止命令输出

Linux 有一个文件叫 null(/dev/null)shell 输出到 null 文件的任何数据都不会被保存,全部都被丢掉。所以,不希望后台输出错误信息时, shell 发送电子邮件给进程属主的话,就将 STDERR 重定向到 nullnull 文件可以快速清除现有文件中的数据而不需要删除文件再重新创建 cat /dev/null > testfile ,类似于清空日志文件。哦,其实我个人更喜欢 echo "" > testfile

# 创建临时文件

/tmp 目录是 Linux 用来存放不永久保留的文件,大部分 Linux 发行版配置了系统在启动时自动删除 /tmp 目录的所有文件。任何用户账户都有权限在 /tmp 读写。
单独 mktemp 命令可以在 /tmp 目录中创建一个唯一的临时文件。 shell 会创建该文件,但不是使用默认的 umask ,会将文件读和写权限分给属主,并将你设为属主。但是其他人没法访问 (root 除外)。

cyan@cyan-virtual-machine:~/Templates$ mktemp
/tmp/tmp.thPVXNPVJp

也可以自定义文件名,但是会在当前目录下创建该文件

# mktemp 会用 6 个字符替换 6 个 X,保证文件名唯一
cyan@cyan-virtual-machine:~/Templates$ mktemp test.XXXXXX
test.k5CvV0
# 在脚本中,一般将创建的文件名保存在变量中
filename=$(mktemp test.XXXXXX)

还可以使用选项

# 在 /tmp 下创建,返回全路径名
cyan@cyan-virtual-machine:~/Templates$ mktemp -t test.XXXXXX
/tmp/test.LRNlpY
# -d 选项创建临时目录
cyan@cyan-virtual-machine:~/Templates$ mktemp -d dir.XXXXXX
dir.Adr0Q6
cyan@cyan-virtual-machine:~/Templates$ mktemp -d -t dir.XXXXXX
/tmp/dir.H9rIut

# 记录消息

tee 命令:
相当于管道的 T 型接头,将从 STDIN 过来的数据同时发送到 STDOUTtee 命令行所指定的文件名: tee testfile

cyan@cyan-virtual-machine:~/Templates$  date | tee testfile
2022年 08月 03日 星期三 15:51:57 CST
cyan@cyan-virtual-machine:~/Templates$ cat testfile
2022年 08月 03日 星期三 15:51:57 CST

tee 每次都会覆盖文件内容, -a 选型是追加模式: who | tee -a testfile

# 实例

下面脚本涉及 sql 语句,如果你不会,可以跳过。该脚本主要是从.csv 文件中读取数据快速生成 sql 执行文件。

#!/bin/bash
# 文件名 create_sql_file
outfile='members.sql' # 要输出的文件
IFS=',' # 重新定义分隔符
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 < ${1} # read 的标准输入重定向到 ${1} 文件。

直接使用 cat << file 可以追加数据到文件中,然后使用 ctrl+d 结束。也可以使用 << 结束符 ,通过输入结束符来结束输入。

我们写一个 mebers.csv 数据

cyan@cyan-virtual-machine:~/Templates$ cat members.csv
Cyan,Jack,US,qiqo,iasui,iwah
Mike,Smith,Japen,isaui,uwif,ianas

然后运行脚本生成 sql 文件。

cyan@cyan-virtual-machine:~/Templates$ ./create_sql_file members.csv
cyan@cyan-virtual-machine:~/Templates$ cat members.sql
INSERT INTO members (lname,fname,address,city,state,zip) VALUES
('Cyan','Jack','US','qiqo','iasui','iwah')
INSERT INTO members (lname,fname,address,city,state,zip) VALUES
('Mike','Smith','Japen','isaui','uwif','ianas')