使用已学习的各种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的更多相关文章

  1. 如何写一个简单的shell

    如何写一个简单的shell 看完<UNIX环境高级编程>后我就一直想写一个简单的shell来作为练习,因为有事断断续续的写了好几个月,如今写了差不多来总结一下. 源代码放在了Github: ...

  2. 一个简单的shell脚本

    一个简单的shell脚本 一个简单的shell脚本 编写 假设我想知道目前系统上有多少人登录,使用who命令可以告诉你现在系统有谁登录: 1.[KANO@kelvin ~]$ who2.KANO tt ...

  3. Linux系统学习笔记之 1 一个简单的shell程序

    不看笔记,长时间不用自己都忘了,还是得经常看看笔记啊. 一个简单的shell程序 shell结构 1.#!指定执行脚本的shell 2.#注释行 3.命令和控制结构 创建shell程序的步骤 第一步: ...

  4. python定义的一个简单的shell函数的代码

    把写代码过程中经常用到的一些代码段做个记录,如下代码段是关于python定义的一个简单的shell函数的代码. pipe = subprocess.Popen(cmd, stdout=subproce ...

  5. Linux——模拟实现一个简单的shell(带重定向)

    进程的相关知识是操作系统一个重要的模块.在理解进程概念同时,还需了解如何控制进程.对于进程控制,通常分成1.进程创建  (fork函数) 2.进程等待(wait系列) 3.进程替换(exec系列) 4 ...

  6. UNIX-LINUX编程实践教程->第八章->实例代码注解->写一个简单的shell

    一 分析 要实现一个shell,需包含3个步骤 1)读入指令 2)指令解析 3)执行指令 1 从键盘读入指令 从键盘读入指令的几个要点: 1)调用getc函数等待并获取用户键盘输入. 2)每一行命令的 ...

  7. 如何在linux下编写一个简单的Shell脚本程序

    在了解了linux终端和其搭配的基本Shell(默认为bash)的基础下,我们就可以在终端中用vi/vim编辑器编写一个shell的脚本程序了 Shell既为一种命令解释解释工具,又是一种脚本编程语言 ...

  8. 一个简单的Shell脚本(解决windows上文本在macos上乱码问题)

    之所以有这一篇文章,是因为之前我写过的一篇文章:“解决Mac上打开txt文件乱码问题”:传送门: https://www.cnblogs.com/chester-cs/p/11784079.html ...

  9. 工作中一个简单的shell程序

    下面是工作中用到的链接数据库的shell程序. #!/bin/bash ] ; then echo "prase is wrong ,please check first" exi ...

随机推荐

  1. [UE4]键盘鼠标输入事件

    然后在角色的事件视图就可以使用预先定义好的事件

  2. [UE4]自动旋转组件

  3. Postman模拟ajax请求 并模拟登陆

    2.设置header头 <span style="font-size:16px;">设置代理.cookie.X-Requested-With   注意 :X-Reque ...

  4. Linux 下 MQ 的安装

    在WebSphere MQ 7.1版本以前,同时只能有一个产品的安装实例,在UNIX和Linux系统上,/usr/lib,/usr/bin和/usr/include目录下会增加一些软连接,也指向了这个 ...

  5. Android 获取手机内部信息,内核版本、基带版本、内部版本等

    TextView text = (TextView) findViewById(R.id.textView1); String phoneInfo = "Product: " + ...

  6. Install Greenplum OSS on Ubuntu

    About Greenplum Database Greenplum Database is an MPP SQL Database based on PostgreSQL.  Its used in ...

  7. selenium在scrapy中的应用

    引入 在通过scrapy框架进行某些网站数据爬取的时候,往往会碰到页面动态数据加载的情况发生,如果直接使用scrapy对其url发请求,是绝对获取不到那部分动态加载出来的数据值.但是通过观察我们会发现 ...

  8. Android轮询器,RxJava Interval;

    基于RxJava实现轮询器,配合Retrofit处理网络请求轮询很好用,其它的一些轮询也都可以使用像Bannre图之类的: implementation 'io.reactivex.rxjava2:r ...

  9. Linux 安装redis 基本配置 发布订阅,安全配置,持久化 rdb ,aof

    redis redis相关配置1.yum  源码 rpm  yum 快速,间接,高效,解决依赖关系,(自动安装到某个路径,不可控),通过yum安装的软件查询命令 rpm -ql nginx  yum源 ...

  10. 权重比较(id class 标签)

    权重的取值: 按照 id   class  标签 的顺序在其位置上标出1或者0 例如下面的例子 <!DOCTYPE html> <html lang="en"&g ...