Linux下的Shell程序设计
我们需要设计一个shell程序,目前实现的功能有如下:
- 获取当前操作系统的用户名、当前路径、以及操作权限展示为提示符
- 输入一系列的内部指令并执行:cd、exit、help以及无效指令
- 输入一系列的外部指令并执行:ps、ls执行当前路径文件等
- 利用管道标识符“||”将标识符的命令输出的内容作为标识符后面的命令的输入内容
- 利用 < 和 >进行输入输出的重定向
- 循环执行命令
下面将逐步拆解shell程序讲解其编写
首先我们需要定义我们的shell程序的运行逻辑如下:
1.开启shell程序进程
2.进入执行循环
2.1获取操作系统的用户名、当前路径、操作权限并作为提示符输出
2.2接收一整行的输入
2.3将一整行的输入进行拆解,以空格为分隔符将一整行命令拆解成一个命令数组
2.4分析命令数组,判断是否需要使用管道、重定向
2.5执行命令数组并输出
2.6结束循环
3.程序结束
在决定了我们程序的框架之后就可以进入程序功能的编写了
获取当前操作系统的用户名、当前路径、以及操作权限展示为提示符
Linux的命令行提示符由以下几个部分组成:
操作系统用户名@主机名 相对路径#($)
所以我定义我自己的Shell程序的提示符如下:
操作系统用户名@KlayShell 绝对路径
当为root权限用户是结束符为#
当为普通用户时结束符为$
我们首先需要用geteuid()获取当前的用户id
然后利用用户id作为参数使用getpwuid(uid uid)函数
getpwuid(uid uid)会返回以下的结构体
struct passwd
{
char *pw_name; /* 用户名*/
char *pw_passwd; /* 密码.*/
__uid_t pw_uid; /* 用户ID.*/
__gid_t pw_gid; /*组ID.*/
char *pw_gecos; /*真实名*/
char *pw_dir; /* 主目录.*/
char *pw_shell; /*使用的shell*/
};
这样子我们就可以获取到当前的用户名了
其次我们还需要获取到当前的绝对路径
getcwd()会将当前工作目录的绝对路径复制到参数buffer所指的内存空间中,参数size为buffer的空间大小
其次我们还需要根据之前获取到的用户id选择输出#还是输出$
这部分完整的代码如下:
void print_prompt(){
struct passwd *myinfo;
//获取用户id
int euid= geteuid();
//获取用户信息结构体
myinfo = getpwuid(euid);
//获取主机名(这个舍弃掉了,被置换为我自己定义的名字)
char hostname[80];
gethostname(hostname,sizeof(hostname));
//获取绝对路径
char path[400];
getcwd(path,sizeof(path));
//输出命令提示符
printf("[%s@KlayShell:%s]",myinfo->pw_name,path);
//根据root权限输出
if(euid==0){
printf("# ");
}
else{
printf("$ ");
}
}
得到的结果:

接收一整行的输入并根据空格符进行分割
我们定义两个全局变量,分别用于接收整行输入以及拆分后的输入
//存放输入的命令
char inputCommand[400];
//存放分解后的命令
char command[30][50];
我们直接利用gets(inputCommand)接收一整行的输入
然后利用以下函数根据空格对指令进行切割,并且返回指令的长度
int breakInput2Command(){
char *s = inputCommand;
int i = 0;int j = 0;
while(1){
if((*s)=='\0'){
command[i][j] = '\0';
break;
}
//当前字符为空格
if(isspace(*s)){
command[i][j] = '\0';
i++;
j=0;
}
//不是空格
else{
command[i][j] = *s;
j++;
}
s++;
}
return i+1;
}
原来的指令:
分割后的指令:
分析内部指令
首先把内部指令的字符定义在静态变量中
//静态变量存储内部命令
static char COMMAMD_HELP[] = "help";
static char COMMAND_EXIT[] = "exit";
static char COMMAND_CD[] = "cd";
然后利用分割后的第一个单词进行strcmp判断
- 当为退出时,直接打印一句后并退出
- 当为help时,程序打开一个本地文件,并将本地文件内的帮助信息输出
- 当为cd时,首先会检查输入的命令单词个数,如果命令单词个数大于或者少于2个都会提示错误
如果输入单词个数为2个,程序会根据第二个参数作为cd的路径调用chdir(const char * path)执行
如果这些命令都不符合,就会作为外部命令执行
这部分的代码如下:
void run_command(int count){
if(strcmp(command[0],COMMAND_EXIT)==0){
printf("Exit the programme\n");
exit(0);
}
else if(strcmp(command[0],COMMAMD_HELP)==0){
do_help();
}
else if(strcmp(command[0],COMMAND_CD)==0){
do_cd(count);
}
else{
//外部命令
do_outside_command_with_pipe(count);
}
}
打印帮助信息
void do_help(){
FILE * fp = fopen("HelpDocument","r");
int ret;
while(1){
ret = fgetc(fp);
if(feof(fp)){
break;
}
fputc(ret,stdout);
}
}
执行cd命令
void do_cd(int count){
if(count>2){
printf("CD_ERROR:参数过多,请重新输入\n");
}else if(count<2){
printf("CD_ERROR:参数过少,请重新输入\n");
}
else{
int ret = chdir(command[1]);
if(ret!=0){
printf("CD_ERROR:不正确的路径参数\n");
}
}
}
该部分结果如下:
Help

