原文链接
有关 Shell 重定向那些你不知道的故事
Contents
1 基础知识 1.1 数字的含义
1.2 什么是重定向 2 重定向小技巧
2.1 输出分类 2.2 进程代入 (Process Substitution)
3 重定向的本质与实现 4 STDIN 偷盗
(本文主要讨论 BaSH,并且基于 Linux 系统讨论)
在 shell 编程里,说起重定向大家恐怕都用过
2>&1
或者 > log
这样的操作。在执行这些操作的时候,你是否偶尔也想过,>/dev/<wbr />null 2>&1=和 =>2>&1 /dev/null
的行为是完全不同的?如果你有这些问题,## 基础知识
首先,先来介绍一些基础知识。
### 数字的含义
重定向里用到的数字被称作文件描述符(File Descriptor)。文件描述符与一个具体的文件相关联,
0,Standard Input 也称 stdin 中文名称是标准输入 1,Standard Output 也称 stdout 中文名称是标准输出
2,Standard Error 也称 stderr 中文名称是标准错误
当你通过
open(2)
系统调用打开一个文件文件时, op<wbr />en(2)
会返回一个新建的文件描述符。open(2)
时递增。所以,open(2)
时得到的返回值就是 _3_ ### 什么是重定向
通常情况下, _0_ 号描述符会从键盘获取输入数据而 _1_ , _2_
date 1>readme.txt
特别注意,由于 _1_ 、 _2_ 号描述符都是输出到显示器上的。
对于输出重定向,
date 1>readme.txt
和 date >readme.txt
是等价的。对于输入重定向 cat 0<readme.txt
和 cat <readme.txt
也是等价的。特别地,当采用
>>
符号做输出重定向时,## 重定向小技巧
### 输出分类
虽然我们可以通过 STDOUT 和 STDERR 将程序输出分成一般输出和错误输出两类,
# open three more fds for funnelling information of various levels
exec 3>/tmp/debug.log
exec 4>/tmp/verbose.log
exec 5>/tmp/info.log
echo >&3 I am a debug message
echo >&4 I am a verbose message
echo >&5 I am an info message
# close them after we finish
exec 5>&-
exec 4>&-
exec 3>&-
注:虽然程序退出后描述符会自动被关闭,
上面的程序引入了两个语法,
exec n>filename
以及 exec n>&-
前者用来开启新的描述符并把目标指向对应的文件。### 进程代入 (Process Substitution)
说实话我不知道这东西应该怎么翻译,
进程代入的意思就是把一个命令的输出直接重定向给另外一个命令作
diff <(ssh server1 rpm -ql) <(ssh server2 rpm -ql)
这条命令的作用就是把命令
ssh server1 rpm -ql
的输出与 ssh server2 rpm -ql
的输出做 diff 比较。进程代入的实现方式颇有意思,可以通过 echo 来一探究竟,
echo <(date)
这条命令会输出一个类似
/dev/fd/12
之类的东西。## 重定向的本质与实现
为什么
>/dev/null 2>&1
和 2>&1 /dev/null
的效果不同呢?为了解决这个问题,我们先来看看文件描述符与文件的关系。
首先,通过最左边的文件描述符表(File Descriptor Table)将一个文件描述符数字对应到一个文件表(File Table)中的文件项。
所谓“重定向”本质上就是通过修改文件描述符表中的文件指针(
比如,
3>&1
就会导致上图的结果。也就是将描述符 _3_ dup2(2)
的系统调用完成的。2>&1
在程序实现上就是#include <stdio.h>
#include <stdlib.h>
int main(int argc, char argv[])
{
dup2(1, 2);
fprintf(stderr, “hello, worldn”);
return 0;
}
理解了
dup2(2)
做的工作后,我们再来看看 2>&1 >/dev/null
和 >/dev/null 2>&1
的实现,int fd = open(“/dev/null”, “w+”);
dup2(fd, 1);
dup2(1, 2);
以上就是
>/dev/null 2>&1
的实现。我们先把描述符 _1_ 指向和 /dev/<wbr />null
的位置,然后再把描述符 _2_ 指向描述符 _1_ dup<wbr />2(2)
修改为了 /dev/null
因此描述符 _2_ /dev/null
了。/dev/<wbr />null
。再来看
2>&1 >/dev/null
的实现,int fd = open(“/dev/null”, “w+”);
dup2(1, 2);
dup2(fd, 1);
这里仅仅就是把两条
dup2(2)
语句的顺序调换了一下。/dev/null
。结果是描述符 _1_ 指向了 /<wbr />dev/null
而描述符 _2_ 仍然指向标准输出。理解了重定向的原理后,就很容易理解在 shell 中重定向的顺序是至关重要的。
## STDIN 偷盗
在理解了上面的内容之后,我们就可以解释并解决一个 shell 编程中的难题了,这就是“被盗窃的标准输入”(好吧,
先来看一个有意思的程序,
while read filename; do
rm -iv $filename
done < <(ls)
从外观上看,这是想把
ls
r<wbr />m -iv
在获得你许可的情况下,优雅地删除他们。rm
rm
答案是
rm -iv
偷了 read
的数据!因为 rm -iv
期待用户从标准输入给出一个 y 或 n 的答案以确认是否删除,但标准输入被 < <(ls)
重定向了。于是 rm -iv
开始在 < <(ls)
里寻找答案。如果找不到 y 或者 n 就一直寻找下去,直到把 < <(ls)
的内容消耗完。read
读不出数据,如何应对这个情况呢?这就需要我们利用之前讲到的知识了。
rm -iv
使用复制出来的标准输入。再把原来的标准输入重定向给 <wbr />< <(ls)
。请看修改后的程序:exec 3<&0
while read filename; do
rm -iv $filename <&3
done < <(ls)
由于在
exec 3<&0
放在了 < <(ls)
之前,描述符 _3_ 很好地保留了原来的标准输入(rm
时,我们把通过 <&3
rm
自身的标准输入再重定向回键盘输入。转载请注明来源链接 http://just4fun.im/2017/01/23/e3-80-90-e8-bd-ac-e8-bd-bd-e3-80-91shell-e9-87-8d-e5-ae-9a-e5-90-91/ 尊重知识,谢谢:)