Shell编程
Shell编程
[ 实验目的 ]
学习如何编写一个Unix Shell程序,使得有机会了解如何创建子进程来执行一项专门的工作以及父进程如何继续进行子进程的工作。
[ 功能要求 ]
编写一个C语言程序作为Linux内核的Shell命令行解释程序,所执行的结果需和系统命令方式保持一致。基本运行方式如下:
当敲入命令行如:identifier[identifier[identifier]],Shell应该解析命令行参数指针数组argv[argc]。
Shell程序需要具有以下几种功能和健壮性:
1. 支持目录检索功能,即文件不存在,继续打印提示符
2. 支持以“&”结束的输入,进行并发执行(前台与后台)
3. 支持输入输出重定向,“<”,“>”为标志符
4. 支持以“|”进行进程间通信操作(管道功能)
5. 支持一定的错误输入处理。
[ 函数功能与参数说明 ]
子函数的功能与参数:
◆ void get_string(char command[]):
功能:接受用户输入的命令字符串。
参数:command[]存放用户输入的字符串。
◆ int split(char* argv[],char usrComm[]):
功能:将用户输入的命令字符串分割成一个一个的单词,其中“<”,“>”,“|”也各自当做一个单独的单词,同时若有这些符号,将相应标志位置成相应值。
参数:usrComm[]存放的是待分割的命令,argv[]是存放分割好的单词的数组,返回值是分割后得到的单词数目(传给argc)。
◆ int set_Background(char *argv[],int argc):
功能:检测是否存在并发执行的符号“&”,并设置并发执行的标志(全局变量)。
参数:argv[]是将用户输入的命令进行分割后的单词数组,argc是命令中单词个数,由于&符号占一个单词的数目,所以如果发现&,命令中单词个数argc实际上应该减一,故有一个返回值给argc。
◆ int delet_space(int ptr,char usrComm[]):
功能:过滤掉用户输入字符串的当前分析位置开始的连续空格
参数:usrComm[]是正在分析的用户输入的命令,ptr指示usrComm[]中当前待分析的字符下标。返回连续空格后面第一个不是空格的字符下标,即过滤掉连续空格后的分析位置。
◆ int get_a_word(char *argv1,int ptr,char com[]):
功能:从用户输入字符串的当前待分析的字符开始向后分析出一个单词,或者一个符号(<、>、|)。
参数:argv1存放这个分析出来的单词,ptr代表待分析的字符的数组下标,com代表用户输入的字符串。返回值是这个单词之后的第一个字符的数组下标。
◆ void setOutFile(char outFile[]):
功能:设置输出重定向到文件。
参数:outFile[],输出文件名称。
◆ void setInFile(char inFile[]):
功能:设置输入重定向到文件。
参数:inFile[],输入文件名称。
◆ int GetPath(char *paths[]):
功能:将从PATH读出的绝对路径列表分割成一个一个的单独路径。
参数:*paths[]存放分割好的存放一组路径的数组,返回这组路径的个数。
◆ void ScanFile(char argv1[],char pathname[]):
功能:在从PATH分割出来的一组路径种,寻找argv1命令对应的文件,确定用户输入的命令文件的位置。
参数:argv1是用户输入的命令,pathname[]用来存放带有正确的路径的文件名,即“路径”+“/”+“命令名”。
◆ void Execute(char *[],int);
功能:执行没有特殊功能(即无重定向或管道)的用户命令。
参数:argv[]存放的是用户的命令和参数,argc是命令和参数的个数。
◆ void ExecuteF1(char *argv[],int argc):
功能:执行带输入或输出重定向功能的用户命令。
参数:argv[]存放的是用户的命令和参数,argc是命令和参数的个数。
◆ void ExecuteP1(char *argv[],int argc):
功能:执行代管道功能的用户命令。
参数:argv[]存放的是用户的命令和参数,argc是命令和参数的个数。
◆ void ExecuteCd(char argv1[]);
功能:执行cd命令,当有错误时显示错误信息(因为cd命令是shell自身实现的,并不是某个可执行文件)。
参数:argv[]存放的是cd 后面的参数
主函数功能: 设置一个循环体,在循环体内,首先获得当前路径,打印提示符,然后接受用户输入,get_string(_usrComm),判断输入是否是leave,若是,跳出循环;若否,调用split(argv,argc,_usrComm)解析用户输入,并将命令及参数信息填入argv以及argc,接着判断argc是否为0,即是否为空输入,若是,进行下一次循环;若不是,判断argv[0]是否为cd,若是,执行ExecuteCd(argv1[]),然后进行下一次循环;若否,根据标志位判断是否有重定向或管道,进行决定执行哪个函数(ExecuteF1,ExecuteP1, Execute)。然后进行下一次循环。
[ 主要功能设计说明 ]
程序完成的主要功能如下:
运行目标代码后,在屏幕上打印当前路径的提示符,当敲入命令行如:identifier[identifier[identifier]],就解析命令行参数指针数组argv[argc]。然后执行。其中,支持“cd”命令,支持空输入,支持多余空格,支持I/O重定向,支持管道,支持后台执行。下面分部分进行功能说明:
1.打印提示符:
每次接收一个新的命令之前,都要在屏幕上打印提示符,[current_dir_name],current_dir_name是当前路径。
2.接受用户输入并解析:
当用户输入一行命令然后回车时,接收这行字符串,然后进行解析,要把其中的命令和参数一一分离,然后填充到argv中,且要把命令和参数的个数赋给argc。同时,在分析过程中,如果遇到“<”、“>”、“|”、“&”这些标志,应该把相应的标志位变量赋上值。
4.执行命令
根据命令以及标志位的值,转到相应的执行处理模块。如果是cd命令,那么就转到执行cd命令的模块,因为cd命令是shell自己实现的。如果带有“>”、“<”,则转到带有输入输出重定向的模块执行。若带有“|”,则转到带有管道功能的模块执行。若没有这两个符号,则转到普通的执行命令模块。而“&”,后台执行,则是在具体执行时决定是否阻塞父进程。
在每个执行命令模块中,要能判断该命令是否可执行,即该命令文件是否存在,这就要利用环境变量。
执行命令(不包括cd)通过创建一个子进程来实现。
对于输入重定向,要能把输出流重定向到用户在“<”符号后面指定的文件中,即输出都流到这个文件中,如果该文件不存在,要创建它;对于输出重定向,要能把输入流重定向到“<”符号后面指定的文件中,即输入流均来自这个文件,输入文件必须存在,若不存在,给出提示。
对于管道,当以command1|command2形式出现时,command1的输出流将作为command2的输入流。即后一个进程的输入来自第一个进程的输出。故第二个进程要等到第一个进程执行完毕才可以往下进行。
对于并行执行,当没有这个符号时,每次接收并执行下一个用户命令时,都要等到上一个用户命令执行完毕,而当以“&”结束时,立即开始接收下一个命令,不必等到这个命令执行完毕,反映在程序中,就是父进程不必等到子进程结束返回就可以继续执行,不必为等待子进程而阻塞自己。
[ 程序设计实现说明 ]
分几个模块进行说明:
1.打印提示符:
通过系统内部定义好的函数get_current_dir_name()实现,返回一个指向字符串的指针,程序如下:
p=get_current_dir_name();
printf("[%s]",p);
这样既可以在屏幕上打印[当前路径]:
2.接受用户输入get_string(char command[]):
即通过一个循环,每次接收一个字符,直到接收到回车为止。
ch=getchar();
while(ch!='\n')
{ command[ptr++]=ch;
ch=getchar();
}
command[ptr]='\0';
3.解析用户输入命令split(char* argv[],char usrComm[]):
需要两个辅助子函数,一个是删除连续空格,即如果是空格,就一直向下分析。
int delet_space(int ptr,char usrComm[])
{ while(usrComm[ptr]==' ')
ptr++;
return ptr;
}
一个是得到一个单词,其中“>”、“<”、“|”、“&”都单独算一个单词,若遇到这些字符,则:直接得到一个单词,若是字母,数字或“_”、“/”、“.”等符号,就继续分析直到不是这些符号,之前的这些字符就组成一个单词。
int get_a_word(char *argv1,int ptr,char com[])
{ char temp1[20]={};
int i=0;
if(com[ptr]=='>'||com[ptr]=='<'||com[ptr]=='|'||com[ptr]=='&')//符号单独算一个单词
{ temp1[i++]=com[ptr++];
}
else
while((com[ptr]>='a'&&com[ptr]<='z')||(com[ptr]>='A'&&com[ptr]<='Z')||com[ptr]=='-'||com[ptr]=='_'||com[ptr]=='/'||com[ptr]=='.'||(com[ptr]>='0'&&com[ptr]<='9'))
{ temp1[i++]=com[ptr++];
}
temp1[i]='\0';//结束符
strcpy(argv1,temp1);
return ptr;//返回此时的分析位置
}
还有一个是设置后台标志的符号
int set_Background(char *argv[],int argc)
{ Bkg=-1;
if(strcmp(argv[argc-1],"&")==0)
//如果最后一个是“&”符号
{ Bkg=1;
argc=argc-1;
argv[argc]=NULL;
}
return argc;
}
split的实现方法如下:首先过滤掉前面的空格delet_space(int ptr,char usrComm[]),然后进入一个循环体,循环条件是命令未分析完。在循环体内,每次调用get_a_word(char *argv1,int ptr,char com[])获得一个单词,然后命令和参数个数增一,且过滤之后的空格,进入下一次循环。
int split(char* argv[],int argc,char usrComm[])
{ int ptr=0;
int i=0;
ptr=delet_space(ptr,usrComm);
//过滤前面多余空格
outfile=-1;infile=-1;Ppipe=-1; //标志位复位
while(usrComm[ptr]!='\0') //待分析串未结束
{ argv[i]=malloc(sizeof(char)*30);
ptr=get_a_word(argv[i],ptr,usrComm);
//得到一个单词
if(strcmp(argv[i],">")==0)
//置输出重定向的标志位,其值代表出现这个符号的单词的序号
outfile=i;
else
if(strcmp(argv[i],"<")==0)
//置输入重定向的标志位,其值代表出现这个符号的单词的序号
infile=i;
else
if(strcmp(argv[i],"|")==0)
//置管道的标志位,其值代表出现这个符号的单词的序号
Ppipe=i;
i++;
argc++; //个数增一
ptr=delet_space(ptr,usrComm);//过滤中间多余空格
}
if(argc>=1) //如果命令和参数个数大于1
argc=set_Background(argv,argc);
//设置&后台标志位
argv[i]=NULL; //最后一个赋空代表结束
return argc;
}
4.寻找命令文件,判断命令是否可执行:
为了判断命令是否可执行,在程序最前面首先要调用GetPath(char *paths[])得到路径数组,这里要用到系统定义好的函数getenv(),得到的是以一个字符串形式存在的路径列表,为了方便使用,要把它分解到以单个路径存在的数组中。因路径之间以“:”分割,所以可以利用一个循环,每次碰到“:”,就把之前的路径提取出来作为一个单独路径。
int GetPath(char *paths[])
{ char *pa;
int i=0;
int ptr=0;
int pn=0;
char temp[30]={};
pa=getenv("PATH"); //得到路径列表
while(pa[ptr]!='\0')
{ while(pa[ptr]!=':'&&pa[ptr]!='\0')
//未碰到“:”,且未结束
{ temp[i++]=pa[ptr++];
}
temp[i]='\0';
paths[pn]=malloc(sizeof(temp));
strcpy(paths[pn],temp); //放到paths数组中
if(pa[ptr]!=’\0’) //如果未结束
ptr++;
i=0;
pn++;
}
}
return pn;
}
然后当要寻找命令文件时,依次将每个路径和argv[0]组合判断这个文件是否存在,若存在,当前这个路径和文件名就是该文件的位置。
char* ScanFile(char argv1[],char pathname[])
{ int i=0;
int p=0;
for(i=0;i<20;i++)
pathname[i]=' ';
for(i=0;i
{ strcpy(pathname,paths[i]); //放入路径名
strcat(pathname,"/");
strcat(pathname,argv1); //放入文件名
if(access(pathname,F_OK)==0)
return pathname; //返回该带路径的文件名
}
printf("No such command!\n"); //若不存在
return NULL; //返回空
}
5.执行命令:
通过调用fork()创建一个子进程,在进程中调用函数execv执行命令。对于execv(pathname,argv),pathname是带完整路径的文件名,argv是存放命令和参数的数组,以NULL代表结束。调用waitpid()阻塞父进程等待子进程的完成。下面具体介绍这三个系统调用:
实现执行没有特殊功能的用户命令:
void Execute(char *argv[],int argc)
{ char pathname[20]={""};
if(ScanFile(argv[0],pathname)==NULL)
//判断命令文件是否存在
return ; //不可执行返回
pid_t pid4=fork(); //创建一个子进程,ID号为pid4
if(pid4==0)
execvp(argv[0],argv); //子进程执行用户输入命令
else
if(Bkg!=1) //若不是后台执行
waitpid(pid4,NULL,0);
//阻塞父进程,等待ID号为pid4的子进程结束
}
6.输入输出重定向的实现:
通过操纵子进程的文件描述符来重定向I/O。一个新创建的子进程将继承其父进程的打开文件描述符,特别是同样将键盘作为stdin以及将终端显示器作为stdout和stderr。每个进程在内核中都有自己的文件描述符表,其中stdin绑定到第0项,stdout绑定到第1项,stderr绑定到第2项。由于open(),dup()总是使用文件描述符中最早可用的表项.
其中:int dup (int oldfd),用来复制参数oldfd所指的文件描述词,并将它返回。此新的文件描述词和参数oldfd指的是同一个文件,共享所有的锁定、读写位置和各项权限或旗标。
这样,可以通过关闭我们想要重定向的流文件,然后利用dup()重新定向到用户制定的文件中。
void setOutFile(char outFile[]) //设置输出重定向
{ int fid=open(outFile,O_WRONLY|O_CREAT); //打开用户指定文件
close(1); //关闭stdout
dup(fid); //将stdout重定向到fid中
close(fid); //关闭fid
}
void setInFile(char inFile1[]) //设置输入重定向
{ int fid2=open(inFile1,O_RDONLY);
//打开用户指定文件
close(0); //关闭stdin
dup(fid2); //将stdin重定向到fid中
close(fid2); //关闭fid2
}
利用这两个函数,就可以实现带有输入输出重定向的用户命令的执行,只要在上文提到的Execute函数中pid==0后面的代码部分有所变化。另外,在输入重定向之前,要对输入文件是否存在进行检测:
void ExecuteF1(char *argv[],int argc)
{ char pathname[20]={""};
char *p;
p=get_current_dir_name();
if(ScanFile(argv[0],pathname)==NULL)
return ;
if(infile>=0) //若有输入重定向
{ strcat(p,"/");
strcat(p,argv[infile+1]); //得到输入文件完整路径名
if(access(p,F_OK)!=0) //检测该文件是否存在
{ printf("No such File!\n");
return;
}
}
pid_t pid1=fork();
if(pid1==0)
{ if(outfile>=0)
{ setOutFile(argv[outfile+1]);
//调用输出重定向文件设置
argv[outfile]=NULL;
}
if(infile>=0)
{
setInFile(argv[infile+1]);
//调用输入重定向文件设置
argv[infile]=NULL;
}
execv(pathname,argv);
}
else
if(Bkg!=1)
waitpid(pid1,NULL,0);
}
这样就可以实现带有“>”、“<”符号的命令的执行,包括有一个“>”或“<”以及既有“>”又有“<”的命令的执行。
6.管道的实现:
通过函数int pipe(int filedes[2]),pipe()会建立管道,并将文件描述词由参数filedes数组返回。filedes[0]为管道里的读取端,filedes[1]则为管道的写入端。若成功则返回零,否则返回-1,错误原因存于errno中。
这样,通过创建两个进程,第一个子进程执行管道符前的命令,并将输出重定向到管道的写端,第二个进程执行管道符后的指令,并将输入重定向到管道的读端,从管道读入。
void ExecuteP1(char *argv[],int argc)
{ char pathname1[20]={""};
char pathname2[20]={""};
if(ScanFile(argv[0],pathname1)==NULL)
return ; //判断第一个进程命令文件是否存在
if(ScanFile(argv[Ppipe+1],pathname2)==NULL)
return ; //判断第二个进程命令文件是否存在
//因为是两个命令要执行,所以两个文件都要查看是否存在。
int A_to_B[2];
pipe(A_to_B); //创建管道
pid_t pid2=fork(); //创建第一个子进程
if(pid2==0)
{ argv[Ppipe]=NULL;
//该位置命令置空,代表命令参数结束
close(A_to_B[0]); //关闭管道的读端
close(1); //关闭stdout
dup(A_to_B[1]);
//将stdout重定向到管道写端
execv(pathname1,argv); //执行
}
else
{ waitpid(pid2,NULL,0);
//等待第一个进程执行完毕
close(A_to_B[1]); //关闭管道写端
}
pid_t pid3=fork(); //创建第二个子进程
if(pid3==0)
{ int i,p;
for(i=Ppipe+1,p=0;i
//调整命令参数,将第二个进程的命令和参数前移, //方便execv使用
{ strcpy(argv[p],argv[i]);
}
argv[p]=NULL;
//该位置命令置空,代表命令参数结束
close(A_to_B[1]); //关闭管道的写端
close(0); //关闭stdin
dup(A_to_B[0]);
//将stdin重定向到管道读端
execv(pathname2,argv); //执行
}
else
{ if(Bkg!=1)
//若不是后台执行
waitpid(pid3,NULL,0);
//阻塞父进程,等待ID号为pid3的子进程结束
close(A_to_B[0]); //关闭管道读端
}
}
7.cd命令的实现:
通过调用系统函数int chdir(const char * path),chdir()用来将当前的工作目录改变成以参数path所指的目录。执行成功则返回0,失败返回-1,errno为错误代码。
void ExecuteCd(char argv1[])
{ int res=0;
if(argv1!=NULL)
res=chdir(argv1);
else
printf("Change dir error!\n");
if(res<0)
printf("Change dir error!\n");
return;
}