shell
shell是壳,外壳的意思,一般我们使用linux系统有用图形化界面的也有使用命令行界面的,这两个都是一种shell,以命令行为例:
如图这个就是我这里的命令行格式,在$符后面写的就是执行的指令,ls指令在linux中也是一个可执行程序,这个程序作用就是显示出当前路径下的文件。可是能执行命令程序就代表我们找到这个文件并对其进程了执行的操作,而我们只是在$符后面写下了ls而已-l是一个选项是传递给ls这个程序的一个参数。能完成找到ls程序并执行它并将参数传递给这个程序的工作就是shell做的事。所以shell也是为了完成某一目的而被编写出来的一个程序,这个程序会在我们打开软件的时候自动执行。我这里用的是XSHELL5也是这个软件的功能之一。
在shell环境下编写shell,我们为什么能在shell环境下编写使用shell呢,这就是shell进程设计的工作模式,当我们使用命令行执行一个程序的时候shell程序会被挂起执行启动的程序,直到我们启动的程序结束shell程序才会接管执行,这个就是父子进程的关系,shell进程作为父进程,命令行输入的程序作为子进程,当执行子进程时父进程对子进程进行等待,子进程结束后父进程在等待后继续执行,这就是shell的工作模式。
shell命令行
我们能看到当进入程序的时候一开始就会看到这样一个命令行提示字符串,里面有我现在用户名,有主机名,有当前路径名字。
这就是shell的第一个模块,进入shell我们需要先有一个提示的字符串
这个是打印命令行提示的代码,先定义一个字符数组用于存放完整的提示,定义了三个字符串指针用于接收对应的数据
这里对应着三个所调用的函数分别是获取用户名,获取主机名,获取路径。三个函数用的是同一个库函数getenv作用是根据传递的参数在环境变量中找寻对应的值。返回字符串首位的地址,然后将这个字符串返回。
由于这里通过环境变量返回的路径是绝对路径所以这里我做了一点小修改
只截取了最后的一个路径,不然整个命令行提示会随着层数加深越来越长。
然后我们只要按照一定的格式将其写入到一开始定义的字符数组中然后打印到屏幕上因为我们后续是直接在命令行提示后面直接写入指令的所以这里不需要要换行,但是系统缓冲区的刷新是按照行刷新的所以这里需要手动刷新标准输出的缓冲区。
获取指令
指令我们都是从命令行中获取,而命令行中其实就是打印了标准输入中的数据,所以实际上是从标准输入中获取的。
因为每次输入都是新的指令,旧的指令就放弃掉,有很多外壳程序会有存储上几条输入的指令所以我们是可以malloc一个缓冲区来存放需要存储的指令的,但是我们这里就简单实现就直接对之前的指令不做处理默认丢弃
我这里在main函数中定义了一个字符数组存放指令这个数组过了生命周期就自动释放每次获取的时候就从新创建,然后将这个数组作为输出型参数传递给获取指令的函数。
这个数组的大小是需要传递的,因为数组传参是传的指针,数组的大小只能在定义的作用域内计算。然后使用fgets函数从标准输入中获取指令到缓冲区中。
标准输入中是不会包含字符串结束符的,"\0"作为字符串的结束标志是C语言中规定的,这里需要自己添加进去,这里返回获取到的字符串的长度。这里不用scanf的原因是scanf遇到空格会当作输入结束就无法获取一整行的指令。
问我这里判断输入用<=0表示程序出错了直接停止程序,因为无论在命令行中输入什么都不可能出现<=0的情况就算只输入了空格回车那也有一个空格符或者回车。
指令分割
指令接收是一整行的,但是里面包含了程序名或路径加程序名还有一些选项,就需要将选项和程序分割开来。
这里将参数分割开来后需要存储,这里定义一个全局的字符指针数组存放。
这里使用strtok函数分割,这个函数有一些些奇怪,第一次分割需要把其实地址和分割的符号传进去这里SEP是一个宏是一个空格,这个分隔符是一个字符串不是字符。然后继续分割就不需要地址了,给他一个NULL会自动在之前的位置继续分割,他会返回分割后的首字符指针,第一个分割的还在原来的缓冲区中,后续分割的会存放在静态区中,这是strtok的性质,所以分割后最好不要继续使用main函数中的缓冲区的用户指令了,我们会将分割后指令和选项的字符串首地址放在全局的字符指针数组中,后续就用这个数组就可以了。
指令执行
分割完指令我能就从全局的字符指针数组中获取单个的指令和选项了,那就可以执行这个指令了,这个指令也是一个程序,我们也没必要将所有程序功能都集中在shell程序中,而且都有现成的还去实现这么的程序功能这样耦合度过高不好,效率也不行,所以我们需要在创建一个执行指令的进程。这里我们使用子进程加程序替换的方式来实现。
子进程的创建替换,父进程的等待都打包给了函数,就不需要主进程这么麻烦了,直接调用就可以了。进程创建替换详细在进程篇中,就说一下execvp这个函数,exec函数家族有好几个函数,主要是参数不同能适应各种场景,这里使用vp是可以直接匹配系统的环境变量就不需要我们自己自定义环境变量了传参了,v是数组的意思我们能以数组的形式传递选项的参数,我们定义的参数列表就是一个字符指针数组所以用这个函数最方便了,第一个参数传入我们需要执行的程序名或路径加程序名,然后将整个参数列表都传入给第二个参数就完成进程替换,若失败返回-1,设置错误码,我们子进程直接退出返回错误码给父进程,若是成功就变成了第一个参数的程序了就不用管了。
父进程就只需要等待子进程完成就可以了,这里我们只对进程替换失败做一些工作,获取错误码并打印。
内建指令
完成上述步骤最简单的外壳程序就已经完成了,能获得用户输入的信息并创建子进程执行对应的程序。只是还有一些小瑕疵,有一些指令例如cd指令要变更当前路径,但这里是创建子进程执行这个程序所以父进程的路径并不会更改,而是子进程的路径被更改了,子进程路径被修改是没有意义的,我的都是从shell程序作为基点来使用linux系统的,所以这条指令需要由父进程自己执行,类似这种的指令叫做内建指令,不能由子进程执行。
在调用子执行函数前先检查是否为内建命令若是就单独执行这个命令,不然子进程执行。
cd指令比较复杂因为不止要执行还需要对环境变量进行更改,还要对命令行提示进行修改所以单独写了一个函数执行,这里先拿到指令的第二个参数即数组下标为一的参数,若这个参数为NULL就说明用户只写了cd,这时我们调用一个函数获取环境变量中的home路径
这个正常的系统都是有的,没有的那些要不自己更改了,要不系统就有些错误了。不过我们还是将没有的也考虑了一下返回一个根目录。
然后使用系统调用函数更改当前路径,这里参数是用的绝对路径。
更改完路径需要对环境变量中的路径进行修改,先定义一个存放新路径的字符数组,使用getcwd这是个c库函数,获取当前的最新路径,然后以标准格式存放,这里是用了一个全局的cwd字符数组存放
这时系统env里面的标准格式,getcwd是只获得路径,前面的PWD=需要手动添加上去,然后使用putenv函数更新当前工作路径完成cd全部步骤。