LinuxSir.cn,穿越时空的Linuxsir!

 找回密码
 注册
搜索
热搜: shell linux mysql
查看: 163|回复: 0

Shell脚本深入教程:Bash解析命令行和eval命令

[复制链接]
发表于 2023-12-14 14:10:02 | 显示全部楼层 |阅读模式

Bash命令行解析和eval
当敲下命令行后,命令并非直接就执行起来,中间还会经历一些事情,比如Shell解析语法是否正确。

从敲下命令行到调用命令并开始执行,中间Shell进程对所敲下命令做的事,就是Shell对命令行的解析。

命令行解析,是深入Shell和Shell脚本的必经之路,也是一个为未来写命令行、写脚本节省大量时间和精力的重要知识点。

特殊符号优先级
重定向属于各个命令
管道连接两个命令
&& || ;优先级相同
小括号、大括号可以将命令组合成一个整体,但它们有特殊意义:
小括号使得命令在子Shell环境下执行
大括号使得命令在当前Shell环境下执行
例如:

# 重定向属于第二个命令,不属于第一个命令或命令整体
echo haha | echo hhh >/tmp/a.log
lsdasd | echo hehe 2>/dev/null   # 仍然会报错
lsdasd 2>/dev/null| echo hehe    # 不会报错
命令生命周期概述
先从全局的角度了解一下命令的生命周期。即一个命令从『出生』到『消亡』中间经历了哪些事。

比如对于下面的echo命令行,shell做了哪些事情?

var='hello world'
echo -e $var $(((2+3)/5))
读取命令行
解析命令行:发现有变量引用$var,于是将其替换成变量的值hello world,发现有数学运算,于是将其替换成对应的值1,所以替换后得到echo -e hello world 1命令行
命令行解析完成后,调用命令:
创建一个子shell进程,父shell进程被阻塞,它要等待子进程的退出,并且此时子进程获得终端控制权
在子shell进程中通过exec加载磁盘中的echo命令
exec加载命令时,会搜索echo命令,然后调用它,于是替换子shell进程并得到echo进程
echo进程开始执行,它要识别选项和参数,于是输出『hello world 1』到终端
echo进程退出,并记录一个退出状态码
echo退出后就回到了shell进程,shell进程会去读取子进程的退出状态码,shell进程读完echo进程记录的退出状态码后,echo进程完全消失,shell进程准备执行下一个命令
详细分析命令行解析的过程
整个命令行解析的过程如下图所示:



对于下面的echo命令:


  1. name="junma"
  2. a=20
  3. touch ~/i{a,b}.sh
  4. /bin/echo -e "some files:" ~/i* "\nThe date:$(date +%F)\n$name's age is $((a+4))" >/tmp/a.log
复制代码

涉及到的过程:

1.读取命令行,并将读取的字符内容交给词法解析器
2.词法解析阶段:

(1).解析引用(即识别双引号、单引号和反斜线),并根据空白符号和bash元字符,将读取的内容划分成token(在Shell语法中也成为word)
划分token的元字符有:| & ; ( ) < > space tab
解析引用是为了防止被引用的整体部分被分割成多个token
比如echo "ls|cat"不会因为里面有竖线就将引号包围的部分划分成多个token
(2).根据控制元字符,将复杂命令结构划分成简单命令结构
即将多个或复杂的命令行,划分成简单的一个一个命令
控制元字符有:|| & && ; ;; ( ) | |& <newline>
(3).检查第一个token:
如果第一个token是别名,则进行别名扩展
别名替换本不该是词法解析阶段完成的,因为涉及了Bash自身的语法支持,但因为别名扩展会直接影响命令行结构,所以在词法解析阶段处理它才更合理
如果第一个token是带有等号=且等号前的字符符合变量命名规范,则本条命令是一个变量赋值
如果是shell函数、shell内置命令、shell保留关键字,则做相应处理
因为上面的命令行中,没有复杂命令结构,只是单个echo命令行,而且第一个token没有别名,所以,划分token后的结果如下:



3.word扩展阶段(各种Shell扩展和替换)

称为word扩展是因为下面这些操作都可能会改变word(即token)的数量。

所谓扩展或替换,指的是Shell会分析各个token中的某些特殊符号,并进行对应的值替换。

Shell按照下面列出来的先后顺序进行各种扩展行为:

大括号扩展
波浪号扩展
变量替换
算术替换
命令替换
单词拆分
引号移除
此外,对于支持命名管道的Shell,还支持进程替换。因为进程替换中的命令是异步执行的,而且它不会将执行结果替换到命令行中,而是以虚拟文件的方式作为命令的标准输入或标准输出,所以不要考虑进程替换在哪个阶段执行,这没有意义。尽管官方手册说,进程替换可能在波浪号扩展、变量替换、算术扩展、命令替换这四个阶段的任何一个阶段执行。

