具有基本功能的shell的具体代码实现,基本功能shell代码偶然看到一篇文章讲解


在前几个月对Linux的学习过程中,一直在与shell进行交互,感觉shell充满了神秘感。偶然看到一篇文章讲解了shell的实现,感觉也不是很难的样子,于是自己也开始开发自己的minishell,顺便也巩固了前一段时间学习的linux系统编程的知识。

先来展示一下我的这个minishell实现的功能:

1. 支持ls,touch,wc 等外部命令


2. 支持输入输出重定向符

3. 支持管道命令

4 .支持后台作业

5. 支持cd,jobs,kill,exit等内部命令(自己还写了一个about 命令 ^ _ ^)

6. 支持对ctrl+c 和ctrl +z 信号的处理

接下来我们按照编写的步骤一一来分析:

(一)命令的解析

输入命令的解析在本程序中占到了很大的比重,虽然像这种解析普通命令的程序(正则表达式太难了。。)的解释器难度不大,但是健壮性和全面性还是需要周全考虑的。


这里采用了分段解析,先除去起始空格,制表符等,并以此和一些‘|’,‘<’为分割界限来解析命令至COMMAND结构体。直接看代码吧,注释很详细!

[cpp]view plaincopy

/*

*解析命令

*成功返回解析到的命令个数,失败返回-1

*/

intparse_command(void)

{

/*cat<test.txt|grep-npublic>test2.txt&*/

if(check("n"))

return0;

/*判断是否内部命令并执行它*/

if(builtin())

return0;

/*1、解析第一条简单命令*/

get_command(0);

/*2、判定是否有输入重定向符*/

if(check("<"))

getname(infile);

/*3、判定是否有管道*/

inti;

for(i=1;i<PIPELINE;++i)

{

if(check("|"))

get_command(i);

else

break;

}

/*4、判定是否有输出重定向符*/

if(check(">"))

{

if(check(">"))

append=1;

getname(outfile);

}

/*5、判定是否后台作业*/

if(check("&"))

backgnd=1;

/*6、判定命令结束‘n’*/

if(check("n"))

{

cmd_count=i;

returncmd_count;

}

else

{

fprintf(stderr,"Commandlinesyntaxerrorn");

return-1;

}

}

/*

*解析简单命令至cmd[i]

*提取cmdline中的命令参数到avline数组中,

*并且将COMMAND结构中的args[]中的每个指针指向这些字符串

*/

voidget_command(inti)

{

/*cat<test.txt|grep-npublic>test2.txt&*/

intj=0;

intinword;

while(*lineptr!='')

{

/*去除空格*/

while(*lineptr==''||*lineptr=='t')

lineptr++;

/*将第i条命令第j个参数指向avptr*/

cmd[i].args[j]=avptr;

/*提取参数*/

while(*lineptr!=''

&&*lineptr!=''

&&*lineptr!='t'

&&*lineptr!='>'

&&*lineptr!='<'

&&*lineptr!='|'

&&*lineptr!='&'

&&*lineptr!='n')

{

/*参数提取至avptr指针所向的数组avline*/

*avptr++=*lineptr++;

inword=1;

}

*avptr++='';

switch(*lineptr)

{

case'':

case't':

inword=0;

j++;

break;

case'<':

case'>':

case'|':

case'&':

case'n':

if(inword==0)

cmd[i].args[j]=NULL;

return;

default:/*for''*/

return;

}

}

}

/*

*将lineptr中的字符串与str进行匹配

*成功返回1,lineptr移过所匹配的字符串

*失败返回0,lineptr保持不变

*/

intcheck(constchar*str)

{

char*p;

while(*lineptr==''||*lineptr=='t')

lineptr++;

p=lineptr;

while(*str!=''&&*str==*p)

{

str++;

p++;

}

if(*str=='')

{

lineptr=p;/*lineptr移过所匹配的字符串*/

return1;

}

/*lineptr保持不变*/

return0;

}

voidgetname(char*name)

{

while(*lineptr==''||*lineptr=='t')

lineptr++;

while(*lineptr!=''

&&*lineptr!=''

&&*lineptr!='t'

&&*lineptr!='>'

&&*lineptr!='<'

&&*lineptr!='|'

&&*lineptr!='&'

&&*lineptr!='n')

{

*name++=*lineptr++;

}

*name='';

}


(二)命令的执行和实现

1、程序框架:

在对命令的解析完毕后,我们先考虑两个大的方向,即是外部命令还是内部命令?

外部命令的话,我们只需要fork一个子进程,用execvp()来执行就可以了;对于内部命令则需要自己去实现。

提出两个问题:第一个,为什么要使用execvp() ?第二个,为什么要fork一个子进程来实现,直接while循环不可以吗?

解答:

(1)我们之所以使用execvp(),是因为函数的原型是 int execvp(const char *file ,char * const argv []); 第一个参数是命令文件名,第二个是参数,执行命令非 常的方便。

(2)一旦执行execvp(),当前进程就会被execvp的进程所替代,执行完后就会结束程序,所以while循环是不可以的,必须要fork一个子进程来执行。

[cpp]view plaincopy

while(1){/*repeatforever*/

type_prompt();/*displaypromptonthescreen*/

read_command(command,parameters);/*readinputfromterminal*/

if(fork()!=0){/*forkoffchildprocess*/

/*Parentcode*/

waitpid(-1,&status,0);/*waitforchildtoexit*/

}else{

/*Childcode*/

execvp(command,parameters);/*executecommand*/

}

}