Cd

Exit

执行普通外部命令
执行普通的外部命令的方式比较简单
只需要调用execvp()函数即可
execvp()会从PATH 环境变量所指的目录中查找符合参数file
的文件名,找到后便执行该文件,然后将第二个参数argv传给该欲执行的文件。 返回值
如果执行成功则函数不会返回,执行失败则直接返回-1,失败原因存于errno中。
其中需要注意的时执行execvp命令时需要新建一个新的进程去执行
这部分的代码跟后面的代码结合
执行重定向输入和输出
要实现重定向输入输出首先需要我们识别出重定向符号然后再进行命令切割
根据<获取到重定向输入文件,根据>获取到重定向输出文件
然后利用freopen()函数对这些文件进行重定向输入输出
然后再根据我们的重定向符号对命令进行切割,切割不包含重定向符号和重定向文件的命令再调用execvp()函数执行
这部分的代码如下:
void do_outside_command(int start,int count){
//输入重定向符号个数
int in = 0;
//输出重定向符号个数
int out = 0;
int end = count;
char *FILEINPUT;
char *FILEOUTPUT;
int i;
for(i=start;i<count;i++){
if(strcmp(command[i],COMMAMD_REINPUT)==0){
in++;
FILEINPUT = command[i+1];
if(strlen(FILEINPUT)==0){
printf("缺少输入重定向文件\n");
return;
}
if(i<end){
end = i;
}
}
else if(strcmp(command[i],COMMAMD_REOUTPUT)==0){
out++;
FILEOUTPUT = command[i+1];
if(strlen(FILEOUTPUT)==0){
printf("缺少输出重定向文件\n");
return;
}
if(i<end){
end = i;
}
}
}
pid_t pid;
pid = fork();
if(pid<0){
perror("创建子进程失败\n");
exit(0);
}
//在子进程中处理外部命令
else if(pid==0){
if(in==1){
//将输入重定向到输入文件
freopen(FILEINPUT,"r",stdin);
}
if(out==1){
//将输出重定向到输出文件
freopen(FILEOUTPUT,"w",stdout);
}
//定义一个命令变量来切割暂存命令
char *commtemp [40];
int j=0;
int i;
for( i = start;i<end;i++){
commtemp[j]=command[i];
j++;
}
commtemp[j] = NULL;
execvp(commtemp[0],commtemp);
exit(0);
}
else {
//等待子进程执行完毕
waitpid(pid,NULL,0);
}
}
执行如下:
输出重定向:

输入重定向:

实现管道功能
在实现管道功能之前,我们需要先识别出管道符号,并返回管道符号的位置
//获取管道符号的位置
int get_pipe_position(int count){
int i;
for(i=0;i<count;i++){
if(strcmp(command[i],COMMAMD_PIPE)==0){
return i;
}
}
return 0;
}
然后创建一个子进程在子进程中创建一个管道,让子进程创建一个孙进程,孙进程执行命令并将输入作为管道输入,子进程接收管道的输出,作为标准输入执行命令
代码如下:
void do_outside_command_with_pipe(int count){
int pipePosition = get_pipe_position(count);
pid_t pid1 = fork();
if(pid1<0){
perror("PIPE_ERROR:无法创建子进程\n");
return;
}
//在子进程中进行执行
else if(pid1==0){
//有管道符号
if(pipePosition>0){
int fd[2];
int ret = pipe(fd);
pid_t pid = vfork();
if(pid<0){
perror("PIPE_ERROR:无法创建子进程\n");
return;
}
else if(pid==0){
//子进程关闭读端
close(fd[0]);
//将输出作为管道输入
dup2(fd[1],1);
close(fd[1]);
do_outside_command(0,pipePosition);
exit(0);
}
else{
waitpid(pid,NULL,0);
//父进程关闭写端
close(fd[1]);
//将管道的输出作为标准写入
dup2(fd[0],0);
close(fd[0]);
do_outside_command(pipePosition+1,count);
exit(0);
}
}
else{
do_outside_command(0,count);
}
}
else{
waitpid(pid1,NULL,0);
}
}
代码执行:

以上就是Shell程序设计的全部过程,完整的代码已经上传到github