5.1(30分)**使用任何一种程序设计语言实现一个shell** **程序的基本功能。**
shell 或者命令行解释器是操作系统中最基本的用户接口。写一个简单的shell 程序——**myshell**,它具有以下属性:
(一) 这个shell 程序必须支持以下内部命令:bg、cd 、clr、dir、echo 、exec 、exit 、environ、fg 、help、jobs 、pwd 、quit、set 、shift 、test 、time 、umask、unset。部分命令解释如下:
1) cd <directory> ——把当前默认目录改变为<directory>。如果没有<directory>参数,则显示当前目录。如该目录不存在,会出现合适的错误信息。这个命令也可以改变PWD 环境变量。
2) pwd ——显示当前目录。
3) time ——显示当前时间
4) clr ——清屏。
5) dir <directory> ——列出目录<directory>的内容。
6) environ ——列出所有的环境变量。
7) echo <comment> ——在屏幕上显示<comment>并换行(多个空格和制表符可能被缩减为一个空格)。
8) help ——显示用户手册,并且使用more 命令过滤。
9) quit ——退出shell。
10) shell 的环境变量应该包含shell=<pathname>/myshell,其中<pathname>/myshell 是可执行程序shell 的完整路径(不是你的目录下的路径,而是它执行程序的路径)。
(二) 其他的命令行输入被解释为程序调用,shell 创建并执行这个程序,并作为自己的子进程。程序的执行的环境变量包含一下条目:
parent=<pathname>/myshell。
(三) shell 必须能够从文件中提取命令行输入,例如shell 使用以下命令行被调用:
myshell batchfile
这个批处理文件应该包含一组命令集,当到达文件结尾时shell 退出。很明显,如果shell 被调用时没有使用参数,它会在屏幕上显示提示符请求用户输入。
(四) shell 必须支持I/O 重定向,stdin 和stdout,或者其中之一,例如命令行为:
programname arg1 arg2 < inputfile > outputfile
使用arg1 和arg2 执行程序programname,输入文件流被替换为inputfile,输出文件流被替换为outputfile。
stdout 重定向应该支持以下内部命令:dir、environ、echo、help。
使用输出重定向时,如果重定向字符是>,则创建输出文件,如果存在则覆盖之;如果重定向字符为>>,也会创建输出文件,如果存在则添加到文件尾。
(五) shell 必须支持后台程序执行。如果在命令行后添加&字符,在加载完程序后需要立刻返回命令行提示符。
(六) 必须支持管道(“|”)操作。
(七) 命令行提示符必须包含当前路径。
**提示:**
1) 你可以假定所有命令行参数(包括重定向字符<、>、>>和后台执行字符&)和其他命令行参数用空白空间分开,空白空间可以为一个或多个空格或制表符(见上面(四) 中的命令行)。
**项目要求**:
1) 设计一个简单的全新命令行shell,至少满足上面的要求并且在指定的Linux 平台上执行。**拒绝使用已有的shell程序的任何环境及功能**。**严禁使用开源代码。**
2) 写一个关于如何使用shell 的简单的**用户手册**,用户手册应该包含足够的细节以方便Linux初学者使用。例如:你应该解释I/O 重定向、管道、程序环境和后台程序执行。
3) **源代码必须有很详细的注释,**并且有很好的组织结构以方便别人阅读和维护;否则会扣除客观的分数。结构和注释好的程序更加易于理解,并且可以保证批改你作业的人不用很费劲地去读你的代码。
### myshell设计文档
**一、设计思想:**
在准备myshell的设计之前,我首先详细研究了bash的命令结构,观察分析了bash相关内建命令的执行结果。首要任务是选择一门编程语言,我在查阅了资料发现,对于进程控制,目录跳转等操作,有很多Linux下的C语言系统调用函数可以直接进行处理,方便我们对myshell的设计,再加上C语言是我最早接触的编程语言,于是我选择了用C语言进行本次myshell的设计。
在确定编程语言之后,仿照bash对于命令的处理过程,我将myshell的主程序设计为一个无限循环,循环的第一步是打印命令提示符,第二步是读取用户的输入,第三步是对用户的输入进行拆分保存,最后一步是执行相应的命令。这个思路流程还是比较清晰的。
在主程序的框架设计完成之后,着重需要完成的就是执行命令的模块,对于这个模块的实现也分步骤进行。首先对需要实现的内建命令进行一一实现并测试,这当中借助许多系统调用函数可以简化命令的实现,例如使用getcwd()函数可以直接获取当前目录,使用chdir()函数可以进行目录间的切换,这两个函数正好可以用在cd命令的实现中。
在内建命令实现并调试完成后,开始对外部命令的执行进行实现。我的思路是先检查用户输入是否是内建命令,如果不是则默认为外部命令。对于外部命令的执行,利用fork()创建一个新的子进程,在子进程中利用execvp()函数查找并执行相应的外部命令,在主进程中使用waitpid()函数阻塞主进程,等待子进程的退出,即可实现外部命令的执行。
接着考虑myshell对于读取脚本文件并执行其中命令的支持,我的想法也很简单,执行myshell时如果不带参数,则打印命令提示符,且读取输入从标准输入流中读取;而执行myshell时如果带上了一个参数,这个参数为脚本文件的路径,则不打印命令提示符,先尝试打开该脚本文件,如果成功打开,则读取输入从脚本文件中读取,后面的输入拆分和命令执行过程都不变。
然后考虑myshell对于后台命令的实现,这个实现也比较容易,只需要定义一个标志是否为后台命令的变量,如果命令以‘&’结尾,就把这个标志置1。对于需要后台执行的命令,在主进程中仍然使用waitpid()函数,但使用其中的非阻塞选项,这样就可以不阻塞主进程的进行,实现命令的后台执行了。
最后需要重点考虑的是重定向和管道操作的实现,在查阅资料后发现,重定向和管道操作的核心实现原理其实十分类似,管道操作其实也是一种重定向,是将标准输入输出重定向到管道文件的两端,而重定向操作是将标准输入输出重定向到指定的文件,这个操作都可以通过dup2()函数来实现。
对于管道操作的实现,在命令读取的过程中检查到‘|’时,将预先定义的表示是否为管道操作的标志置1。在标志为1的情况下,首先调用pipe()函数创建无名管道,管道两端的文件描述符分别保存在pipeFd[0]和pipeFd[1]中,管道的作用可以类比为一个共享文件,一个进程将信息写到管道内,另一个进程再从管道内读取信息,就完成了两个进程之间的通信。之后利用fork()函数先创建一个子进程pid1,在pid1中,将标准输出重定向到管道的读入端,并执行命令,命令的输出将输进管道文件。在主进程中,首先用waitpid()函数阻塞主进程,等待子进程pid1返回,待pid1返回后,再次利用fork()函数创建一个子进程pid2,在pid2中,将标准输入重定向到管道的输出端,并执行命令,命令的输入将从管道文件中读取。在主进程中,用waitpid()函数阻塞主进程,等待子进程pid2返回,待pid2也返回后,利用close()函数关闭管道两端即可。
对于重定向的实现,在命令读取的过程中检查到‘<’,‘>’或者‘>>’时,将预先