• minishell的实现


      直接上各个模块的代码,注释都在文档代码中,非常详细,加上最后的Makefile文件完全可以自行运行看懂:

    main函数一个文件main.c

     1 /*
     2 minishell实现的功能:简单命令解析、管道行解析、输入输出重定向解析、一些内置命令实现、简单的信号处理
     3 未能实现的功能:语法分析、别名处理、路径扩展、通配处理、算术处理、变量处理、作业控制
     4 shell_loop{
     5     read_command //读
     6     parse_command //解析
     7     execute_command //执行
     8 }
     9 */
    10 #include "parse.h"
    11 #include "init.h"
    12 #include "def.h"
    13 
    14 char cmdline[MAXLINE+1];//定义全局变量存放读取的命令
    15 char avline[MAXLINE+1];//保存解析出来的参数
    16 char *lineptr;//初始指向cmdline数组
    17 char *avptr;//初始指向avline数组
    18 
    19 char infile[MAXNAME+1];//输入文件名,用于保存输入重定向文件名
    20 char outfile[MAXNAME+1];//输出文件名
    21 COMMAND cmd[PIPELINE];//参数列表
    22 
    23 int cmd_count;//命令个数
    24 int backgnd;//是否是后台操作
    25 int lastpid;//这是最后一个子进程退出
    26 
    27 int append;
    28 int main()
    29 {
    30         
    31     setup();//安装信号,划分到初始化模块
    32     shell_loop();//进入shell循环
    33     return 0;
    34 }

    setup信号安装部分在初始化模块中,分为两个部分init.h和init.c

    1 #ifndef _INIT_H_
    2 #define _INIT_H_
    3 void setup(void);
    4 void init(void);
    5 #endif
     1 #include "init.h"
     2 #include "externs.h"
     3 #include<stdio.h>
     4 #include<signal.h>
     5 #include<string.h>
     6 void sigint_handler(int sig)
     7 {
     8     printf("
    [minishell]$ ");
     9     fflush(stdout);//没有(
    )
    10 
    11 }
    12 void setup(void)
    13 {
    14     signal(SIGINT,sigint_handler);
    15     signal(SIGQUIT,SIG_IGN);
    16 }
    17 
    18 void init(void)
    19 {
    20     
    21     memset(cmd,0,sizeof(cmd));
    22     int i=0;
    23     for(i=0;i<PIPELINE;i++)
    24     {
    25         cmd[i].infd=0;//初始命令的输入默认为标准输入0。
    26         cmd[i].outfd=1;//初始所有输出默认标准输出1
    27     }
    28     memset(&cmdline,0,sizeof(cmdline));
    29     lineptr=cmdline;
    30     avptr=avline;
    31     memset(avline,0,sizeof(avline));
    32     memset(infile,0,sizeof(infile));
    33     memset(outfile,0,sizeof(outfile));
    34     cmd_count=0;
    35     backgnd=0;
    36     lastpid=0;
    37     append=0;
    38     printf("[minishell]$ ");
    39     fflush(stdout);//
    40 }

    shell_loop的主循环在parse.h和parse.c这两个命令解析模块中:

    #ifndef _PARSE_H_
    #define _PARSE_H_
    //定义函数接口
    void shell_loop(void);//shell循环
    int read_command(void);
    int parse_command(void);
    int execute_command(void);
    int check(const char*str);
    #endif
    #include"parse.h"
    #include<stdio.h>
    #include "def.h"
    #include "externs.h"//声明外部变量
    #include "init.h"
    #include <unistd.h>
    #include <sys/wait.h>
    #include <sys/types.h>
    #include <sys/stat.h>
    #include <fcntl.h>
    #include <signal.h>
    #include "builtin.h"
    #include "execute.h"
    void get_command(int i);
    void getname(char *name);
    void print_command();
    /*
    shell 循环
    */
    void shell_loop(void)
    {
        
        while(1)
        {
            
            //初始化环境
            init();//每次循环前初始化cmdline和COMMOND
            //读取命令
            if(read_command()==-1)
                break;
            //解析命令
            parse_command();
            
            //print_command();//打印命令
            //执行命令
            execute_command();
                
        }
        printf("
    exit
    ");
    }
    /*
    读取命令,成功返回0,失败或者读到文件结束符返回-1
    */
    int read_command(void)
    {
        /* 按行读取命令,cmdline中包含
    字符  */
        if(fgets(cmdline,MAXLINE,stdin)==NULL)//利用extern来引用全局变量,将所有的extern引用声明放入头文件
            return -1;
        return 0;
    }
    /*
    解析命令:成功返回解析到的命令个数,失败返回-1
    例如: cat < test.txt | grep -n public > test2.txt &这个命令
    cat 先解析出来cmd[0] 以及参数cmd[0].args
    然后 <输入重定向 将test.txt保存
    然后 |管道  再解析命令grep到 cmd[1] 以及两个参数到cmd[1].args...
    */
    int parse_command(void)
    {    
        //开始就检测到
    
        if (check("
    "))
            return 0;
        /*先判定是否内部命令并执行它*/
        if(builtin())
            return 0;//内部命令直接执行,不用解析
        //cmd  [< filename] [| cmd]...[or filename] [&]:方括号表示可选,省略号表示前面可以重复0次或多次
        //or 可以是 > 或者 >> 输出重定向清除文件或者追加到文件尾部方式
        //&是否由后台处理
        //例如:cat < test.txt | grep -n public > test2.txt &
        
        if(check("
    "))
            return 0;//一开始回车
        /*第一步:解析第一条简单语句*/
        get_command(0);
        /*第二步:判定是否有输入重定向符*/
        if(check("<"))
            getname(infile);//解析文件名字,check成功时,lineptr移动过所匹配的字符串
        /*第三步:判定是否有管道*/
        int i;    
        for(i=1;i<PIPELINE;i++)
        {
            if(check("|"))
                get_command(i);
            else
                break;
        }
        /*第四步:判定是否有输出重定向符*/
        if(check(">"))
        {
            //连续两个>
            if(check(">"))
            {
                append=1;//以追加的方式打开
            }
            getname(outfile);//获取后面文件名,解析到全局变量outfile中
        }
        /*第五步:判定是否有后台作业&*/
        if(check("&"))
            backgnd=1;
        /*第六步:判定命令结束'
    '*/
        if(check("
    "))
        {
            cmd_count=i;//总的命令个数 cat grep...
            return cmd_count;
        }
        //解析失败
        else
        {
            fprintf(stderr,"Command line systax error
    ");
            return -1;
        }
        return 0;
    }
    /*
    执行命令:成功返回0,失败返回-1
    */
    int execute_command(void)
    {    
        
        
        /*执行外部命令*/
        execute_disk_command();
        return 0;
    }
    
    
    
    
    //例如cmd[]      ls | wc -w 
    
    //    avline[]   ls  wc  -w   参数列表数组
    //COMMAND cmd[PIPELINE]; cmd[i]是第i条命令,cmd[i].args[j]:是第i条命令的第j个参数
    //解析命令至cmd[i],提取cmdline命令参数到avline数组中,并且将COMMAND结构中的args[]中的每个指针指向avline对应参数字符串
    void get_command(int i)
    {
        int j=0;
        int inword;//针对cat 之后有无参数。如果无参数直接遇到<,inword就不会置1.那么switch遇到<直接args[1]为NULL
        //cat < test.txt | grep -n public > test2.txt &
        while(*lineptr!='')
        {
            //lineptr指向cmdline
            while(*lineptr==' '||*lineptr=='	')
                lineptr++;
            if(*lineptr=='
    '||*lineptr=='')
                break;
            //将第i条命令第j个参数指向avptr
            cmd[i].args[j]=avptr;//例如 agrs[0]指向cat  args[1]应该指向空,所以引入inword
            while(*lineptr!=''&&*lineptr!='
    '&&*lineptr!='<'&&*lineptr!='|'
                    &&*lineptr!='>'&&*lineptr!='&'&&*lineptr!=' '&&*lineptr!='	')
            {
                *avptr++=*lineptr++;//参数提取至avptr指针指向的数组avline。
                inword=1;
            }
            *avptr++='';
            switch(*lineptr)
            {
                //解析到下一个参数。break回来继续。
                case ' ':
                case '	':
                    inword=0;
                    j++;
                    break;
                //这条命令提取结束
                case '<':
                case '>':
                case '|':
                case '&':
                case '
    ':
                    if(inword==0)  cmd[i].args[j]=NULL;
                    return ;//只解析第i条语句。完了函数就返回
                //  for 
                default:
                    return ;        
            }
        }
    }
    void print_command()
    {
        int i;
        int j;
        printf("cmd_count=%d
    ",cmd_count);
        if(infile[0]!='')
            printf("infile=[%s]
    ",infile);
        if(outfile[0]!='')
            printf("outfile=[%s]
    ",outfile);
        for(i=0;i<cmd_count;i++)
        {
            j=0;
            while(cmd[i].args[j]!=NULL)
            {
                printf("[%s] ",cmd[i].args[j]);
                j++;
            }
            printf("
    ");
        }
        
    }
    /*
    将lineptr中的字符串与str进行匹配
    成功返回1,失败返回0,成功时lineptr移过所匹配的字符串。失败时lineptr不变
    */
    int check(const char*str)
    {
    //lineptr指向cmd 遇到<  >  | & 会返回
        char *p;
        while(*lineptr==' '||*lineptr=='	')
            lineptr++;
        p=lineptr;
        
        while(*str!=''&&*str==*p)
        {
            str++;
            p++;
        }
        //*str== 或者*str!=*p
        if(*str=='')//str中字符都匹配完了,之前的全部一致
        {
            lineptr=p;//移动lineptr.
            return 1;
        }
        //未解析到则不用移动
        return 0;
    }
    void getname(char *name)
    {
        while(*lineptr==' '||*lineptr=='	')
            lineptr++;
        while(*lineptr!=''&&*lineptr!='
    '&&*lineptr!='<'&&*lineptr!='|'
                    &&*lineptr!='>'&&*lineptr!='&'&&*lineptr!=' '&&*lineptr!='	')
        {
            *name++=*lineptr++;
        }    
        *name='';
    }

    在shell_loop的主循环while(1)中解析完命令就是执行命令,执行命令在execute.h和execute.c两个文件中实现

    1 #ifndef _EXECUTE_H
    2 #define _EXECUTE_H
    3 
    4 void execute_disk_command(void);
    5 void forkexec(int);
    6 #endif
      1 #include "execute.h"
      2 #include "def.h"
      3 #include "externs.h"
      4 #include <unistd.h>
      5 #include <sys/wait.h>
      6 #include <sys/types.h>
      7 #include <sys/stat.h>
      8 #include <fcntl.h>
      9 #include <signal.h>
     10 #include <stdio.h>
     11 //执行命令,通过输入输出文件是否为空来判断是否有重定向命令要执行
     12 //通过命令个数来判断是否有管道符来决定直接执行命令(标准输入输出)还是执行管道命令
     13 void execute_disk_command(void)
     14 {
     15     if(cmd_count==0)
     16             return ;//没有命令只有换行就不执行,除BUG
     17         //解析执行带输入输出重定向命令.
     18         //cat < test.txt | grep -n public > test2.txt &
     19         if(infile[0]!='')//  <  输入重定向,只可能是第一条命令
     20         {
     21             cmd[0].infd=open(infile,O_RDONLY);
     22         }
     23 
     24         if(outfile[0]!='')// > 或者 >> 输出重定向只能是最后一条命令
     25         {
     26             if(append)//追加方式
     27             {
     28                 umask(0);
     29                 cmd[cmd_count-1].outfd=open(outfile,O_WRONLY|O_CREAT|O_APPEND,0666);
     30             }
     31             else
     32             {
     33                 umask(0);
     34                 cmd[cmd_count-1].outfd=open(outfile,O_WRONLY|O_CREAT|O_TRUNC,0666);
     35             }
     36         }
     37         //后台作业,忽略掉SIGCHLD信号,防止僵尸进程
     38         //后台作业不会调用wait等待子进程退出
     39         if(backgnd==1)
     40         {
     41             signal(SIGCHLD,SIG_IGN);//下个命令之前需要还原,忽略了退出信号。无法在backgnd==0时等待
     42         }
     43         else signal(SIGCHLD,SIG_DFL);//如果不还原。例如刚执行了一个wc & 那么后台进程会时SIGCHLD被忽略,前台进程父进程才要wait。执行ls前台进程时,如果不将SIGCHLD处理函数还原就会使得while(wait(NULL)!=lastpid)
     44         /*只带管道的话 例如: ls | grep init | wc -w*/
     45         int i=0;
     46         int fd;
     47         int fds[2];
     48         //ls | grep init | wc - w     cmd[0]:ls  cmd[1]:grep  cmd[2]:wc
     49         for(i=0;i<cmd_count;i++)
     50         {
     51             if(i<cmd_count-1)//不是最后一条命令。如果cmd_count=1那么就没有管道符就不用创建管道符
     52             {
     53                 pipe(fds);//创建管道 cmd[i]的输出为 cmd[i+1]的输入。所以把cmd[i]的输出置为管道的写端,管道的读端作为cmd[i+1]的输入
     54                 cmd[i].outfd=fds[1];//将当前命令的输出定向到管道的写端
     55                 cmd[i+1].infd=fds[0];//将下一条命令的输入定向到管道的读端
     56             
     57             }
     58             forkexec(i);//fork子进程执行命令,传入结构体指针cmd结构体数组
     59         
     60             if((fd=cmd[i].infd)!=0)//进程执行完,还原
     61                 close(fd);·    
     62             if((fd=cmd[i].outfd)!=1)//标准输出
     63                 close(fd);
     64         }
     65         //后台作业控制,backgnd==1不需要等待,需要防止产生僵尸进程
     66         if(backgnd==0)//前台作业0
     67         {
     68             /* 前台作业,需要等待管道中最后一个命令退出 */
     69             while(wait(NULL)!=lastpid)
     70             ;//等待最后一个进程结束。如果不等待,那么父进程可能先退出,重新开始循环等待输入命令,先打印出[minishell$]。子进程再输出结果
     71         }
     72 }
     73 void forkexec(int i)
     74 {
     75     pid_t pid;
     76     pid=fork();
     77     if(pid==-1)
     78         ERR_EXIT("fork error");
     79     if(pid>0)
     80     {
     81         //父进程
     82         if(backgnd==1)
     83             printf("%d
    ",pid);//打印后台进程的进程ID
     84         lastpid=pid;//保存最后一个进程ID.
     85         
     86     }    
     87     else if(pid==0)
     88     {
     89         //ls | wc -c
     90         //backgnd==1,将第一条简单命令的infd重定向至/dev/null
     91         //当第一条命令试图从标准输入获取数据的时候,立即返回EOF。
     92         //这样就不用考虑作业控制了
     93         if(cmd[i].infd==0&&backgnd==1)//输入描述符等于0,肯定是第一条命令
     94             cmd[i].infd=open("/dev/null",O_RDONLY);
     95         //将第一个简单命令进程作为进程组组长,信号发给当前整个进程组,父进程不再收到
     96         if(i==0)
     97         {
     98             //将第一个简单命令进程单独设置为一个进程组,那么信号SIGINT只会发给这个进程组。不会发给父进程minishell这样就不会打印两次minishell$
     99             setpgid(0,0);
    100     
    101         }
    102         //子进程
    103         if(cmd[i].infd!=0) //输入不是标准输入,命令从管道输入
    104         {
    105             //等价于dup2(cmd[i].infd,0)
    106             close(0);
    107             dup(cmd[i].infd);//将命令输入描述符也就是管道读端,置位命令的标准输入
    108             
    109         }
    110         if(cmd[i].outfd!=1)//命令的输出不是标准输出,那么命令的输出就是输出到管道。
    111         {
    112             close(1);
    113             dup(cmd[i].outfd);
    114         }
    115         int j;
    116         for(j=3;j<sysconf(_SC_OPEN_MAX);j++)
    117             close(j);//关闭3以上文件描述符
    118         if(backgnd==0)//前台作业恢复信号
    119         {
    120             signal(SIGINT,SIG_DFL);//前台作业需要将信号还原,不然如果ctrl+c会调用init中的信号处理函数打印两次minshell$
    121             signal(SIGQUIT,SIG_DFL);
    122         }
    123         /*
    124         实现I/O重定向
    125 
    126     调用exec后,原来打开的文件描述符仍然是打开的。利用这一点可以实现I/O重定向。 
    127     先看一个简单的例子,把标准输入转成大写然后打印到标准输出:
    128 
    129     例大小写转换源码upper.c:
    130     #include <stdio.h>
    131 
    132     int main(void)
    133     {
    134     int ch;
    135     while((ch = getchar()) != EOF) {
    136     putchar(toupper(ch));
    137     }
    138     return 0;
    139     }
    140 
    141     程序wrapper.c:
    142     #include <unistd.h>
    143     #include <stdlib.h>
    144     #include <stdio.h>
    145     #include <fcntl.h>
    146     int main(int argc, char *argv[])
    147     {
    148     int fd;
    149     if (argc != 2) {
    150     fputs("usage: wrapper file
    ", stderr);
    151     exit(1);
    152     }
    153 
    154     fd = open(argv[1], O_RDONLY);
    155     if(fd<0) {
    156     perror("open");
    157     exit(1);
    158     }
    159 
    160     dup2(fd, STDIN_FILENO);
    161     close(fd);
    162 
    163     execl("./upper", "upper", NULL);
    164     perror("exec ./upper");
    165     exit(1);
    166     }
    167 
    168     wrapper程序将命令行参数当作文件名打开,将标准输入重定向到这个文件,然后调用exec执行upper程序,这时原来打开的文件描述符仍然是打开的,upper程序只负责从标准输入读入字符转成大写,并不关心标准输入对应的是文件还是终端。
    169         */
    170         execvp(cmd[i].args[0],cmd[i].args);
    171         //替换失败就到这行
    172         exit(EXIT_FAILURE);
    173     }
    174 
    175 }

    build.c和build.h是内部命令解析模块,这部分内容基本还没有去实现...

    1 #ifndef _BUILTIN_H
    2 #define _BUILTIN_H
    3  
    4 int builtin(void);
    5 #endif
     1 #include "builtin.h"
     2 /*
     3 内部命令解析,返回1表示内部命令,返回0不是内部命令
     4 */
     5 void do_exit();
     6 void do_cd();
     7 int builtin(void)
     8 {
     9     if (check("exit"))
    10         do_exit();
    11     else if (check("cd"))
    12         do_cd();
    13     else
    14         return 0;
    15     return 1;
    16 }
    17 
    18 void do_exit()
    19 {
    20     printf("exit");
    21     exit(EXIT_SUCCESS);
    22 }
    23 void do_cd()
    24 {
    25     printf("do_cd...
    ");
    26 }

    def.h声明一些各个模块中用到的宏

    //头文件声明宏
    #ifndef _DEF_H_
    #define _DEF_H_
    
    #include<stdio.h>
    #include <stdlib.h>
    
    #define ERR_EXIT(m)
        do
        {
            perror(m);
            exit(EXIT_FAILURE);
        }while(0) 
    #define MAXLINE 1024//输入行最大长
    #define MAXARG  20  //每个简单命令参数最多个数
    #define PIPELINE  5//一个管道行简单命令最多个数
    #define MAXNAME   100//IO重定向文件名最大个数
    
    typedef struct command
    {
        char *args[MAXARG+1];//参数解析出来放到args中,参数列表
        int infd;//输入描述符
        int outfd;//输出描述符
    } COMMAND;
    
    
    
    #endif

    externs.h主要是一些外部变量的声明

    #ifndef _EXTERNS_H
    #define _EXTERNS_H
    
    #include "def.h"
    extern char cmdline[MAXLINE+1];
    extern COMMAND cmd[PIPELINE];
    extern char avline[MAXLINE+1];
    extern char *lineptr;//指向cmdline数组
    extern char *avptr;//指向avline数组
    extern int cmd_count;
    extern int backgnd;
    extern char infile[MAXNAME+1];
    extern char outfile[MAXNAME+1];
    extern int lastpid;
    extern int append;
    #endif

    最后是一个Makefile文件

    .PHONY:clean 
    CC=gcc
    CFLAGS=-Wall -g
    BIN=minishell 
    OBJS=main.o parse.o init.o execute.o builtin.o
    $(BIN):$(OBJS)
        $(CC) $(CFLAGS) $^ -o $@
    %.o:%.c
        $(CC) $(CFLAGS)  -c $< -o $@
    clean:
        rm -f *.o $(BIN)
  • 相关阅读:
    aria2安装webui
    c++指针参数是如何传递内存的
    ssl 证书申请
    LNMP一键包安装后解决MySQL无法远程连接问题
    流水线设计 转:http://www.opengpu.org/forum.php?mod=viewthread&tid=2424
    IUS nc simulator
    ccd与coms摄像头的区别
    昨天下午写的FPGA驱动VGA显示图片
    tcl脚本
    用FPGA驱动ov7670摄像头用tft9328显示
  • 原文地址:https://www.cnblogs.com/wsw-seu/p/8280531.html
Copyright © 2020-2023  润新知