实现一个简单的shell
使用已学习的各种C函数实现一个简单的交互式Shell,要求:
1、给出提示符,让用户输入一行命令,识别程序名和参数并调用适当的exec函数执行程序,待执行完成后再次给出提示符。
2、该程序可识别和处理以下符号:
1) 简单的标准输入输出重定向:仿照例 "父子进程ls | wc -l",先dup2然后exec。
2) 管道(|):Shell进程先调用pipe创建管道,然后fork出两个子进程。一个子进程关闭读端,调用dup2将写端赋给标准输出,另一个子进程关闭写端,调用dup2把读端赋给标准输入,两个子进程分别调用exec执行程序,而Shell进程把管道的两端都关闭,调用wait等待两个子进程终止。
实现步骤:
1. 接收用户输入命令字符串,拆分命令及参数存储。(自行设计数据存储结构)
2. 实现普通命令加载功能
3. 实现输入、输出重定向的功能
4. 实现管道
5. 支持多重管道
以上。
出于简单,我假设我们输入的命令字符串是符合要求,没有错误的。
我们要实现的有:普通命令;输入输出重定向;单个管道。有四种情况:①ls -ahl 单个命令;②ls -alh > a.txt 输出重定向;③ls -ahl | grep root 管道;④cat < a.txt输出重定向。其实更具体细分还有命令带参数和不带参数的情况。情况有这几种,我们应该用标志将他们区分,所以,储存命令的数据结构就很重要了。这是我设计的结构体:
typedef
struct
My_order
{
char *argv[32]; //命令以及参数、文件
int pipe;
int right;
int left;
} My_order;
我将其命名为My_order。现在我们要做的事是解析用户输入的命令字符串:ls -ahl | grep root 。理想情况下,我们应该将其拆分为 ls 、-ahl、|、grep、root这些字符串。该怎么拆分呢?观察命令字符串:命令参数之间用空格隔开的,我们可以利用这个特性。但是我们要自己造轮子么?不用,C库函数为我们提供了一个字符串分割函数strtok():
原型:char *strtok(char *restrict s1,const char * restrict s2);
描述:该函数把s1字符串分解为单独的记号。s2字符串包含了作为记号分隔符的字符。按顺序调用该函数。第一次调用时,s1应指向待分解的字符串。函数定位到非分隔符后的第一个记号分隔符,并用空字符替换它。函数返回一个指针,指向存储第一个记号的字符串。若未找到,返回NULL。再次调用strtok查找字符串中的更多记号。每次调用都返回指向下一个记号的指针。未找到返回NULL。
于是,我们像下面这样调用该函数就可以完美的解决问题了。
int resolve_order(My_order *my_order, char p[])
{
//先初始化
my_order->pipe = my_order->left = my_order->right = 0;
for (int i = 0; i != 32; i++)
{
my_order->argv[i] = NULL;
}
int i = 0;
int option = 0;
my_order->argv[i] = strtok(p, " ");
while (my_order->argv[++i] = strtok(NULL, " "))
{
if (strcmp(my_order->argv[i], " | ") == 0)
{
my_order->pipe++;
}
else
if (strcmp(my_order->argv[i], ">") == 0)
{
my_order->right++;
}
else
if (strcmp(my_order->argv[i], "<") == 0)
{
my_order->left++;
}
}
return 0;
}
当命令字符串中有管道,输入输出重定向符的时候,相应的值就要增加。但是最多也只能是1,再多的话,我这个简单的shell就不能胜任了。即像这样的命令:cat|cat|cat我是解决不了的。
我们上面的示例命令有管道,所以我们要用到pipe函数,建立管道,使进程之间能够相互通讯。但是我们第一步是要创建进程,不多,一个就够了,使用fork()函数。但是在此之前我们还有问题要解决:是子进程解决管道前面的命令呢还是父进程先解决?子进程和父进程谁先执行?这里废话一点:以前有个牛人(抱歉不记得是谁了,若是知道请告知)做了个实验:观察父子进程谁先被执行,最后得出的结论是绝大部分情况下是父进程先抢到CPU资源。但是这并没有理论支撑。计算机科学没有理论来支持这个结论。(当故事听就好哈,不要较真,本人还是萌新。)虽然有大牛得出这样的结论来了,但是我还是没有遵循这个结论。^_^。所以我让子进程去执行管道前面的命令了, 哎。还好我写了这个博客,不然我会闹大笑话。必须要两个子进程,一个不行,除非我就执行这一个管道命令。exec族函数的一大特点是什么?执行完成指定程序之后根本就不回来!意味着这个进程死掉了,无论是父进程还是子进程都会被回收掉。所以还是要两个子进程,这里要注意的是,使用兄弟进程进行通讯的时候父进程应该使用waitpid函数进行非阻塞回收。但是在我的实现上依旧有那种阻塞情况发生,是在是不懂怎么回事。不过这不重要。(玛德,废话真多。)代码:
void my_pipe(My_order *my_order)
{
int fd[2];
int p_ret = pipe(fd);//fd[0]->r;fd[1]->w
if (-1 == p_ret)
{
perror("pipe error ");
exit(1);
}
int i = 0;
int pid;
for (; i != 2; i++)
{
if (!(pid = fork()))
{
break;
}
}
if (0 == i)
{
if (strlen(my_order->argv[1]) > 1)
{
close(fd[1]);
dup2(fd[0], STDIN_FILENO);
execlp(my_order->argv[3], my_order->argv[3], my_order->argv[4], NULL);
}
else
{
close(fd[1]);
dup2(fd[0], STDIN_FILENO);
execlp(my_order->argv[2], my_order->argv[2], my_order->argv[3], NULL);
}
}
else
if (1 == i)
{
if (strlen(my_order->argv[1]) > 1)//有参数
{
close(fd[0]);
dup2(fd[1], STDOUT_FILENO);
execlp(my_order->argv[0], my_order->argv[0], my_order->argv[1], NULL);
}
else
{
close(fd[0]);
dup2(fd[1], STDOUT_FILENO);
execlp(my_order->argv[0], my_order->argv[0], NULL);
}
}
else
{
waitpid(-1, NULL, WNOHANG);
waitpid(-1, NULL, WNOHANG);
}
return 0;
}
我写的不够严谨,都没有什么错误检查。别像我这样写,要检查错误,检查函数返回值。
比如就是万一有用户这样写 ls -alh | a.txt 虽然这样在真正的shell也不能通过,但是别人有错误提示啊。
其实有管道这个是整个程序中最难的部分。接下来的重定向其实很简单的。进过我的测试(用我那点可怜的知识)发现,重定向无非三种正确(的简单的)情况:命令>命令;命令>文件;命令<文件。前面的部分全是命令,后面的就稍微有点不同。那么问题来了:如何判断后面的是文件还是命令?以有无后缀区分?但是在Linux中后缀是方便我们识别的而不是系统的刚需啊。我也经常看到gcc main.c -o a这样的命令啊。(别喷别喷)没事,大部分的Linux命令都在/bin目录下呢。简单的实现也无需考虑那么多,现在就是我们需要去查看目录中有无对应字符串内容的命令。读目录也很简单啊,我的博客前几篇(忘了哪一篇了)介绍了读取指定目录获取文件数目内容。我们稍微变换一下就可以用来区分文件or命令了:
int get_dirfile(char *name) //命令存在返回0;不存在返回-1;
{
DIR *dir = opendir(" / bin");
struct
dirent *di;
while ((di = readdir(dir)) != NULL)
{
if (strcmp(di->d_name, name) == 0)
{
return 0;
break;
}
}
return -1;
}
//是命令就执行。是文件就打开(创建)。打开文件也很简单嘛:
int open_file(char
p[])
{
int o_ret = open(p, O_RDWR | O_CREAT | O_TRUNC, 0644);
if (o_ret == -1)
{
perror("open file error ");
exit(1);
}
return o_ret;
}
相关的函数、宏若不知道意思,请参阅前几篇(也忘了是哪一篇了)的介绍。
接下来,就要解决重定向了。dup2函数一定是需要的(我也在前几篇介绍了的),这里就不介绍了。接下来就很简单了,就是每个命令就要确定一下参数有无。
int exec_order(My_order *my_order)
{
if ((my_order->pipe == 0) && (my_order->left == 0) && (my_order->right == 0))
{
if (!fork())
{
if (my_order->argv[1] != NULL)
execlp(my_order->argv[0], my_order->argv[0], my_order->argv[1], NULL);
else
execlp(my_order->argv[0], my_order->argv[0], NULL);
}
else
{
wait(NULL);
}
}
else
if ((my_order->pipe == 1) && (my_order->left == 0) && (my_order->right == 0))
{
my_pipe(my_order);
}
else
if ((my_order->pipe == 0) && (my_order->left == 1) && (my_order->right == 0))
{
if (!fork())
{
execlp(my_order->argv[0], my_order->argv[0], my_order->argv[2], NULL);
}
else
{
wait(NULL);
}
}
else
if ((my_order->pipe == 0) && (my_order->left == 0) && (my_order->right == 1))
{
if (!fork())
{
if (strlen(my_order->argv[1]) > 1)
{
int fd = open_file(my_order->argv[3]);
dup2(fd, STDOUT_FILENO);//执行之后,标准输入就指向了fd
execlp(my_order->argv[0], my_order->argv[0], my_order->argv[1], NULL);
close(fd);
}
else
{
int fd = open_file(my_order->argv[2]);
dup2(fd, STDOUT_FILENO);
execlp(my_order->argv[0], my_order->argv[0], NULL);
close(fd);
}
}
else
{
wait(NULL);
}
}
}
其实这里有个小问题,就是像ps这样的命令参数是没有-的,直接就是ps a这样。为了简便,先这样吧。
main函数就很简单了。
int main(void)
{
while (1)
{
My_order my_order;
char p[32] = { '\0' };
puts("GYJ_LoveDanDan@desktop:—————————————— - ");
gets(p);
//char p[8] = { "ls -alh | grep lovedan " };
resolve_order(&my_order, p);
exec_order(&my_order);
}
return 0;
}
写个这程序,真的是,感觉到了自己真是菜鸡。最开始的任务其实有这个:
你的程序应该可以处理以下命令:
○ls△-l△-R○>○file1○
○cat○<○file1○|○wc△-c○>○file1○
注:○表示零个或多个空格,△表示一个或多个空格
5. 支持多重管道:类似于cat|cat|cat
我最开始为了解析字符串,操碎了心,眼看就要成功了,但是因为我用来储存的数据结构不好用于执行execlp函数,就放弃了,几经波折,我看透了。自己砍了要求,很勉强的实现了这个四不像shell。我想问人,没人回答我,我想查资料,没找到。这也许就是小说中散修和宗门的区别吧。
实现一个简单的shell的更多相关文章
- 如何写一个简单的shell
如何写一个简单的shell 看完<UNIX环境高级编程>后我就一直想写一个简单的shell来作为练习,因为有事断断续续的写了好几个月,如今写了差不多来总结一下. 源代码放在了Github: ...
- 一个简单的shell脚本
一个简单的shell脚本 一个简单的shell脚本 编写 假设我想知道目前系统上有多少人登录,使用who命令可以告诉你现在系统有谁登录: 1.[KANO@kelvin ~]$ who2.KANO tt ...
- Linux系统学习笔记之 1 一个简单的shell程序
不看笔记,长时间不用自己都忘了,还是得经常看看笔记啊. 一个简单的shell程序 shell结构 1.#!指定执行脚本的shell 2.#注释行 3.命令和控制结构 创建shell程序的步骤 第一步: ...
- python定义的一个简单的shell函数的代码
把写代码过程中经常用到的一些代码段做个记录,如下代码段是关于python定义的一个简单的shell函数的代码. pipe = subprocess.Popen(cmd, stdout=subproce ...
- Linux——模拟实现一个简单的shell(带重定向)
进程的相关知识是操作系统一个重要的模块.在理解进程概念同时,还需了解如何控制进程.对于进程控制,通常分成1.进程创建 (fork函数) 2.进程等待(wait系列) 3.进程替换(exec系列) 4 ...
- UNIX-LINUX编程实践教程->第八章->实例代码注解->写一个简单的shell
一 分析 要实现一个shell,需包含3个步骤 1)读入指令 2)指令解析 3)执行指令 1 从键盘读入指令 从键盘读入指令的几个要点: 1)调用getc函数等待并获取用户键盘输入. 2)每一行命令的 ...
- 如何在linux下编写一个简单的Shell脚本程序
在了解了linux终端和其搭配的基本Shell(默认为bash)的基础下,我们就可以在终端中用vi/vim编辑器编写一个shell的脚本程序了 Shell既为一种命令解释解释工具,又是一种脚本编程语言 ...
- 一个简单的Shell脚本(解决windows上文本在macos上乱码问题)
之所以有这一篇文章,是因为之前我写过的一篇文章:“解决Mac上打开txt文件乱码问题”:传送门: https://www.cnblogs.com/chester-cs/p/11784079.html ...
- 工作中一个简单的shell程序
下面是工作中用到的链接数据库的shell程序. #!/bin/bash ] ; then echo "prase is wrong ,please check first" exi ...
随机推荐
- [UE4]键盘鼠标输入事件
然后在角色的事件视图就可以使用预先定义好的事件
- [UE4]自动旋转组件
- Postman模拟ajax请求 并模拟登陆
2.设置header头 <span style="font-size:16px;">设置代理.cookie.X-Requested-With 注意 :X-Reque ...
- Linux 下 MQ 的安装
在WebSphere MQ 7.1版本以前,同时只能有一个产品的安装实例,在UNIX和Linux系统上,/usr/lib,/usr/bin和/usr/include目录下会增加一些软连接,也指向了这个 ...
- Android 获取手机内部信息,内核版本、基带版本、内部版本等
TextView text = (TextView) findViewById(R.id.textView1); String phoneInfo = "Product: " + ...
- Install Greenplum OSS on Ubuntu
About Greenplum Database Greenplum Database is an MPP SQL Database based on PostgreSQL. Its used in ...
- selenium在scrapy中的应用
引入 在通过scrapy框架进行某些网站数据爬取的时候,往往会碰到页面动态数据加载的情况发生,如果直接使用scrapy对其url发请求,是绝对获取不到那部分动态加载出来的数据值.但是通过观察我们会发现 ...
- Android轮询器,RxJava Interval;
基于RxJava实现轮询器,配合Retrofit处理网络请求轮询很好用,其它的一些轮询也都可以使用像Bannre图之类的: implementation 'io.reactivex.rxjava2:r ...
- Linux 安装redis 基本配置 发布订阅,安全配置,持久化 rdb ,aof
redis redis相关配置1.yum 源码 rpm yum 快速,间接,高效,解决依赖关系,(自动安装到某个路径,不可控),通过yum安装的软件查询命令 rpm -ql nginx yum源 ...
- 权重比较(id class 标签)
权重的取值: 按照 id class 标签 的顺序在其位置上标出1或者0 例如下面的例子 <!DOCTYPE html> <html lang="en"&g ...