使用已学习的各种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. linux system()函数详解

    system(3) - Linux man page Name system - execute a shell command Synopsis #include <stdlib.h> ...

  2. 第15课 右值引用(2)_std::move和移动语义

    1. std::move (1)std::move的原型 template<typename T> typename remove_reference<T>::type& ...

  3. (转)C#SocketAsyncEventArgs实现高效能多并发TCPSocket通信

    原文地址:http://freshflower.iteye.com/blog/2285272.http://freshflower.iteye.com/blog/2285286 一)服务器端 说到So ...

  4. cobbler部署centos6与centos7系列

    cobbler部署centos6与centos7系列 转载自:http://www.jianshu.com/p/a4bed77bf40d 版权声明:完全抄自 http://www.jianshu.co ...

  5. 简单说明一下Token ,Cookie,Session

    在Web应用中,HTTP请求是无状态的.即:用户第一次发起请求,与服务器建立连接并登录成功后,为了避免每次打开一个页面都需要登录一下,就出现了cookie,Session. Cookie Cookie ...

  6. POJ2311 Cutting Game 博弈 SG函数

    Cutting Game Description Urej loves to play various types of dull games. He usually asks other peopl ...

  7. this 基础使用方法

    在Java中,this是调用类中变量和内部类的构造方法的关键词,在对象有同名变量时,可以指定类的变量. 例子1: package example_1; import java.lang.*; publ ...

  8. 1.正则re

    正则 :规则表达式 一般在匹配非结构化的数据时用的比较多,结构化的数据一般用xpath,bs4.但具体使用起来都是视情况而定,相对而言.正则规则平时涉及最多也就是匹配邮箱,电话,及特殊字符串.规则相对 ...

  9. 41.纯 CSS 绘制一支栩栩如生的铅笔

    原文地址: https://segmentfault.com/a/1190000015153865 感想: 不难 HTML code: <div class="pencil" ...

  10. oozie 工作流调试及报错

    1.  oozie 调用sql文件的workflow 错误汇总: 1)hive2server密码错误.(有时设置可以无密码,有时需要登陆密码,有时是单独的hive2server密码) Connecti ...