Shell 骨干流程0——流程总述
Shell 骨干流程
我们在进行shell变成或使用CLI shell进行交互时,shell背后进行了复杂的处理流程。借由理清这个处理流程,会给我们对shell有更加深刻的认知。
shell关键概念中英文
为了更清楚的描述,我们给出如下shell中常用的中英文定义:
- blank:包含空格
space
和制表符tab
- control operator, 控制符:包含
newline, '||', '&&', '&', ';', ';;', ';&', ';;&', '|', '|&', '(', ')'
- field, 字段:shell扩展之一带来的文本单位。扩展后,当执行命令时,生成的字段将用作命令名称和参数
- job, 作业:在同一个进程组中的一系列进程,可由管道或衍生的进程组成
- job control, 作业控制:一种机制,用户可以通过该机制选择性地停止(挂起)并重新开始(恢复)进程的执行。
- metacharacter, 元字符:当不在引号中时,用于分割单词的字符,包括
space, tab, newline, '|', '&', ';', '(', ')', '<', '>'
- operator, 操作符:分为控制符或重定向符,操作符由至少一个元字符组成
- process group, 进程组:一系列具有相同组进程ID的进程
- reserved word, 保留字:对shell具有特殊含义的单词。,大多数保留字用于流程控制,如
for, while, if
- signal, 信号:一种机制,内核可以通过该机制将系统中发生的事件通知给进程
- token, 标记:可以被shell认为是一个独立单元的一串字符,分为单词word或操作符operator
- word, 单词:可以被shell认为是一个单元的一串字符,单词不能包括不带引号的元字符。
Shell处理流程图
我们首先给出shell的执行流程图,接下针对每一个步骤进行详细说明。
Figure 1: shell执行流程
大体流程
- 从文件、用户终端或其他唤起
shell
的方法中读取输入,通常shell
会按行处理,如果有复合命令和多行命令符号则另加处理步骤。 - 根据元字符(
space, tab, newline, '|', '&', ';', '(', ')', '<', '>'
)将输入的内容分割成各个标记(单词word或操作符operator),其中单词包括普通单词和保留字,操作符包括控制符或重定向符。 - 检查第一个标记(token)是否为引号(包括单引号,双引号,反斜杠),如果有引号则跳过部分流程。(引号处理)
- 检查第一个标记(token)是否为保留字(关键字),决定是否启用复合命令流程。(流程控制)。
- 检查第一个标记(token)是否为别名(alias),如果是则展开别名。
- 展开命令中的大括号。
- 展开波浪符号,即得到HOME_PATH。
- 参数展开,为
${...}
的展开,以及$Varname
的替换。 - 命令替换"``"或者
$(...)
使用子shell执行。 - 计算算术表达式。
- 将之前展开的命令、替换的命令根据分隔符再一次切割,然后重组成真正可以执行的命令。
- 根据通配符(
*,?
)等展开路径名和文件名。 - 根据重定向符号执行任何必要的重定向,之后参数列表中删除重定向运算符及其操作数。
- 根据扩展后命令的首个单词在
$PATH
和内建命令中查找可执行命令或文件。 - 执行命令,其中首单词为命令用
$0
表示,后面为此命令的参数。如果遇到文件结束符号EOF则完成shell流程,否则读取下一条命令从第1步再开始执行。
形成初步命令
我们将第1-4步分为第一大步,其主要作用是形成初步命令。在流程图中为橙色部分,其主要处理流程为元字符分割为标记、引号处理、shell命令解析、别名展开。具体内容见《shell-骨干流程1——形成初步命令》。
复合命令与流程控制
这一步发生在第3步,即检查标记是否为保留字这步,如果为合法保留字就需要组成复合命令。如图中红色部分所示。由于复合命令与形成初步命令往往是交互进行的,因此我并没有将其标注成独立的步骤,详见《shell-骨干流程2——复合命令与流程控制》。
命令展开
命令展开如图中绿色部分所示,包含5-11步。第5-7步都是各式各样的命令展开。基本的命令展开包括3种,分别为:大括号展开、波浪符号展开、参数与变量展开。这三种命令展开本质上是shell语法糖的展开。第8,9两步实际上是子命令执行,并非语法糖,原始命令将启动子进程(子shell)来执行子命令,执行的结果作为标记,嵌入到原命令中。5-7步和8-9步的区别在于,5-7步本质是查找语法糖对应的内容进行展开,无需使用子进程;8-9步本质是采纳子进程执行的结果,而非简单的查找替换。经过5-9步的处理,原始命令已经能够被shell直接执行,因此我们需要第10步将这些替换过后的内容重组起来,根据系统分隔符(Internal Field Seperator,IFS)再次分割(因为命令展开过程中会带来新的内容)。最后我们还需要第11步,展开路径和文件名,这一步和之前5-7步展开又是不同的,之前是语法糖替换,而这次是使用shell模式匹配方式(通配符)替换。第11步的shell模式匹配是正则表达式简化版,主要是利用通配符,并非完整的正则表达式规则。详见《shell-骨干流程3——命令展开》。
I/O与重定向
第12部分(紫色方框)是执行任何必要的重定向,并从参数列表中删除重定向运算符及其操作数。这部分涉及到进程标准I/O和/dev
下的各种设备文件描述符,在文章《linux-从设备文件看重定向》中有详细介绍。
命令执行与job控制
第13,14步是真正的命令执行阶段。如图中蓝色部分所示。第13步是保证命令的存在及可执行性,在非复合命令中,首个单词(word)通常指的是需要执行的命令,后面的部分都是该命令的参数。最终在第14步执行命令+参数,并返回结果。如果存在未执行命令则读取下一条命令从头在开始处理,若遇到文件结束符(EOF)则完成shell流程。
作业控制(job control)是针对即将执行和正在执行命令的一套控制机制,也是shell流程中不可缺少的一部分。命令执行与job控制部分详见《shell-骨干流程4——命令执行与job控制》。
具体例子
为了更好的理解整体流程,我们使用https://se.ifmo.ru/~ad/Documentation/Bash_Shell/bash3-CHP-7-SECT-3.html中的例子对应上图中的步骤进一步讲解。
- 读取命令
ll $(type -path cc) ~/.*$(($$%1000))
; - 将
ll $(type -path cc) ~/.*$(($$%1000))
分割成不同的标记,此处分割为:ll, $, (, type, -path, cc, ), ~/.*, $, (, (, $$%1000, ), )
; - 命令中不含有引号,无操作;
ll
不是保留字,无操作;- 检测到
ll
为别名,替换为ls -l
。ls -l $(type -path cc) ~/.*$(($$%1000))
,然后,从流程开始再执行一遍步骤1-3,在步骤1中将ls -l
再分割为ls, -l
两部分; - 不含有大括号,无操作;
- 发现波浪符号,将
~
展开为/home/username
,ls -l $(type -path cc) /home/username/.*$(($$%1000))
; - 发现
$$
符号,将$$
参数展开为当前进程号2537(根据实际情况,进程号都不相同),且ls -l $(type -path cc) /home/username/.*$((2537%1000))
; - 发现
$()
符号,执行命令替换,开启子shell执行type -path cc
,结果为/usr/bin/cc
,ls -l /usr/bin/cc /home/username/.*$((2537%1000))
; - 发现
$(( ))
算数运算符号,进行算术运算2537%1000=537,代入原命令:ls -l /usr/bin/cc /home/username/.*537
; - 未发现新的分隔符(IFS),无需进行再分割,无操作;
- 发现通配符"*",进行展开得到
.hist537
文件,ls -l /usr/bin/cc /home/username/.hist537
; - 未发现重定向操作符,无操作;
- 首个单词为“ls”,在
$PATH,/usr/bin
中检索到ls
命令; - 执行命令
/usr/bin/ls
,后面的-l /usr/bin/cc /home/username/.hist537
为命令的参数,其作用为查看/usr/bin/cc /home/username/.hist537
这两个文件的详细属性。此命令后不再有其他命令,结束此shell流程。