利用之前的知识,这个实验中我们要自己实现一个简单的 shell ——跟我们之前使用到的 bash shell 类似,它的功能是 显示当前所在的目录,然后对输入的命令进行解释。在目录下有一个文件main_sol
,它是一个已经编译好的标程,会用来生成测试用的文件。你可以在目录下执行make
后直接运行这个程序,查看它的效果——你的 shell 必须跟它的显示效果保持一致。
在实现shell
之前,你需要实现一个数据结构log_t
——结构体的布局可以参见log.h
头文件,它相当于一个用链表实现的栈,作用是保存输入的每一条命令以便查找。
在log.h
中定义了这样几个函数,你需要全部实现它们:
-
void log_init(log_t *l);
初始化一个log_t
——熟悉面向对象程序设计的同学应该能看出,这相当于是log_t
的构造函数。你需要将l
的head
设置为NULL
。 -
void log_destroy(log_t* l);
销毁一个log_t
,相当于log_t
的析构函数——你需要回收一个log_t
所占用的所有内存,并将l
的head
设置为NULL
,如果head
已经为NULL
则直接返回。 -
void log_push(log_t* l, const char *item);
把一个字符串item
插入到一个log_t
的末尾。如果l
的head
为NULL
的话,你需要在这个函数中为log_t
建立第一个结点并分配对应的内存空间。 -
char *log_search(log_t* l, const char *prefix);
遍历整个log_t
的链表,搜索是否有包含前缀prefix
存在的字符串,如果有的话则返回这个字符串(只返回第一个),没有的话直接返回NULL
。
有 C++ 或者其他面向对象语言基础的同学可以看出,这一部分我们其实是在用 C 语言来进行面向对象编程——对于系统编程来说,很多情况下确实需要这种看起来有些“蹩脚”的编程方式,因此这里需要大家对此熟练掌握。
接下来,我们需要来实现 shell 本身——一个 shell 相当于一个无限循环的程序,它在循环中应该执行这些操作:
- 向标准输出打印一个命令提示符
- 从标准输入读取一个命令
- 判断要执行哪些命令
在这一步,你需要修改 shell.c 里的prefix
函数和execute
函数。接下来我们将依次介绍进一步的细节:
命令提示符
你需要在prefix
函数里输出以下格式的命令提示符:
/path/to/cwd$
这里可以直接用printf("%s$ ", cwd)
来实现——cwd
部分为当前所在目录。你可以自行查阅文档,阅读关于getcwd()
函数的相关信息。
从标准输入读取命令
你的程序要每次从标准输入读取一行,获取输入的命令——你可以查阅文档学习getline()
函数的用法,这里要 注意回收内存,不然你将无法获得内存泄露检测的分数。这部分功能已经在 main.c 文件中实现了,你可以阅读这部分代码,但无法对其进行修改,最终测试时会将你所做的任何修改覆盖掉。读入后,会将命令作为参数传递给execute
函数。
对内置命令进行解析
你需要在execute
函数里对内置命令进行解析,实现如下几个命令:
1)cd
cd xxx
进入某个目录(上述代码会进入xxx
目录)——你可以查阅chdir()
函数的用法。如果目录不存在,则要打印xxx: No such file or directory
并换行,其中xxx
表示输入的目录名称。
2)exit
exit
退出 shell 程序,在execute
函数中直接返回 000。在其他情况下,execute
返回一个非零值即可。
3)显示历史命令
!#
所有输入的命令都保存在一个log_t
中,这个命令的作用是显示所有输入过的命令,每个占一行。从栈底元素开始输出,log_t
变量名为Log
,定义在 shell.h 文件中,请勿修改。
注意,以!
开头的所有命令都不会被放入命令栈中。
4)根据前缀查找命令
!prefix
查找是否曾经输入过包含前缀prefix
的命令,如果找到(如果有多个,只找最靠近栈底的一个),则执行这条命令,如果没有,则返回No Match
换行。新执行的命令也会被放入栈顶。
5)ls
ls
跟 bash shell 的ls
命令一样,列举当年目录下所有子目录和文件——你可以直接用system()
函数执行这个命令。
执行外部命令
你需要使用fork()
和exec()
等函数来执行一个外部命令——如果执行失败,则输出%s: no such command
换行。注意,这里输入的命令采用的是 DOS 格式,而不是 Unix 格式——运行当前目录下可执行文件args
应该直接输入args
而非./args
,因此你 不能使用system()
函数来执行该命令,否则将通不过判题测试。
文件夹下有一个
args.cpp
文件,你可以调用g++
(注意不是gcc
,因为是 C++ 代码)编译它,然后试着用你的 shell 来执行这个程序。这个程序会输出一行Hello world!
然后依次输出它接收的每一个参数。
如果外部命令无法执行,则输出XXX: no such command
并换行,其中XXX
表示输入的完整外部命令。注意,你必须确保无论执行成功还是失败,都 不要有额外的子进程留下。
即使外部命令未正确执行,也依然将这条命令放入命令栈中。
在/include
文件夹内包含了 头文件(header file) ,头文件声明了函数和类;
在/src
文件夹内含有具体的 .c,其中包含了对相关函数的实现——你需要实现的函数在shell.c
和log.c
中。
主函数保存在main.c
中。不要擅自修改主函数代码! 否则可能会导致你的代码不能通过判题。
在自测时你可以在终端中输入make
来编译执行本地判分器,输入make clean
可以清空之前编译出的文件。其它的make
指令可以在Makefile
中查看。
编译出的可执行文件名为main
,在运行make
之后,你可以直接执行./main
来直接运行你的 shell。
正如前面提到的,这次作业由在线判分器进行自动判分。
在这次任务中,你只能修改 shell.c 和 log.c 文件,对其他文件进行的修改将会被覆盖。你不能通过standard output
输出任何额外的内容(这意味着你 不可以 使用printf
或类似的函数输出多余的信息),因为这会导致在线判分器判分失误从而影响你的成绩。在你提交作业后,自动判分器将针对你提交的内容进行正确性测试与内存测试——本地判题将只进行正确性测试,一共占 45%45\%45% 的成绩。
在你已经通过正确性测试的前提条件下,你的程序接下来将会接受内存测试——如果没有内存泄露的话你将获得额外 20%20\%20% 成绩。至此,你可以获得基础的 65%65\%65% 的成绩。
在文件夹下有一个空的fbomb.txt
,输入任意内容让该文件不为空,就可以进行fork bomb
测试——通过测试后,你将获得剩余的 35%35\%35% 的成绩。
注意,你必须确保你的fork
实现是正确的——如果产生了额外的进程,无法终止的话判题器将不能正常结束,被系统杀死,然后你将 无法得到任何分数。
注 1:你可以反复提交评分,你的最终分数将以最高一次提交得分为准。
注 2:本项目作业需要你独立完成,使用他人提供的代码进行提交(无论是否为最高分提交)都将被视为违背学术纯洁性的行为。