Bash解析器在命令行中转义字符和拆分单词/标记的顺序是什么



我正试图明确地理解Bash解析器的业务顺序。


此wiki页面声明以下顺序:

  1. 读取行
  2. 处理/删除引号
  3. 以分号分隔
  4. 处理"特殊运算符",根据文章,它们是:
    • 命令分组和大括号展开,例如{…}
    • 处理替换,例如cmd1 <(cmd2)
    • 重定向
    • 管道
  5. 执行扩展,这些扩展未全部列出,但包括:
    • 支架扩展,例如{1..3}。出于某种原因,这篇文章把这个问题塞到了前一阶段
    • 颚化符展开,例如~root
    • 参数&可变展开,例如${var##*/}
    • 算术扩展,例如$((1+12))
    • 命令替换,例如$(date)
    • 分词,适用于展开结果的;使用CCD_ 8
    • 路径名扩展或globbing,例如ls ?d*
  6. 分词,适用于整行的使用$IFS
  7. 执行

这不是引用,而是对链接文章内容的转述


此外,还有Bash手册页,这个SO的答案声称是基于这些页面。根据答案,命令解析的阶段如下:

  1. 初始分词
  2. 支架膨胀
  3. 波浪形展开
  4. 参数、变量和算术展开
  5. 命令替换
  6. 二次分词
  7. 路径扩展
  8. 报价删除

强调矿

我假设,作者所说的"初始单词分割"意味着对整行的分割,而"次要单词分割"则意味着对展开结果的分割。这将要求在命令解析过程中至少存在两个不同的标记化过程。


考虑到两个源之间的排序矛盾,相对于正在执行的其他操作,输入命令行被取消引号并拆分为单词/令牌的实际顺序是什么?


编辑注释:

为了解释部分答案,这个问题的早期版本有一个子问题:

为什么cmd='var=foo';$cmd产生bash: var=foo: command not found

shell解析的第一步是应用shell语法规则,这些规则必须提供POSIX shell命令语言语法规范中指定的语法的超集。

只有在这个初始阶段,才能检测到分配,而且只有在非常特殊的情况下:

  • ASSIGNMENT_WORD令牌必须由解析器生成(请注意,解析器只运行一次,并且在进行任何扩展后不会重新运行!)
  • =字符本身及其前面的有效变量名不得加引号

如果不显式调用eval(或将结果作为代码传递给另一个shell,或采取一些类似的显式操作),解析器永远不会在扩展结果上重新运行,因此,如果操作在进行扩展之前没有解析为赋值,则扩展的结果将永远不会生成赋值。

Posix为shell解释提供了一个精确的过程。然而,大多数shell(包括bash)都添加了自己的语法扩展。此外,该标准并没有坚持要求实际使用它的算法;只是最终结果是一样的。因此,标准算法和关于单个外壳的描述之间存在一些差异。尽管如此,总体情况是一样的。

理解标记化和分词之间的区别很重要。标记化将输入划分为具有语法意义的标记,然后shell语法使用这些标记对输入进行语法分析。语法标记包括分号和圆括号(标准术语中的"运算符")。一种特殊类型的令牌是WORD。

正如标准所指出的,标记化基本上是解析输入的第一步(但如下文所述,它取决于引用字符的识别。)

WORD可以随后通过应用各种展开来进行解释。应用于每个单词的精确扩展集取决于语法上下文;并不是所有的单词都是一样的。这一点已记录在标准的叙述性文本中。应用于某些word的一种转换是分词,它根据字段分隔符的存在将一个word拆分为word列表,默认情况下为空白(可通过更改IFSshell变量的值进行配置)。分词不会改变句法标记类型;事实上,当它发生时,句法分析已经完成。

并非所有单词都要进行分词。特别是,除非有一些扩展,并且只有扩展不在双引号内,否则不会执行分词。(即便如此,也不是在所有的句法语境中。)

将输入划分为令牌的算法必须与标准中的算法等效。该算法要求知道哪些字符被引用;大多数历史实现都是通过在内部用一个"引号"位标记每个输入字符来实现的。在标记化过程中是否删除引用字符在某种程度上取决于实现;该标准将引号删除步骤放在最后,但如果最终结果相同,则实现可以更早地完成。

请注意,=不是运算符字符,因此它不会导致var=foo被拆分为多个令牌。然而,以标识符开头、后跟=的令牌由shell解析器进行特殊处理;它们稍后被视为参数分配。但是,如上所述,分词不会改变word的语法性质,因此shell解析器不会将分词产生的word视为参数赋值。

我同意,我的问题要求很高,我非常感谢所有宝贵的意见。我感谢@rici和@CharlesDuffy。


下面是Bash如何解释和执行代码的大致概述。

第1阶段:线路馈电

Shell根据行读取输入。

第二阶段:代币化

行被分割成标记——单词和运算符,由元字符分隔。尊重引用('…'"…"),替换别名,删除注释。令牌边界在内部记录。

元字符为:<space><tab><newline>|&;()<>

第3阶段:命令解析

管道列表复合命令(循环、条件、分组)解析行。这为Bash提供了执行子命令的顺序概念。然后,每个子命令由其自己的解析周期单独处理。

第四阶段:语法

指定(命令名称左侧的指定)和重定向将被删除并保存以备以后使用。

第5阶段:扩张

展开按顺序进行:

  1. 支架扩展,例如{1..3}
  2. 颚化符展开,例如~root
  3. 参数&可变展开,例如${var##*/}
  4. 算术扩展,例如$((1+12))
  5. 命令替换,例如$(date)
  6. 过程替换,如果支持,例如cat <(ls)
  7. 单词拆分,应用于展开的未引用结果,使用IFS变量作为分隔符
  8. 文件名扩展或globbing,例如ls ?d*
  9. 报价删除:清除所有未报价的",而不是由扩展产生的

第6阶段:重定向

重定向现在执行,然后删除。以前从管道进行的重定向可能会被覆盖。

如果该行不包含命令名,则重定向不会产生任何影响;否则它们仅影响所述命令。

第7阶段:作业

分配现在执行,然后删除。它们的值(=右侧)经过:

  • 波浪形展开
  • 参数扩展
  • 命令替换
  • 算术扩展
  • 报价删除

如果该行不包含命令名,则赋值会影响当前shell环境;否则,它们仅为所述命令而存在。

第8阶段:命令和论据

此时,如果没有生成命令名,则该命令将退出。

否则,该行的第一个单词将变为命令,后面的单词是arguments。

第9阶段:执行


现在,回答我的问题。

如上所述:

  1. 代币化发生在第2阶段;在第5阶段和第7阶段中发生分词。这两者是不同的概念
  2. 引号(和反斜杠)在第2阶段发挥作用,通常在第5阶段删除。对于任务,他们可以活到第7阶段
  3. 赋值在第4阶段被识别,因此它们不能来自第5阶段发生的变量扩展

最新更新