1 实验内容
设计一个C语言编写的外壳程序,可以接受用户命令并在一个单独进程中执行用户命令。另外,该外壳程序支持history命令,允许用户访问最近输入的命令,支持!!/!<number>的检索命令并进行基本的错误处理。
外壳接口可以接受用户命令,然后可以在一个单独进程中执行用户命令。它为用户提供提示符,以便输入下一个命令。
接受用户命令并执行:父进程首先读取用户命令行的输入,然后创建一个单独的子进程来完成这个命令。当命令以“&”结束时,命令执行的过程中,父子进程并发执行。否则,父进程在继续之前等待子进程退出。外壳程序不断读取与执行命令,直至满足退出条件时,退出循环,程序结束。
提供历史(history)特征:在用户键入命令后,记录用户键入的文本。通过维护一定大小的命令存储空间,实现历史命令的存储与更新。当用户键入“history”后,外壳程序输出一定数量的用户最近输入的命令。当用户键入“!!” “!<number>”(<number>为一个整数)后,外壳程序在存储的历史命令查询指定指令。若查询成功,外壳程序执行对应命令。若查询失败,外壳程序进行错误处理并输出错误描述。
2 代码分析
其中,MAX_LINE为单次键入命令文本的长度的最大值。MAX_HISTORY为可记录命令数的最大值。
外壳程序执行后进入循环,使用fgets()读取命令文本并清空输出缓冲区。fgets()读取字符串,会读入最后键入的回车。由于回车对后续操作有影响,所以读入命令文本后对字符串尾部的“\n”进行处理,并重新计算字符串长。
当用户键入检索命令时,从命令历史中取出指定命令,复制给args。如果命令历史为空或检索的命令不存在,则输出错误信息。
由于命令历史是使用二维数组存储的,所以,在选择用户所指定的命令时,程序使用了相对复杂的模运算来构建用户键入整数与命令历史数组索引之间的映射。这样的优势是存储命令文本的操作和更新命令历史的操作简单方便。
对exit命令和history命令单独处理。用户键入命令为“exit”时,程序退出循环,结束运行。用户键入命令为“history”时,输出历史命令,进入下一次循环。
程序对命令尾部是否有“&”进行判断:如果命令末尾有“&”,则flag置1并删去“&”。flag为1,说明父子进程并发执行。
对命令文本的分割使用strtok()函数。strtok()函数以待处理的字符串和分隔符字符串为参数,找到待处理字符串中第一个分隔符,将其改为“\0”并返回分割得到的字符串(分隔符前的字符串)的首地址。同时,strtok()函数还会记录此处“\0”的地址。下一次调用时,第一个参数置NULL,第二个参数为分隔符。strtok()函数将自动从上一次记录的地址开始执行。
对命令文本分割后,将各命令段字符串的首地址存入array。
以strtok(args, “ “)分割命令“ls -a”为例:
字符串的第一个空格被改为“\0”,函数返回分割掉的字符串的首地址。
分割后,创建子进程,调用execvp()函数执行命令。如果flag为0,则父进程等待子进程。否则父子进程并发执行。在父进程中,将args的副本sub_args填入历史命令中。
execvp()函数的第一个参数直接传入可执行程序的文件名,第二个参数是一个指针数组,数组当中存放可执行程序的参数。要注意的时,指针数组一定要以NULL结尾。函数如果执行失败则返回 -1。
3 问题
关于history命令是否要存入命令历史
将刚刚执行过的history加入命令历史中,会导致紧跟history后的!!命令发生错误:所执行的结果与用户看到的history输出的命令后的预期不一致。(如下图例)
如图,运行shell后,首先键入“ls”命令,正常执行。接着,键入“history”,显示命令历史,正常执行。然后,键入“!!”。按照history输出的命令历史推测,应该被执行的命令应该是“ls”,但实际被执行的命令是“history”。在“!!”后再次查看history,显示最近执行的命令是“history”而不是“ls”。
实验中,对两种方式(将history加入命令历史与不将history加入命令历史)都进行了实现。实际上,两者实现的代码差异很小。出于“检索命令应与history输出结果相匹配”的考虑,程序没有将history加入命令历史。这样也不易给用户带来困惑与不便。
4 完整代码
(程序可能有疏漏或错误,谨慎复制。请一定先了解程序设计思路。)
#include<unistd.h>
#include<stdio.h>
#include<string.h>
#include<stdlib.h>
#include<sys/types.h>
#define MAX_LINE 80
#define MAX_HISTORY 10
void display(int p_cmd_num, char history[MAX_HISTORY][MAX_LINE / 2 + 1]);
int main(void){
char history_cmd[MAX_HISTORY][MAX_LINE / 2 + 1]; //存储历史命令
int cmd_num = 0; //命令数
while(1){
char args[MAX_LINE / 2 + 1]; //存储命令文本
char sub_args[MAX_LINE / 2 + 1]; //处理后命令文本的副本
char* array[sizeof(args)]; //execvp()的参
memset(array, 0, sizeof(array)); //array 初始化
char* p = NULL; //用以分割命令文本
int i = 0;
int flag = 0; //有无'&'的标记
pid_t pid;
printf("osh>");
fgets(args, sizeof(args), stdin);
//清空输出缓冲区
fflush(stdout);
//对读入的'\n'处理
int len = strlen(args);
args[len - 1] = '\0';
len = strlen(args);
//只键入回车
if(args[0] == '\n'){
continue;
}
//键入"!!"或"!<number>"
if(args[0] == '!'){
//从历史命令中取出 指定命令 存入args
if(args[1] == '!'){
if(cmd_num == 0){ //历史指令为空
printf("No command in history.\n");
continue;
}
else{
printf("%s\n", history_cmd[(cmd_num + 9) % MAX_HISTORY]);
memcpy(args, history_cmd[(cmd_num + 9) % MAX_HISTORY], sizeof(args));
}
}
if(args[1] <= '9' && args[1] >= '0'){
int keyin_num = args[1] - '0';
if(keyin_num > MAX_HISTORY || keyin_num >= cmd_num){ //指令不存在
printf("No such command in history.\n");
continue;
}
else{
int index;
if(cmd_num < MAX_HISTORY){
index = keyin_num;
}
else{
index = ((cmd_num % MAX_HISTORY) + keyin_num) % MAX_HISTORY;
}
printf("%s\n", history_cmd[index]);
memcpy(args, history_cmd[index], sizeof(args));
}
}
}
//键入exit命令
if(strcmp(args, "exit") == 0){
break;
}
//键入history命令
if(strcmp(args, "history") == 0){
display(cmd_num, history_cmd); //输出最近输入的MAX_HISTORY条命令
continue;
}
//复制args; sub_args在执行时被存入历史命令
memcpy(sub_args, args, sizeof(args));
//如果有参数'&', flag置 1, 并将'&'改为'\0'
if(args[len - 1] == '&'){
flag = 1;
args[len - 1] = '\0';
}
//对命令文本进行分割, 以空格为分隔符, 分割结果为array
p = strtok(args, " ");
while(p != NULL){
if(*p != ' '){
array[i++] = p;
}
else{
array[i++] = NULL;
}
p = strtok(NULL, " ");
}
//执行命令
pid = fork();
if(pid < 0){
perror("fork");
}
else if(pid == 0){
int cmd_flag = 0;
cmd_flag = execvp(array[0], array);
//没有该命令
if(cmd_flag == -1){
printf("%s: ", args);
printf("Command not found\n");
}
exit(1);
}
else if(!flag){ //无'&', 父进程等待子进程结束
int status = 0;
pid_t ret = waitpid(pid, &status, 0);
if(ret < 0){
perror("waitpid:");
}
memcpy(history_cmd[cmd_num % MAX_HISTORY], sub_args, sizeof(sub_args));
cmd_num += 1;
}
else{ //有'&', 父子进程并发执行
memcpy(history_cmd[cmd_num % MAX_HISTORY], sub_args, sizeof(sub_args));
cmd_num += 1;
}
//重置
memset(array, 0, sizeof(array));
}
return 0;
}
void display(int p_cmd_num, char history[MAX_HISTORY][MAX_LINE / 2 + 1]){
int i;
int limit;
int first_index;
if(p_cmd_num < MAX_HISTORY){ //命令数 < MAX_HISTORY, 输出全部历史命令
limit = p_cmd_num;
first_index = 0;
}
else{ //命令数 > MAX_HISTORY, 输出最近输入的MAX_HISTORY条命令
limit = MAX_HISTORY;
first_index = p_cmd_num % MAX_HISTORY;
}
for(i = 0;i < limit;i++){
int index = (first_index + i) % MAX_HISTORY;
printf("%d %s\n", i, history[index]);
}
}
5 参考
代码参考:
https://blog.csdn.net/m0_46785351/article/details/116921820
https://github.com/wangzitiansky/OperatingSystemConcepts/blob/master/practice/myshell.c
strtok函数讲解: