Shell:一个命令行解析和重定向的问题分析
也许你并没有真的掌握bash的重定向
今天有一位大佬问了我一个shell命令行重定向的问题。原问题如下:
1 | # /err是不存在的,所以ls /err会报错 |
大佬疑惑的点在于,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。