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)
原文地址:https://www.cnblogs.com/wsw-seu/p/8280531.html