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

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

输入

交互模式

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

非交互模式

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

解析

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

|  & ; ( ) < > space tab

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

然后分析器继续分析下一条命令(控制操作符之后的),直到整行都分析完毕。

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

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

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

扩展

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

#!/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后面的单词),执行特定的命令,根据命令的返回值来变更执行流程等等。

管道命令

对于管道命令,管道两侧的命令会在不同的两个子进程中执行。
此时命令要先后经历
1、fork()系统调用创建子进程。
2、连接管道
然后命令的执行步骤如下述简单命令的执行。

简单命令

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

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


学习
学习与分享

道可道,非常道。为学而不得不道。

3.1k 声望
249 粉丝
0 条评论
推荐阅读
SHELL(bash)脚本编程八:技巧
至此,我们介绍了linux系统中常用命令的使用方法,简述了bash程序的使用方法和工作流程。在使用bash编写脚本程序时,熟练掌握这些工具的用法,往往能够达到事半功倍的效果。

vvpale5阅读 5k

Bash 常用脚本片段
这段脚本非常有用,你只要在你的脚本开头加上下面的内容,就能以 --param value 的格式解析参数。由于这段脚本尽可能写的短小不占空间,所以格式方面会要求所有的参数都有值,例如不接受无参数的 --daemon,而必...

捏造的信仰5阅读 1.7k评论 1

工具篇:iTerm与Zsh
iTerm2支持许多的主题配色,可以自己定义,也可以参考网上现成的主题配色。我个人比较喜欢draculatheme配色。支持item,vim,phpstorm , 下方存在主题官网路径,按照教程安装即可。

super白4阅读 4.7k

如何使用zx编写shell脚本
在这篇文章中,我们将学习谷歌的zx库提供了什么,以及我们如何使用它来用Node.js编写shell脚本。然后,我们将学习如何通过构建一个命令行工具来使用zx的功能,帮助我们为新的Node.js项目引导配置。

chuck3阅读 1.2k

封面图
麒麟操作系统 (kylinos) 从入门到精通 - 常用软件安装 - 第三篇 常用软件安装(windows下的习惯)
本篇内容大部分从应用商店进行安装,部分通过官网下载,少部分通过命令行安装。1.原生应用1.1钉钉1.2飞书1.3 蓝信1.4 腾讯文档1.5 金山文档1.6 搜狗输入法(拼音)1.7 五笔输入法1.8 libreoffice官方也带了WPS,...

码上世界3阅读 7.5k评论 17

封面图
深入剖析容器网络和 iptables
Docker 能为我们提供很强大和灵活的网络能力,很大程度上要归功于与 iptables 的结合。在使用时,你可能没有太关注到 iptables 的作用,这是因为 Docker 已经帮我们自动完成了相关的配置。

张晋涛3阅读 1.3k

封面图
Metasploit实现木马生成、捆绑及免杀
在一次渗透测试的过程中,避免不了使用到社会工程学的方式来诱骗对方运行我们的木马或者点击我们准备好的恶意链接。木马的捆绑在社会工程学中是我们经常使用的手段,而为了躲避杀毒软件的查杀,我们又不得不对木...

白风之下1阅读 9k

道可道,非常道。为学而不得不道。

3.1k 声望
249 粉丝
宣传栏