当前位置: 首页 > Linux

SHELL(bash)脚本编程六:执行过程

时间:2023-04-06 23:14:53 Linux

bash命令的执行分为四个步骤:输入、解析、扩展和执行。本文将详细介绍bash命令的一般处理过程:交互方式下的输入在交互方式下,输入来自于终端。bash使用GNUReadline库处理用户命令输入,Readline提供了类似vi或emacs的行编辑功能(如Ctrl+a、Ctrl+e等)。当在键盘上打字时,字符会存储在Readline的编辑缓冲区中,Readline会对输入的变化进行处理,并及时将结果显示到终端。Readline也让命令提示符(prompt)保持稳定(比如提示符的颜色)。在将编辑缓冲区的内容交给bash之前,Readline会进行历史扩展(见这里),然后bash会负责将这条命令存储到历史列表中,并进入下一步。非交互模式在非交互模式下,输入通常来自文件。这时bash使用C语言标准库的stdio来获取输入。与Readline需要实现各种功能不同,stdio的工作比较简单:缓冲文件内容,提供输入给bash逐行处理。语法分析阶段的主要工作是:词法分析和句法分析。词法分析是指分析器从Readline或其他输入中获取字符行,根据元字符将其分词,并根据上下文对这些词进行标记(确定词的类型)。元字符包括:|&;()<>spacetab语法解析是指解析器和分析器相互配合,根据每个词的类型及其位置判断命令是否合法,判断命令类型。单词有很多种,bash从左到右按顺序分析它们的类型。下面简单介绍一些情况:1、重定向分析器对每一个词进行分析,如果该词表示重定向,则一直保留到执行阶段再进行处理。2、赋值语句分析第一个没有被重定向的词,如果该词是赋值语句,则一直保留到展开阶段处理。然后继续分析下一个词,对连续的赋值语句或者重定向做上面的处理。3.关键字判断非重定向或赋值语句的第一个词。如果是保留关键字,则根据语法定义判断命令类型的语法和结尾(结尾通常是某个控制运算符)。4.别名如果非重定向或赋值语句的第一个词是普通词,bash会根据别名记录判断该词是否为命令别名,如果是则将别名替换为对应的文本(请注意,此文本可以是shell可以接受的任何字符)。然后继续对替换后的文本进行划分判断,重复上述相同过程,如果替换后还有别名(与之前展开的别名不同),则递归展开判断。此外,默认情况下别名扩展只允许在交互式shell环境中使用。如果需要在脚本中使用命令别名,则需要启用选项shopt-sexpand_aliases。由于别名的功能可以用函数来实现,所以建议在脚本中使用函数而不是命令别名。5.其他如果非重定向或赋值语句的第一个词不是别名或复合命令的起始词,解析器会将其标记为命令名并赋值给位置变量0,其余部分此命令的单词(在控制运算符之前)参数($1、$2...$n)。然后解析器继续解析下一个命令(在控制运算符之后),直到解析完整行。请注意,在同一命令中,赋值语句后必须跟一个简单的命令。如果是复合命令,会报错。另请注意,引用(请参阅此处)会导致元字符失去其特殊含义,并且它们中的多个单词可能会被bash视为一个单词。最终,解析器返回一个表示命令的C结构(在复合命令的情况下可能还有其他命令),然后将其传递到shell的下一个阶段:单词扩展。Expansion扩展阶段对应单词的各种变换,产生可用于执行的命令。下面以脚本为例,讲解一下这个阶段的顺序扩容(各种扩容方法见上一篇文章):#!/bin/bashTMP='temp/tmp'num=2cat~/"${TMP:0:$((num+2))}"/test_{[0-9],[a-z]}.txt脚本的第三行是一个简单的命令(仅作为示例)。BraceExpansion大括号扩展以大括号扩展开始,这会导致单词数发生变化。扩展命令如下所示:cat~/"${TMP:0:$((num+2))}"/test_[0-9].txt~/"${TMP:0:$((num+2))}"/test_[a-z].txt波浪号扩展后面跟着波浪号扩展,~被$HOME的值代替。扩展命令如下所示:cat/root/"${TMP:0:$((num+2))}"/test_[0-9].txt/root/"${TMP:0:$((num+2))}"/test_[a-z].txt变量、命令、过程、数学展开在波浪符展开后依次展开,依次是变量展开、命令替换、过程替换、数学展开,依次展开他们的发生。对于嵌套情况,先进行内部扩展。扩展后的命令如下:cat/root/"temp"/test_[0-9].txt/root/"temp"/test_[a-z].txt分词分词只对前一个扩展有效(变量,命令,process,mathexpansion),如果展开是在双引号内,则不会被拆分(使用@的变量或数组除外)。bash使用环境变量IFS的值来拆分单词。如果扩展后的结果词中包含IFS中的任何字符,则将其拆分为多个词。如果扩展的结果为空,则删除该词(保留引号中的空值)。在我们的例子中,扩展后的词temp不包含IFS中的字符,因此不进行分词。注意,如果以上扩展都没有发生,则该阶段不会进行分词。路径扩展单词拆分结束后,bash扫描每个单词中的字符*、?和[。如果包含这些字符,则该词将用作对文件名执行通配符匹配的模式。所有匹配结果都将成为该命令的新词。在我们的示例中,路径扩展后的命令如下所示:cat/root/"temp"/test_1.txt/root/"temp"/test_4.txt/root/"temp"/test_x.txtRemovereference路径扩展完成后也就是说,所有非扩展结果引号字符(包括''""\)都将被删除。在我们的示例中,应用于单词temp的双引号未展开,因此它们将被删除:cat/root/temp/test_1.txt/root/temp/test_4.txt/root/temp/test_x。txt脚本执行:[root@centos7temp]#./test.sh我是文件test_1.txt我是文件test_4.txt我是文件test_x.txt[root@centos7temp]#除了我们的例子,如果一个简单的命令前面是赋值语句,等号右边的单词被传递:波浪括号扩展、变量|命令|处理|数学扩展和删除引用。不会发生大括号扩展、分词和路径扩展。执行的命令类型不同,bash的执行方式也不同。复合命令bash中的每个复合命令都是由一个C函数实现的。功能包括进行适当的扩展(比如for循环中关键字in后面的词)、执行特定的命令、根据命令的返回值改变执行过程等等。管道命令对于管道命令,管道两边的命令会在两个不同的子进程中执行。这时候命令要经过1.fork()系统调用创建子进程。2、连接管道,执行命令如下:简单命令执行。简单命令不管是什么类型的命令,最终都会归结为简单命令的执行。一个简单命令的执行过程如下:命令搜索1.如果命令名中包含字符/(目录分隔符),则直接执行路径指定的文件。2.如果命令名中没有斜线,则搜索当前环境中定义的函数,找到则执行该函数。3.如果没有找到该函数,则搜索内置命令,如果找到,则执行内置命令(注意内置命令eval会使后面的所有单词重新解析、展开并执行)。4.如果没有对应的内置命令,则查找hash缓存中记录的对象,如果有该命令的缓存,则直接执行绝对路径对应的文件。5.如果哈希表中没有缓存记录,则在环境变量PATH值中查找所有目录下的文件。如果找到具有此名称的文件,则执行它(并将其缓存在哈希表中);如果没有找到,则返回错误信息并设置返回值为127并退出。命令执行对于命令的执行,我们介绍一种更一般的情况(命令位于磁盘文件系统的情况):1.bash执行fork()系统调用创建一个子进程(如果命令已经在子shell,则不会再执行)fork(),如上述管道命令)2.执行重定向3.执行execve()系统调用,控制权交给操作系统。4、内核判断文件是否为操作系统可以处理的可执行格式(如ELF格式的可执行二进制文件或头顶带#!的可执行文本文件)5、操作系统是否可以处理文件,调用相应的函数(二进制文件)或解释器(脚本文件)执行。6.如果文件不具有操作系统的可执行格式(例如文本文件但没有#!),则execve()失败。这时bash会对文件进行判断。如果文件具有可执行权限且不是目录,则认为该文件是脚本,调用默认解释器解释并执行文件内容。7、执行完成后,bash收集命令的返回值。以上就是bash执行命令的全过程。