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

bash命令的执行分为四大步骤:输入解析扩展执行
本文将详述bash命令的通常处理过程:
如图所示
图片描述shell

输入

交互模式

在交互模式下,输入来自终端。bash使用GNU Readline库处理用户命令输入,Readline提供相似于vi或emacs的行编辑功能(如Ctrl+aCtrl+e等等)。
当敲击键盘时,字符会存入Readline的编辑缓冲区,Readline会处理输入的变化并及时地将结果显示到终端上。
Readline还要保持命令提示符(prompt)的稳定(好比提示符的颜色)。
在将编辑缓冲区的内容交给bash以前,Readline会执行历史扩展(见这里),以后由bash负责将本条命令存储到历史列表并进入下一步骤。segmentfault

非交互模式

在非交互模式下,输入通常来自文件。此时,bash使用C语言标准库的stdio来得到输入。
不像Readline那样须要实现各类功能,stdio的工做较为简单:缓冲文件内容并逐行提供输入给bash处理。centos

解析

解析阶段的主要工做为:词法分析语法解析
词法分析指分析器从Readline或其余输入获取字符行,根据元字符将它们分割成word,并根据上下文环境标记这些word(肯定单词的类型)。
元字符包括:数组

|  & ; ( ) < > space tab

语法解析指解析器和分析器合做,根据各个单词的类型以及它们的位置,判断命令是否合法以及肯定命令类型。
单词(word)有不少种,bash从左到右依次分析它们的类型。下面对一些状况作一下简介:
一、重定向
分析器分析每一个单词,若是单词表示一个重定向,则保持至执行阶段再处理。
二、赋值语句
对于非重定向的首个单词进行分析,若是该单词是一个赋值语句,则保持至扩展阶段处理。
而后继续分析下一个单词,对于连续的赋值语句重定向都作如上处理。
三、关键字
对于非重定向或赋值语句的第一个单词进行断定,若是是保留关键字,则根据语法定义断定该种命令类型的语法和结尾(结尾通常为某种控制操做符)。
四、别名
若是非重定向或赋值语句的第一个单词是一个普通单词,bash会根据别名记录断定该单词是否是一个命令别名,若是是,则使用对应的文本替换该别名(注意此文本能够是shell可以接受的任意字符)。
而后继续分割并断定替换后的文本,重复上述一样过程,若是替换后仍有别名(不一样于前面曾扩展过的别名),则递归地展开并断定。
另外,默认时只有在交互式shell环境下才容许别名扩展。若是须要在脚本中使用命令别名,则需开启选项shopt -s expand_aliases。因为别名的功能均可以用函数实现,建议在脚本中使用函数来代替命令别名。
五、其余
若是非重定向或赋值语句的第一个单词不是别名或复合命令的起始单词,解析器将标记它为命令名,并赋值给位置变量0,其他单词(控制操做符以前的)为此命令的参数($一、$2...$n)。缓存

而后分析器继续分析下一条命令(控制操做符以后的),直到整行都分析完毕。bash

注意,在同一命令内,赋值语句后面必须是一个简单命令。若是是复合命令,将会报错。函数

还要注意,引用(见这里)会使元字符失去其特殊意义,其内部的多个单词可能会被bash看作是一个wordcentos7

最终解析器返回一个C结构体来表达一个命令(对于复合命令,这个结构体中可能还包含有其余命令),而后将其传递给shell的下一阶段:单词展开。spa

扩展

扩展阶段对应于单词的各类变换,最终获得可用于执行的命令。
以以下脚本为例解释此阶段依次进行的扩展(各类扩展的方法请看以前的文章):操作系统

#!/bin/bash
TMP='temp/tmp' num=2
cat ~/"${TMP:0:$((num+2))}"/test_{[0-9],[a-z]}.txt

脚本第三行是一条简单命令(只为举例说明)。

大括号扩展

首先进行的是大括号扩展,此扩展会致使单词数量的变化。

扩展后的命令形如:

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

单词分割

