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("\n[minishell]$ "); 9 fflush(stdout);//没有(\n) 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);//无\n 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("\nexit\n"); } /* 读取命令,成功返回0,失败或者读到文件结束符返回-1 */ int read_command(void) { /* 按行读取命令,cmdline中包含\n字符 */ 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) { //开始就检测到\n if (check("\n")) return 0; /*先判定是否内部命令并执行它*/ if(builtin()) return 0;//内部命令直接执行,不用解析 //cmd [< filename] [| cmd]...[or filename] [&]:方括号表示可选,省略号表示前面可以重复0次或多次 //or 可以是 > 或者 >> 输出重定向清除文件或者追加到文件尾部方式 //&是否由后台处理 //例如:cat < test.txt | grep -n public > test2.txt & if(check("\n")) 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; /*第六步:判定命令结束'\n'*/ if(check("\n")) { cmd_count=i;//总的命令个数 cat grep... return cmd_count; } //解析失败 else { fprintf(stderr,"Command line systax error\n"); return -1; } return 0; } /* 执行命令:成功返回0,失败返回-1 */ int execute_command(void) { /*执行外部命令*/ execute_disk_command(); return 0; } //例如cmd[] ls | wc -w \n // avline[] ls \0 wc \0 -w \0 参数列表数组 //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!='\0') { //lineptr指向cmdline while(*lineptr==' '||*lineptr=='\t') lineptr++; if(*lineptr=='\n'||*lineptr=='\0') break; //将第i条命令第j个参数指向avptr cmd[i].args[j]=avptr;//例如 agrs[0]指向cat args[1]应该指向空,所以引入inword while(*lineptr!='\0'&&*lineptr!='\n'&&*lineptr!='<'&&*lineptr!='|' &&*lineptr!='>'&&*lineptr!='&'&&*lineptr!=' '&&*lineptr!='\t') { *avptr++=*lineptr++;//参数提取至avptr指针指向的数组avline。 inword=1; } *avptr++='\0'; switch(*lineptr) { //解析到下一个参数。break回来继续。 case ' ': case '\t': inword=0; j++; break; //这条命令提取结束 case '<': case '>': case '|': case '&': case '\n': if(inword==0) cmd[i].args[j]=NULL; return ;//只解析第i条语句。完了函数就返回 // for \0 default: return ; } } } void print_command() { int i; int j; printf("cmd_count=%d\n",cmd_count); if(infile[0]!='\0') printf("infile=[%s]\n",infile); if(outfile[0]!='\0') printf("outfile=[%s]\n",outfile); for(i=0;i<cmd_count;i++) { j=0; while(cmd[i].args[j]!=NULL) { printf("[%s] ",cmd[i].args[j]); j++; } printf("\n"); } } /* 将lineptr中的字符串与str进行匹配 成功返回1,失败返回0,成功时lineptr移过所匹配的字符串。失败时lineptr不变 */ int check(const char*str) { //lineptr指向cmd 遇到< > | & 会返回 char *p; while(*lineptr==' '||*lineptr=='\t') lineptr++; p=lineptr; while(*str!='\0'&&*str==*p) { str++; p++; } //*str==\0 或者*str!=*p if(*str=='\0')//str中字符都匹配完了,之前的全部一致 { lineptr=p;//移动lineptr. return 1; } //未解析到则不用移动 return 0; } void getname(char *name) { while(*lineptr==' '||*lineptr=='\t') lineptr++; while(*lineptr!='\0'&&*lineptr!='\n'&&*lineptr!='<'&&*lineptr!='|' &&*lineptr!='>'&&*lineptr!='&'&&*lineptr!=' '&&*lineptr!='\t') { *name++=*lineptr++; } *name='\0'; }
在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]!='\0')// < 输入重定向,只可能是第一条命令 20 { 21 cmd[0].infd=open(infile,O_RDONLY); 22 } 23 24 if(outfile[0]!='\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\n",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\n", 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...\n"); 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)