利用这个框架,外部命令(可执行文件)的功能基本实现(vi ,top ,ps等均可使用)。

2、输入输出重定向

当分析出来有输入输出重定向的符号时,我们要使用dup()函数来实现。函数详解请参考我的博客

对于输入的句法分析结果,我们使用一个结构体来保存:

[cpp]view plaincopy

typedefstructcommand

{

char*args[MAXARG+1];/*解析出的命令参数列表*/

intinfd;

intoutfd;

}COMMAND;

基本流程:

[cpp]view plaincopy

/*子进程*/

if(cmd[i].infd!=0)

{

close(0);

dup(cmd[i].infd);

}

if(cmd[i].outfd!=1)

{

close(1);

dup(cmd[i].outfd);

}

intj;

for(j=3;j<OPEN_MAX;++j)

close(j);

其中cmd[i].infd和cmd[i].outfd是解析出来的重定向位置的全局变量。

3、管道命令

管道命令是使用pipe()函数实现的。关于管道的详解请参考我的博客

假如我们有 a | b | c 这样一个形式的命令,那么是需要创建两条管道的,依次类推。

[cpp]view plaincopy

inti;

intfd;

intfds[2];

for(i=0;i<cmd_count;++i)

{

/*如果不是最后一条命令,则需要创建管道*/

if(i<cmd_count-1)

{

pipe(fds);

cmd[i].outfd=fds[1];

cmd[i+1].infd=fds[0];

}

forkexec(i);

if((fd=cmd[i].infd)!=0)

close(fd);

if((fd=cmd[i].outfd)!=1)

close(fd);

}

if(backgnd==0)

{

/*前台作业,需要等待管道中最后一个命令退出*/

while(wait(NULL)!=lastpid)

;

}

4.后台作业和信号处理

判断后台,我们只需要解析命令看是否存在 “&”,若存在则backgnd = 1,不再对后台进程进行wait。为了避免僵尸进程,我们可是选择使用signal()处理SIGCHLD,将其忽略,同时忽略SIGINT和SIGQUIT信号(后台不响应ctrl+c,ctrl+z)。但是注意backgnd=0的时候要将这两个信号再设置成默认处理,否则前台也不能响应信号了。


5.内部命令

1、 cd命令的实现


cd命令的实现主要依赖于系统调用chdir()。我们通过将第一个参数传入chdir就可以进行一次成功的cd调用。通过判断chdir()不同的返回值可以判断出更改目录成功与否,并能输出错误原因。

[cpp]view plaincopy

voiddo_cd(void)

{

get_command(0);

intfd;

fd=open(*(cmd[0].args),O_RDONLY);

fchdir(fd);

close(fd);

}

2、 jobs命令的实现


jobs命令我们维护一个链表,每次当有一个后台进程运行的时候,都要向这个链表中添加一个数据。并当子进程结束的时候会向父进程发送SIGCHLD信号,父进程也就是Shell要处理这个信号,并且将后台进程链表中相应的进程进行处理,也就是将其移除。

[cpp]view plaincopy

/*父进程*/

if(backgnd==1)

{

/*添加入jobs的链表*/

NODE*p=(NODE*)malloc(sizeof(NODE));

p->npid=pid;

printf("%s",cmd[0].args[0]);

strcpy(p->backcn,cmd[0].args[0]);

//printf("%s",p->backcn);

NODE*tmp=head->next;

head->next=p;

p->next=tmp;

}


3、 exit命令的实现


exit命令分两部分实现。第一,当词法分析到exit的时候直接调用系统调用exit()就可以了。第二,退出之前要判断一下后台进程链表中是否还有未执行完的任务,如果有未执行完的任务,要提示用户,等待用户选择。

[cpp]view plaincopy

voiddo_exit(void)

{

intPgnum=0;

NODE*tmp=head->next;

while(tmp!=NULL)

{

Pgnum++;

tmp=tmp->next;

}

if(Pgnum!=0)

{

printf("Thereareprogramsinthebackground,areyousuretoexit?y/Nn");

charc=getchar();

if(c=='N')

return;

else

gotoloop;

}

loop:

printf("exitn");

exit(EXIT_SUCCESS);

}

4、 kill命令的实现


kill命令的实现是通过信号来实现的,我们使用kill -9 +pid来强制结束后台进程,用kill系统调用向相应的进程发送SIGQUIT信号来使进程强制退出。


[cpp]view plaincopy

voiddo_kill(void)

{

get_command(0);

intnum=atoi(cmd[0].args[1]);

signal(SIGQUIT,SIG_DFL);

kill(num,SIGQUIT);

signal(SIGQUIT,SIG_IGN);

NODE*bng=head->next;

NODE*pre=head;

while(bng!=NULL)

{

if(bng->npid==num)

{

NODE*nxt=bng->next;

pre->next=nxt;

break;

}

pre=bng;

bng=bng->next;

}

}


到这里,本程序的功能已经基本实现,效果还算不错。

注:本程序的具体源码托管至Github ,欢迎大家关注!

然而依然存在一些不足之处:

1.因为时间和测试不足的关系,肯定存在着bug

2.没能支持正则表达式等复杂的命令解析

3.不能执行shell脚本。

4.没有实现上下键查看历史命令的功能。

总的来说,自己收获很大,也希望可以帮助到大家!

相关内容

    暂无相关文章