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)
minishell的实现的更多相关文章
- 三十五、minishell(3)
35.1 内容 在当前的 minishell 中,如果执行 date clear 命令等,minishell 会停止: 这是因为引入进程组的时候,mshell 放置在前台进程组,同时之后在子进程中又创 ...
- 二十九、Linux 进程与信号——minishell(2)
编程内容: 1.完成 echo env export 命令 2.完成前后台进程 3.完成重定向 完整代码如下: 29.1 主函数.通用头文件和Makefile 29.1.1 主函数 mshell_m ...
- 手把手带你写一个minishell
先解释一下Shell : Shell是一个功能为命令行解释器的应用程序,连接了用户和Linux内核,让我们能高效和安全地使用Linux内核. 要写一个minishell,我们要先理解它的过程: 读取输 ...
- 十七、文件和目录——minishell(1)
主函数运行要去读取从标准输入或终端上输入的整个命令行,然后再去解析命令行参数,解析出来之后,要将其封装成一个 program,然后再将 program 放入 job 中,然后再去执行 job 中的命令 ...
- Tinyshell: 一个简易的shell命令解释器
这是自己最近学习Linux系统编程之后写的一个练手的小程序,能很好地复习系统编程中的进程管理.信号.管道.文件等内容. 通过回顾写的过程中遇到的问题的形式记录程序的关键点,最后给出完整程序代码. 0. ...
- 第8章 信号(6)_贯穿案例2:mini shell(3)
4. 贯穿案例2:mini shell(3) (1)之前存在问题 ①刚运行时,mshell作为前台进程.运行的其他命令会被加入新的进程组,并且调用tcsetpgrp将这个进程组设置为前台进程组,因此m ...
- 第7章 进程关系(5)_贯穿案例2:mini shell(2)
5. 贯穿案例2:mini shell(2) (1)己经完成的功能:pwd.cd.exit命令 (2)阶段性目标: ①env.export.echo及其他命令 ②标准输入.输出重定向"> ...
- Linux程序设计:目录维护
一.相关系统调用 1.1 chmod 改变访问权限. #include <sys/stat.h> int chmod(const char *path, mode_t mode) 1.2 ...
- linux系统编程综合练习-实现一个小型的shell程序(二)
上节minishell当中,已经初步实现了一个简单命令的解析,这节来继续对更加复杂命令进行解析,包含:输入重定向的解析.管道行的解析.输出重定向的解析以及是否有后台作业的解析,如下: 下面对其进行实现 ...
随机推荐
- ttl转以太网
ttl转以太网 ttl转以太网ZLSN3007S是实现TTL电平串口转以太网的"超级网口",产品自带网络变压器和RJ45网口,可以方便实现单片机.各类TTL电平串口设备的联网.首先 ...
- 在VC6.0下运行C语言程序,以及编程入门必备的常识类小知识!
今天给大家分享在VC6.0环境下编写C语言程序的基本步骤,为初学者打开学习C语言的第一道门.具体步骤如下(如果需要软件资源,可以留言): 1)新建工作区 依次点击 文件--新建--工作区 或是Ctrl ...
- JVM系列【6】GC与调优2.md
JVM系列笔记目录 虚拟机的基础概念 class文件结构 class文件加载过程 jvm内存模型 JVM常用指令 GC与调优 了解HotSpot常用命令行参数 JVM的命令行参数参考: https:/ ...
- spring boot:接收数组参数及多文件混合json参数(spring boot 2.3.4)
一,生产环境中的复杂参数上传的场景 1,保存排序值 : 例如:某一件商品的多张展示图片排序,提交的排序值要和图片的id相对应 2,上传多张图片,图片要和指定的变量相对应 例如:在添加商品sku时, 需 ...
- composer 阿里云加速 转
阿里云 Composer 全量镜像 本镜像与 Packagist 官方实时同步,推荐使用最新的 Composer 版本. 最新版本: 1.10.8 下载地址: https://mirrors.aliy ...
- centos6.8 Mysql5.6.22 升级 mysql-5.7.20
一.检查系统环境 二.备份数据库 mysqldump –all-databases > allbackupfile.sql (建议:有条件的话可使用图形化界面备份,操作灵活) 三.下载安装文件 ...
- Python可迭代对象和迭代器对象
可迭代对象iterable: 对象字面意思:Python中一切皆对象.一个实实在在存在的值. 可迭代:更新迭代.迭代是一个重复的过程,每次重复是基于上一次的结果而继续的,每次都有新的内容. 可迭代对象 ...
- SSM中 web.xml配置文件
<!--核心监听器 当tomcat(web容器,应用服务器,web服务器)启动的时候创建spring 工厂类对象,绑定到tomcat上下文中 --> <listener> &l ...
- Python opencv resize图片并保存原有的图像比例
参考链接:https://www.jianshu.com/p/3092835eab61 现有的图像是高瘦高瘦的,所以直接resize成矩形不合适.改变了整个结构. 所以采用的是先resize再padd ...
- ResultSet 处理方法
结果集(ResultSet)是数据中查询结果返回的一种对象,可以说结果集是一个存储查询结果的对象,但是结果集并不仅仅具有存储的功能,他同时还具有操纵数据的功能,可能完成对数据的更新等. 结果集读取数据 ...