3.4. 解析
解析引擎的初始工作是词法分析:将字符流分成单词,并将含义应用于结果。单词是解析器操作的基本单位。单词是由元字符分隔的字符序列,其中元字符包括空格和制表符等简单分隔符,或对 shell 语言有特殊意义的字符,例如分号和与号。
关于 shell 的一个历史问题,正如 Tom Duff 在他的关于 rc(Plan 9 shell)的论文中所说,没有人真正知道 Bourne shell 语法是什么。Posix shell 委员会在最终发布了 Unix shell 的明确语法方面功不可没,尽管它有很多上下文依赖关系。该语法并非没有问题——它不允许一些历史上的 Bourne shell 解析器在没有错误的情况下接受的构造——但它是我们所拥有的最好的语法。
bash 解析器源自 Posix 语法的早期版本,据我所知,它是唯一一个使用 Yacc 或 Bison 实现的 Bourne 风格的 shell 解析器。这带来了它自己的困难——shell 语法实际上并不适合 yacc 风格的解析,需要一些复杂的词法分析,并且解析器和词法分析器之间需要大量协作。
无论如何,词法分析器从 readline 或其他来源获取输入行,在元字符处将它们分解成标记,根据上下文识别标记,并将它们传递给解析器,以将它们组装成语句和命令。其中涉及大量上下文——例如,单词 for 可以是保留字、标识符、赋值语句的一部分或其他单词,以下是完全有效的命令
for for in for; do for=for; done; echo $for
它显示 for。
在这一点上,需要简要讨论一下别名。Bash 允许使用别名将简单命令的第一个单词替换为任意文本。由于它们完全是词法的,因此别名甚至可以用来更改 shell 语法:可以编写一个别名,它实现了一个 bash 没有提供的复合命令。bash 解析器完全在词法阶段实现别名,尽管解析器必须在允许别名扩展时通知分析器。
与许多编程语言一样,shell 允许对字符进行转义,以消除其特殊含义,以便元字符(如 &)可以出现在命令中。有三种类型的引用,每种引用略有不同,并且允许对引用的文本进行略有不同的解释:反斜杠,它转义下一个字符;单引号,它阻止解释所有包含的字符;双引号,它阻止某些解释,但允许某些词语扩展(并且对反斜杠的处理方式不同)。词法分析器解释引用的字符和字符串,并防止解析器将它们识别为保留字或元字符。还有两种特殊情况,$'…' 和 $"…",它们以与 ANSI C 字符串相同的方式扩展反斜杠转义的字符,并允许使用标准国际化函数分别翻译字符。前者被广泛使用;后者,可能是因为很少有好的例子或用例,使用较少。
解析器和词法分析器之间的其余接口很简单。解析器对一定量的状态进行编码,并与分析器共享,以允许语法所需的上下文相关分析。例如,词法分析器根据标记类型对单词进行分类:保留字(在适当的上下文中)、单词、赋值语句,等等。为了做到这一点,解析器必须告诉它解析命令的进度,它是否正在处理多行字符串(有时称为“here-document”),它是否在 case 语句或条件命令中,或者它是否正在处理扩展的 shell 模式或复合赋值语句。
在解析阶段识别命令替换结束的大部分工作都封装在单个函数(parse_comsub)中,该函数了解大量 shell 语法,并且复制了比最佳情况更多的标记读取代码。此函数必须了解 here 文档、shell 注释、元字符和词语边界、引用以及保留字何时可接受(因此它知道何时处于 case 语句中);花了相当一段时间才使它正常工作。
在词语扩展期间扩展命令替换时,bash 使用解析器来找到构造的正确结束位置。这类似于将字符串转换为 eval 的命令,但在此情况下,命令不是以字符串的结束位置为界。为了使这能够工作,解析器必须将右括号识别为有效的命令终止符,这会导致许多语法生成中的特殊情况,并且要求词法分析器将右括号(在适当的上下文中)标记为表示 EOF。解析器还必须在递归调用 yyparse 之前保存和恢复解析器状态,因为命令替换可以在读取命令过程中的提示字符串中间被解析和执行。由于输入函数实现了预读,因此此函数必须最后负责将 bash 输入指针回滚到正确的位置,无论 bash 是从字符串、文件还是使用 readline 的终端读取输入。这不仅对防止输入丢失很重要,而且对命令替换扩展函数构建正确的执行字符串也很重要。
可编程词语补全带来了类似的问题,它允许在解析另一个命令的同时执行任意命令,并且通过在调用周围保存和恢复解析器状态来解决。
引用也是导致不兼容和争论的来源。在第一个 Posix shell 标准发布 20 年后,标准工作组的成员仍在争论模糊引用的正确行为。与以前一样,Bourne shell 除了作为观察行为的参考实现之外,没有任何帮助。
解析器返回一个表示命令的单个 C 结构(在循环等复合命令的情况下,可能反过来包含其他命令),并将其传递给 shell 操作的下一阶段:词扩展。命令结构由命令对象和词列表组成。大多数词列表会根据其上下文进行各种转换,如以下部分所述。