**前言:**经过过去两章十二篇文章的学习,我们已经知道了进程的基本概念以及进程的控制方法。 本篇内容就是使用过去学习的内容自己写一个功能简单的shell外壳程序, 也就是我们使用的bash命令行。 本篇内容是过去进程知识的集大成者。 我们在这个实战程序中, 将过去学过的进程控制方法——创建、等待、退出、终止,各种概念——PID、优先级、子进程、环境变量、命令行参数等等都会回忆起来, 复习起来。 **ps:本节内容很综合, 难度很大。需要学完进程的概念以及进程控制的方法。建议学习前先去复习一遍进程概念相关知识点。**
shell是什么
shell是外壳程序, 也就是说bash, shell本质上也是一个进程。 而在执行命令的时候, 对于普通命令本质上就是自己创建子进程执行的。而对于内建命令, bash命令其实是直接在自己上面调用的一个函数。 我们看到的bash命令行, 其实就是一个输出的字符串。 这个字符串是由用户名 + 主机名 + 当前路径组成的。
我们自己制作的shell主要分为几个板块——用户交互获取命令行、分解命令行参数、内建命令执行、普通命令执行
实现过程
交互板块
命令行的外表
用户名, 主机名,当前路径都保存在环境变量中。
那么, 我们就可以封装函数, 获取三个环境变量——一个是用户名、一个是主机名、一个是当前路径
主函数部分我们这里只写一下输出, 先将命令行的外表打印出来:
上图的三个黄框框是博主定义的宏, LEFT是打印"[", RIGHT是打印"]"。
然后对于普通用户最后一个字符一般都是$符号, 那么我们就定义一个LABLE的宏, 让这个宏替换$。如下图:
此时完整的代码如下:
此时运行结果就能打印一个属于我们自己的命令行:
解决输入问题:
但是不能输入, 接下来我们就解决输入问题。我们可以定义一个字符串,这个字符串的长度是1024。
在主函数这里添加一个scanf:
要注意此时黄框框这里没有\n, 不然输入参数会多打印一个换行。 此时的运行结果如下, 可以输入了:
但是此时只能输入, 这些输入的字符不能全部保存到commendline数组中。 因为scanf遇到空格会终止读取。 所以就用到了另一个输入函数——fgets , 如下图:
fgets中, 第一个参数是要拷贝到的起始地址, 第二个参数是要拷贝的字节数, 第三个是从哪个文件读取。 我们是从显示器读取, 也就是stdin。 上图的s用来接收fgets的返回值, fgets的返回值是读取成功后的地址, 如果读取不成功就是返回NULL。 下图是运行结果:
上面我们打印之所以多了一个换行, 是因为我们在输入的时候回车符被读取进去了。 也就是相当于printf那里有一个我们自己加的换行, 然后commendline最后一个字符也是一个换行。 commendline里面的换行是我们不想要的, 请问我们怎么去掉呢?——既然回车符是最后一个字符, 那么我们就把最后一个字符变成\0。如下图:
下面是打印结果:
现在, 我们将我们上面写的命令行的外表, 命令行的获取这些与用户交互的操作独自封装成一个函数——Interact。
以上, 我们就实现了与用户的交互,获取命令行。 大体就是这样, 但是后续可能随着我们其他板块的增加, 修改里面的部分代码。 但不会影响大思路。
解析字符串
在正式解析字符串之前我们先把多次询问的问题解决。 因为我们的shell命令行是一直存在的, 不应该是使用一次后就退出。 这里我们要给代码套上一层循环。 ——即在我们要实现的逻辑:交互、解析、执行的外面套上一层死循环。这样就能让逻辑循环起来了。
运行情况就是如下,程序正常情况下已经不会退出了。(ctrl + C可以退出)
接下来正式对命令行进行解析:
下面是解析的原理:
strtok函数如下:
这里我们先定义一个分隔符的宏:
然后创建一个数组, 用来存储切割好的字符串。
然后我们就实现截取字串, 将字串放到argv[i]中, 放进去后就i++, 等到截取不了了就返回一个空串。 但是strtok第一次截取的时候可以传参commendline, 如果之后还是截取这个字符串, 那么就传NULL,我们把解析字符串也单独封装成一个函数, 返回值是一个argc, 也就是截取出来的字符串的个数。 如下图:
写好之后, 我们就在主函数上放上测试代码:
如下就是分解后的效果:
普通命令执行
然后就是执行命令,当id == 0的时候执行。
这个时候, 如果execvpe加载成功, 就执行加载的程序如果加载失败, 就直接退出。并且退出码EXIT_CODE, 这个是我们自己定义的宏。
我们这里先定义一个lastcode获取状态信息:
下面是执行普通命令的代码, environ是获取环境变量, 然后创建子进程, 如果id小于0的时候, 说明子进程创建失败, 那么直接返回。 如果id == 0, 说明是子进程,就加载程序。 我们的argv是数组, 所以带v, 但是没有绝对路径, 只有文件名, 所以要有默认搜索路径, 也就是加p。 另外还要有环境变量, 加上e。
如果是id > 0就说明是父进程, 父进程要等待子进程, 也就是下图的代码等待子进程。
然后, 我们运行我们的程序就可以执行一般的命令了。
内建命令
cd命令
但是, 这里有一个问题, 就是我们使用cd的时候, 我们会发现, 我们的工作目录不发生变化。这个是因为cd是一个内建命令!!!cd不是由bash的子进程加载得来的。就如同下图:
为什么会这样呢? 因为我们使用的fork创建的子进程。 子进程数据的修改不会改变父进程。 所以我们使用cd命令的时候, 虽然加载了cd命令, 但是子进程使用cd命令, 子进程工作路径修改, 不会影响父进程。
那么正常使用cd命令, 就不能创建子进程, 我们可以使用chdir。 现在是内建命令的处理:
对于这些内建命令, 我们的解决方式是对于这些内建命令一个一个地做特殊处理。 首先, 为了方便维护, 我们同样将内建命令的执行封装成一个函数:
函数要带参数argc, 我们的代码都是在这个函数中完成。
内建函数创建后, 我们的大体逻辑就搭建好了, 现在来看一下主函数的逻辑——先交互, 再解析, 再执行:
回到cd上来,首先chdir可以修改路径。 chdir可以修改当前的工作路径。但是我们也要获取当前路径用来修改PWD环境变量。
这里我们可以使用getcwd, 先创建一个pwd字符数组, 用来保存当前工作路径:
然后将我们原先定义的pwd修改成下图:、
既然获取当前路径的代码变了, 我们上面写的代码中, 有些地方也要改, 首先是交互函数的修改, 下图的红框框是getpwd的使用, 黄框框是工作路径的获取:
然后是内建函数的修L
此时, 我们的cd就能跑了:
ls颜色问题
处理颜色问题, 需要在最后加一个--color选项。 如果命令是ls, 那么就要处理一下, 也就是再最后加一个--color选项。 最好的是在命令行解析的函数里进行处理。 但是在里面处理会有argc的返回值问题。 所以为了方便, 博主这里放在了内建命令执行的函数里, 因为内建命令执行函数在普通命令执行函数之前, 并且博主的内建命令也有argc, 方便控制。
当我们再运行时, 就能看到ls的颜色了:
写到这里的时候, 博主发现了一个问题, 就是我们可以给内建命令一个返回值, 只要返回真, 说明执行了内建命令, 假就没有执行。 这样就能防止又执行了内建, 又执行了普通。
export
现在看一下新建环境变量export:
如果我们不做特殊处理, export创建环境变量, 是创建不出来的。因为我们直接使用export, 那就是创建子进程, 然后加载到子进程帮我们执行,然而子进程不会影响父进程。 所以就没有用处。如下图就是创建不出的例子:
所以, 这里就需要将export也当作内建命令特殊处理——使用putenv在当前进程导入环境变量。 但是由于putenv导入环境变量只是修改环境变量的指针指向传给的参数指向的空间。也就是相当于一个浅拷贝。 如果我们直接给putenv传argv[1], 那么环境变量的指针指向putenv指向字符串, 当这个字符串被覆盖时环境变量就变了!!所以我们要先malloc一块新空间。 再将数据拷贝到这个空间。 让环境变量指向这块创建的malloc空间。
当前进程导入环境变量。 那么就能使用我们自己的shell导入环境变量了。
echo
对于echo也要做一下特殊处理。 因为一般情况下echo会打印正常, 但是对于环境变量来说, 它就会直接打印环境变量
处理方式就是做一个特殊判断, 如果argv[1]的第一个字符时$那么就按照环境变量打印:
如下图红框框处是做一下特殊判断, 防止发生段错误。
这样就能把环境变量打出来:
但是还不行, 因为echo可能打印$, 也就是打印最后一次退出码。
那么就要再进行一次特殊处理:
lastcode里面保存了退出码, 当执行了一次echo $?后要把lastcode置为0
当我们当进行登录的时候, 我们的系统就是要启动一个shell进程。 我们shell本身的环境变量表是从哪里来的??——是在当前用户的家目录下, 有一个叫做bash_profile 或者 bashrc的文件。 这里面就有各种各样的文件。
当用户登录的时候, shell会读取用户目录下的.bash文件, 里面保存了导入环境变量的方式!!!
如果我们想要自己导入这种默认的环境变量, 那么我们就要和标准的shell一样, 创建一个环境变量表。 然后自己创建一个环境变量的配置文件。将这些环境变量读入环境变量表当中!!!
版权归原作者 打鱼又晒网 所有, 如有侵权,请联系我们删除。