Linux基础系列文章大纲
Shell系列文章大纲


也许你并没有真的掌握bash的重定向

今天有一位大佬问了我一个shell命令行重定向的问题。原问题如下:

1
2
3
# /err是不存在的,所以ls /err会报错
# /tmp存在,所以ls /tmp没有错误
$ (ls /err /tmp 2>&1) >a.log

大佬疑惑的点在于,ls的标准输出和标准错误的目标分别是哪里。

对于上面这个命令,肉眼看上去,答案挺明显的,标准输出和标准错误都输出到a.log文件中,但可能有些人并不理解背后是为什么。

而且下面再改一下,可能会更具迷惑性:

1
$ (ls /err /tmp 2>&1) >a.log 2>err.log

再问,ls命令的标准输出和标准错误的目标分别是哪里?

答案仍然一样:标准输出和标准错误的目标都是a.log文件。

这条命令的背后发生了什么才会导致如此结果呢?这条命令看上去是重定向的问题,但实际上是shell解析命令行的问题。理解了shell是如何解析命令行的,这个重定向问题将非常透彻。

对此,在这里我做一个详细的分析。

细节分析

对于命令(ls /err /tmp 2>&1) >a.log 2>err.log来说,下面是从敲下回车键到ls命令开始执行的整个过程描述。

1.当前的bash进程扫描(读取)我们所敲下的命令行,扫描完成后,bash进程开始解析命令行

2.bash在解析命令行的过程中,首先发现命令行中有一对小括号,包含命令的小括号是bash的保留关键字,表示将括号内的命令放在子shell进程中执行

这里保留关键字是一个关键点。除了包含命令的小括号()是保留关键字,还有包含命令的大括号{}、for循环的for、case语句的case、if条件语句的if,等等都是保留关键字。

保留关键字有什么特殊呢?不用管它的特殊性,不过既然都是保留关键字,bash对待包含命令的()和对待for关键字的方式是一样的,相当于是一个名为(的特殊命令。

这意味着该命令行小括号部分不会立即去解析执行,而是保留下来不动。

3.bash将小括号部分保留下来后,继续解析后面的内容,发现有两个重定向动作

对于>a.log,bash会立即打开a.log文件,并将标准输出fd=1关联到打开的a.log文件。

然后对于2>err.log,bash会立即打开err.log,并将标准错误fd=2关联到打开的err.log。

4.命令行解析完成,开始执行命令。这个命令就是上面保留下来的括号部分

5.因为小括号表示在子shell中执行括号内的命令行,所以当前正在解析命令行的bash进程会fork创建一个子bash进程用来执行括号内的命令行

fork出来的子进程继承了父bash进程设置的标准输出和标准错误重定向,也就是说,在子bash进程中标准输出fd=1也关联到了打开的a.log文件,标准错误fd=2也关联到了打开的err.log文件。

子bash进程开始读取并解析括号内的命令行,即ls /err /tmp 2>&1部分。

子bash在解析命令行过程中发现有一个重定向操作2>&1,这表示将标准错误fd=2重新关联到fd=1所关联的目标,而此时fd=1所关联的目标是已打开的a.log,所以2>&1的结果是子bash进程的fd=2关联到已打开的a.log,而fd=1则没有变化,仍然是关联到已打开的a.log,也就是说,标准错误和标准输出都关联到了a.log。

6.子bash进程解析完括号内的命令行后,开始执行括号内的命令ls,于是ls命令将标准错误和标准输出都写入到a.log