单词分割只做用于前一种扩展(变量、命令、进程、数学扩展)的结果,若是扩展处于双引号中,则不会分割(变量或数组使用@的状况例外)。

bash利用环境变量IFS的值进行单词分割,若是扩展的结果单词中包含IFS中的任意字符,则被分割为多个单词。若是扩展的结果为空,则此单词被移除(引号中的空值会被保留)。

咱们的例子中扩展的结果单词temp不包含IFS中字符,因此没有进行单词分割

注意若是没有上述扩展发生,也不会进行本阶段的单词分割。

路径扩展

单词分割结束后,bash扫描每一个单词中的字符*?[,若是包含这些字符,此单词就做为一个模式对文件名进行通配符匹配
匹配到的全部结果将成为命令的新单词。

咱们的例子中,路径扩展后的命令形如:

cat /root/"temp"/test_1.txt /root/"temp"/test_4.txt /root/"temp"/test_x.txt

移除引用

路径扩展完毕后,将移除全部的非扩展结果的引用字符(包括'' "" \)。

咱们的例子中,做用于单词temp的双引号,并非扩展后的结果,因此会被移除:

cat /root/temp/test_1.txt /root/temp/test_4.txt /root/temp/test_x.txt

脚本执行:

[root@centos7 temp]# ./test.sh 
我是文件 test_1.txt
我是文件 test_4.txt
我是文件 test_x.txt
[root@centos7 temp]#

抛开咱们的例子,若是一条简单命令有前置的赋值语句,等号右边的单词会通过:波浪号括展变量|命令|进程|数学扩展移除引用。大括号扩展、单词分割和路径扩展不会发生。

执行

不一样类型的命令,bash的执行方式有所差别。

复合命令

bash中每种复合命令都使用一个C函数来实现,功能包括执行恰当的展开(如for循环中关键词in后面的单词),执行特定的命令,根据命令的返回值来变动执行流程等等。

管道命令

对于管道命令,管道两侧的命令会在不一样的两个子进程中执行。
此时命令要前后经历
一、fork()系统调用建立子进程。
二、链接管道
而后命令的执行步骤以下述简单命令的执行。

简单命令

不管是什么类型的命令,最终都将归结到简单命令的执行。
一条简单命令的执行过程以下:
命令搜索
一、若是命令名中包含字符/(目录分隔符),则直接执行该路径指定的文件。
二、若是命令名中无斜线,则搜索当前环境中定义的函数,若是找到,则执行该函数。
三、若是未找到函数,则搜索内置命令,若是找到,则执行该内置命令(注意内置命令eval会使其后的全部单词再次通过解析、扩展和执行)。
四、若是没有对应的内置命令,则搜索hash缓存中记录的对象,若是有该命令的缓存,则直接执行该绝对路径对应的文件。
五、若是hash表中无缓存记录,则搜索环境变量PATH值中全部目录内的文件,若是找到该名称的文件,则执行(并缓存至hash表);若是未找到,则返回错误信息,设置返回值为127并exit。
命令执行
对于命令的执行,咱们介绍更通常的状况(命令位于磁盘文件系统之上的状况):
一、bash执行fork()系统调用建立子进程(若是命令已经处于子shell内,则不会再次fork(),例如上述管道命令)
二、执行重定向
三、执行execve()系统调用,控制权移交给操做系统。
四、内核判断该文件是不是操做系统可以处理的可执行格式(如ELF格式的可执行二进制文件或开头顶格写#!的可执行文本文件)
五、若是操做系统可以处理该文件,则调用相应的函数(二进制文件)或解释器(脚本文件)进行执行。
六、若是文件不具有操做系统的可执行格式(如文本文件但没有顶格写的#!),execve()失败,此时,bash会判断该文件,若是该文件有可执行权限而且不是一个目录,则认为该文件是一个脚本,因而调用默认解释器解释执行该文件的内容。
七、执行完毕后,bash收集命令的返回值。

这些,就是bash执行命令的整个流程。

相关文章
相关标签/搜索