Shell:变量赋值时的命令行解析
变量赋值时的命令行解析
问题描述
今天群里遇到了这么个bash的变量赋值问题:
1 | $ c=1 |
初看之下,就是一个变量赋值的错误,但这个问题却一点都不简单,甚至难度系数很高。如果没有深入理解bash解析命令行的规则,基本不可能分析出报错结果为什么是tmp1=abc: command not found
。
涉及到的理论
在bash中,虽然我们经常敲的是下面几种命令行格式:
1 | var=value # 变量赋值语句 |
但实际上,上面三种命令行格式可以随意组合:
1 | var=value CMD args |
甚至,对于变量赋值和重定向操作可以同时有多个:
1 | var1=value1 var2=value2 |
事实上,bash中一个简单命令(即不是命令组合或结构体)的完整格式是这样的:
1 | var=value ... CMD args >redirection ... |
也就是说,一个完整的简单命令由三部分组成:变量赋值部分、命令和参数部分、重定向部分。
其中:
- 变量赋值和重定向部分可以同时有多个
- 变量赋值必须在CMD的前面
- 变量赋值部分的变量名必须符合bash要求的命名规范:
- 普通变量的变量名可以是字母、下划线和数字,但以字母或下划线开头
- 还有bash支持的一部分特殊变量名,比如
$$ $1 $0 $?
(实际上变量名不带$
前缀,这里加上是为了更易懂)
- 重定向部分可以在任意位置,比如
>redirection CMD args
也是合理的 - 变量赋值和重定向操作从左向右执行
- 这三部分可以随意省略,随意组合
对于变量赋值部分:
- 如果有变量赋值而没有命令部分,则变量赋值在当前shell下生效
- 如果有变量赋值也有命令部分,则变量赋值只在该命令的环境变量中生效,不影响当前shell
- 如果变量名不符合bash的变量命名规范,则不认为是变量赋值语句,而是当作CMD命令部分
1 | # 只有变量赋值部分,而没有命令部分,因此a变量在当前shell下生效 |
理论再进一步:shell如何解析一个完整的简单命令
前面解释过,一个简单命令的完整格式包括三部分:
1 | var=value CMD args >redirection |
无论它们如何省略或如何组合,shell都将它们作为一个包含三部分的完整简单命令来解析。
对于简单命令,解析规则和顺序如下:(已经明确说了是简单命令,因此在解析简单命令之前已经对命令行划分好了token和操作符)
问题最终的解答
有了前面的理论基础之后,再回到最初的问题:
1 | $ c=1 |
现在分析这个问题已经很简单了。
对于tmp$c=$(echo abc)
,它是一个简单命令,它看上去是一个变量赋值语句。
但实际上,在上文描述的第一步:标记变量赋值和重定向并将它们保留下来的步骤当中,就被发现了这不是一个变量赋值语句,因为变量名不符合命名规范,于是将它当作一个普通的命令而非变量赋值语句。
既然这个看似是变量赋值的部分被当作命令,那么在上文描述的第二步,开始对该命令部分执行各种Shell扩展,对于tmp$c=$(echo abc)
这个命令来说,它涉及两部分扩展。首先变量扩展后得到tmp1=$(echo abc)
,然后命令替换得到tmp1=abc
。然后第二步完成。
因为这里没有变量赋值和重定向,所以没有第三步和第四步的过程。
于是到第五步,开始执行第二步完成之后得到的命令tmp1=abc
,显然这个命令不存在,于是报错tmp1=abc: command not found
。
参考阅读
全部在man bash中:
- 变量命名规范:Definitions小节的name说明
- 赋值语句:Shell Parameters小节
- 简单命令的解析流程:Simple Command Expansion小节