下面是各个扩展阶段的分析:

(1).大括号扩展
例如echo hey{1..3}在这一阶段替换后变成echo hey1 hey2 hey3
(2).波浪号扩展,最常见的是~扩展成家目录,此外还有~+、~-等也是波浪号扩展
例如,对于root用户执行的命令ls ~/.ssh ~/.bashrc来说,在这一阶段替换后会得到ls /root/.ssh /root/.bashrc
(3).变量替换,最常见的是将变量的值替换到变量引用位置处,此外还有各种变量操作也是变量扩展
例如,ls /$USER在这一阶段替换后变成ls /root
再例如,echo ${#USER}在这一阶段替换后变成echo 4
(4).算术替换,即将算术运算的评估结果替换到算术表达式位置处
(5).命令替换,即执行命令替换中的命令,并将命令的标准输出替换在命令替换位置处
例如,echo $(hostname -I)在这一阶段替换后会变成echo 192.168.100.11
如果命令替换的命令有多行,则默认会压缩成单个空格。可使用双引号保护命令替换的结果
例如echo $(echo -e 'a\nb')会替换成echo a b
echo "$(echo -e 'a\nb')"会替换成echo $'a\nb'
(6).单词拆分(word splitting)
Shell重新扫描变量替换、算术扩展、命令替换后的结果,如果这三种替换是使用双引号包围的,则不会拆分开,如果它们没有使用双引号包围,则根据IFS变量的值再次对它们划分单词
例如n="name age";test $n -eq "name age"是错的,因为单词拆分后得到test name age -eq "name age",这会语法报错,但如果加上双引号包围"$n",则得到test "name age" -eq "name age"
如果没有变量替换、算术扩展、命令替换,则不会执行单词拆分
(7).路径名扩展,也即通配符扩展
通配符包括* [] ?
例如/root下有ia.sh和ib.sh文件,那么ls /root/i*.sh,路径扩展后命令变成ls /root/ia.sh /root/ib.sh
(8).引号去除,即移除为了保护Shell解析的那一层引号
命令在开始执行之前,所有不需要的引号(即Shell层次的引号)都会被移除
例如cat "/proc/self/cmdline"查看到的结果是cat/proc/self/cmdline
整个扩展过程如下所示:



关于word splitting和路径扩展,有一个注意事项:

touch "aa aaa.txt"
touch "bb bbb.txt"
for i in *.txt;do
  echo $i
done
因为在单词分割时,*.txt还没有扩展,等到路径扩展时,aa aaa.txt自然会被作为一个元素整体。

而下面代码是有问题的,因为命令替换在单词分割之前:

touch "aa aaa.txt"
touch "bb bbb.txt"
for i in $(ls *.txt);do
  echo $i
done
改进方式是修改IFS的值:

(IFS=$'\n';for i in $(ls *.txt);do echo $i;done)
当Shell处理完各种Shell扩展之后,意味着Shell的解析完成了,接下来准备让命令运行起来。

4.搜索命令并执行

Shell首先判断第一个token(即命令):

(1).如果命令中不含任何斜杠:
先判断是否有此名称的shell function存在,如果有则调用它,否则进行下一步搜索
判断该命令是否为bash内置命令,如果是则执行它,如果不是,则当作外部命令处理
(2).如果命令中包含一个或多个斜杠,则当作外部命令处理
如果发现要执行的是外部命令:

(1).Shell通过fork创建一个子shell进程,然后父Shell进程自身进入阻塞并等待子进程终止,同时会让出终端的控制权
(2).子Shell进程通过exec去调用外部命令并替换当前子Shell进程
exec调用外部命令时,会搜索命令,如果token中包含了斜线,则从相对路径或绝对路径中查找,否则从$PATH中搜索,如果找不到,则报错
替换子Shell进程后,就不再称为子Shell进程,而称之为对应命令的进程(比如echo进程)
命令退出后回到父Shell,父Shell去获取命令退出状态码并赋值给变量$?,然后就可以执行下一条命令。

eval命令


如果发现要执行的命令是eval命令,则会回到第一步从头开始解析(但移除eval这个token)。

所以,eval命令有二次解析的功能:第一轮解析已经将该扩展的扩展该替换的替换了,第二轮还可以再次扩展替换。

例如:

a=name
name=junmajinlong
eval echo \$$a
第一轮解析后得到的命令行为eval echo $name,然后eval会让Shell再次解析命令行,于是得到echo junmajinlong。

文章链接: https://www.junmajinlong.com/shell/script_course/shell_cmdline_parse_eval/
您需要登录后才可以回帖 登录 | 注册

本版积分规则

快速回复 返回顶部 返回列表