文章目录
- 一、bash是什么
- 二、bash工作原理
- 三、项目实现mybash
- 1.项目框架
- 2.项目伪代码
- 3.半成品代码
- 先看代码
- 代码解释
- 4.优化代码
- 5.完整mybash代码实现
- 四、其他常用命令的实现
- 1.myclear清屏的实现
- 2.mypwd的实现
- 3.myls的实现
一、bash是什么
命令解释器:收到命令后解析它然后执行
用户运行./main、ls等命令时,就是交付给bash运行,当其结果呈现给用户
二、bash工作原理
先fork自己一份,复制出来一份子进程,然后子进程替换成要执行的命令。不断复制自身,然后用exec执行程序
三、项目实现mybash
1.项目框架
要先写个死循环,不执行exit退出就一直在bash内进行死循环
循环内执行步骤:
(1)获取提示符信息,并打印:stu@stu-vertual-machin:~.....
(2)获取命令:fegts(buff);//从键盘上获取个命令,把命令放到Buff内
(3)分割命令 :分割命令和参数,如下所示,将cp a.c b.c进行分割
- cp a.c b.c
- cp->argv[0]
- a.c->argv[1]
- b.c->argv[2]
分割完,cmd接收命令:char * cmd=myargv[0];//获取命令 cmd接收
(4)进行命令分类
命令有2类分类(内置命令和普通命令),如下:
- 内置命令:
- cd、exit
- 通过which找不到
普通命令:
除内置命令(cd、exit)外的命令
通过which可以找到,如下所示,ls,ps就是普通命令,cd是内置命令
- 普通命令一般子进程执行fork()+exec,内置命令由父进程内执行
- 代码就如下所示:
if(内置命令)//内置命令,父进程内执行
{....}
else//普通命令,子进程执行
{ fork()+exec//普通命令,谁执行都可以....}
2.项目伪代码
3.半成品代码
目前代码可以运行出ls,pwd,cd的结果,但是在输出提示符信息处选择的方式是打印出固定信息,这个可以进行进一步优化——自动打印,会在下面进行介绍。
先看代码
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<string.h>
#include<sys/wait.h>
#include<sys/types.h>
#include<pwd.h>
#define ARG_MAX 10
//分割命令
char *get_cmd(char* buff,char *myargv[])
{
if(buff==NULL||myargv==NULL)
{
return NULL;
}
char * s=strtok(buff," ");
int i=0;
while(s!=NULL)
{
myargv[i++]=s;
s=strtok(NULL," ");
}
return myargv[0];
}
void run_cmd(char *file,char* myargv[])
{
if(file==NULL||myargv==NULL)
{
return ;
}
pid_t pid=fork();
if(pid==-1)
{
printf("fork error");
return;
}
if(pid==0)//子进程内执行execvp
{
//替换
execvp(file,myargv);//file也就是主函数内的cmd,它的文件路径系统会自己找
//如果没找到,失败就会执行下面代码:
perror("execvp error\n");
exit(0);//失败的话,子进程必须要退出
//因为子进程不退出,会在main内执行程序打印提示符
}
else//父进程防止僵死进程
{
wait(NULL);//防止僵死
}
}
int main()
{
while(1)
{
//先把信息写死:
printf("stu@stu-virtual-machine:~/桌面/223");
//把信息自动输出调用该函数:printf_info();后面介绍
fflush(stdout);//刷新
//键盘输入命令
char buff[128]={0};
fgets(buff,128,stdin);
buff[strlen(buff)-1]=0;//把最后一个置零
//因为输入命令之后再输入回车运行时,回车符号会被记录
//对命令进行识别
char *myargv[ARG_MAX]={0};
char *cmd=get_cmd(buff,myargv);//放在buff内
if(cmd==NULL)//敲回车,什么也不执行,进入下一次循环
{
continue;
}
//执行命令
//内置命令,在父进程执行
else if(strcmp(cmd,"exit")==0)
{
break;//退出大循环
}
else if(strcmp(cmd,"cd")==0)
{
if(myargv[1]!=NULL)//保证cd后面有东西 不能命令只有cd
{
if(chdir(myargv[1])==-1)//出错
{
perror("cd error");
}
}
}
else//普通命令,子进程内执行
{
//fork()+exec
run_cmd(cmd,myargv);
}
}
exit(0);
}
代码解释
代码段一:
//分割命令
char *get_cmd(char* buff,char *myargv[])
{
if(buff==NULL||myargv==NULL)
{
return NULL;
}
char * s=strtok(buff," ");
int i=0;
while(s!=NULL)
{
myargv[i++]=s;
s=strtok(NULL," ");
}
return myargv[0];
}
get_cmd函数的功能是实现对命令进行分割,其中需要用到strtok()函数。
- 该函数的用法是用于分割字符串,如下图所示,将a/bbb///cc分割为‘a’,'bbb','ccc'
- 需要引用的头文件:#include<string.h>
- 该函数的使用
- 函数在使用的时候只需要指向buff一次,因为该函数内部自定义了一个指针,专门用来记录把东西分割在哪里了,剩下的几次使用只用传个空NULL进行占位就行,内部指针已经帮我们定位到下一个字符的位置,注意需要用空隔开。如下所示:
char * s=strtok(buff," ");//分割buff数组内内容,以空格符做第一次分割 s=strtok(NULL," "); s=strtok(NULL," "); s=strtok(NULL," ");//最后一次调用时,s=NULL
代码段二:
void run_cmd(char *file,char* myargv[])
{
if(file==NULL||myargv==NULL)
{
return ;
}
pid_t pid=fork();
if(pid==-1)
{
printf("fork error");
return;
}
if(pid==0)//子进程内执行execvp
{
//替换
execvp(file,myargv);//file也就是主函数内的cmd,它的文件路径系统会自己找
//如果没找到,失败就会执行下面代码:
perror("execvp error\n");
exit(0);//失败的话,子进程必须要退出
//因为子进程不退出,会在main内执行程序打印提示符
}
else//父进程防止僵死进程
{
wait(NULL);//防止僵死
}
}
该函数的功能是在子进程中执行普通命令。上面讲到,普通命令一般就是在子进程执行fork()+exec来完成执行。因此,先复制出子进程,然后在子进程中使用execvp()函数替换进程,子进程开始执行,file对应主函数的cmd,系统会自己查找file文件所在路径,找到后就在该路径下执行myargv[],找不到打印下面的错误提示,然后子进程退出。
代码段三:
else if(strcmp(cmd,"cd")==0)
{
if(myargv[1]!=NULL)//保证cd后面有东西 不能命令只有cd
{
if(chdir(myargv[1]==-1))//出错
{
perror("cd error");
}
}
}
该代码是实现cd命令。需要引入对chdir系统调用和getcwd函数的知识:
- chdir函数的作用是浏览目录
- 详情点击该链接:【Linux】文件与目录的底层系统调用_又秃又弱的博客-CSDN博客
如果只输入cd命令则不执行,因此myargv[1]!=NULL,保证不只是输入cd命令,确保命令后面有东西(就是输入cd ..或者cd+文件目录名等) ,然后调用chdir进行浏览切换目录。
4.优化代码
此处介绍,如何不把提示写死,写成可自动输出的?
修改printf代码为printf_info:
//先把信息写死
printf("stu@stu-virtual-machine:~/桌面/223");//stu@localhost ~$");//$普通用户
printf_info();
//用户名+@+主机名+路径+普通用户($)/管理员(#)
做法:
1.获取用户名
getpwuid(),通过把uid传给getwuid(),函数返回一个包含用户名信息的结构体。
2.获取主机名hostname
调用gethostname()函数,失败返回-1
3.判断用户是管理员还是普通用户
uid=0,则为root用户,用getuid()获取uid的值
用user_str存放"#"或者"$"
4.得到路径getcwd(),getcwd是确定当前工作目录,详情点击链接:【Linux】文件与目录的底层系统调用_又秃又弱的博客-CSDN博客
代码
#include<sys/types.h>
#include<pwd.h>
void printf_info()
{
char *user_str="$";
int user_id=getuid();//得到uid
if(user_id==0)
{
user_str="#";
}
struct passwd*ptr=getpwuid(user_id);//用户名
if(ptr==NULL)//判断getwuid的返回值
{
printf("mybash1.0>> ");//失败就打印mybash版本号
fflush(stdout);//刷新
return;
}
//主机名
char hostname[128]={0};
if(gethostname(hostname,128)==-1)
{
printf("mybash1.0>> ");//失败就打印mybash版本号
fflush(stdout);
return ;
}
//得到路径
char dir[256]={0};
if(getcwd(dir,256)==NULL)
{
printf("mybash1.0>> ");//失败就打印mybash版本号
fflush(stdout);
return ;
}
printf("%s@%s %s%s ",ptr->pw_name,hostname,dir,user_str);
fflush(stdout);
}
此时,运行可得到如下所示,第四行为mybash打印出来的命令提示,和前三行系统自动出来的已经时一摸一样了,唯一就是颜色不同。
下面介绍如何修改颜色:
这个不太算重点,参照这篇文章照着写下就好:printf 颜色,关于printf如何输出颜色_XY LIU的博客-CSDN博客
举例:其中\033 开头,1是高亮,32是绿色,34是蓝色,结尾需要\033[0m关闭所有属性:
printf("%s@%s %s%s ",ptr->pw_name,hostname,dir,user_str);
printf("\033[1;32m%s@%s\0330m \033[1;34m%s%s\033[0m ",ptr->pw_name,hostname,dir,user_str);
运行结果:
5.完整mybash代码实现
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<string.h>
#include<sys/wait.h>
#define ARG_MAX 10
#include<sys/types.h>
#include<pwd.h>
//输出提示信息stu@stu-virtual-machine:...
void printf_info()
{
char *user_str="$";
int user_id=getuid();//得到uid
if(user_id==0)
{
user_str="#";
}
struct passwd*ptr=getpwuid(user_id);//用户名
if(ptr==NULL)//判断getwuid的返回值
{
printf("mybash1.0>> ");//失败就打印mybash版本号
fflush(stdout);
return;
}
//主机名
char hostname[128]={0};
if(gethostname(hostname,128)==-1)
{
printf("mybash1.0>> ");//失败就打印mybash版本号
fflush(stdout);
return ;
}
//得到路径
char dir[256]={0};
if(getcwd(dir,256)==NULL)
{
printf("mybash1.0>> ");//失败就打印mybash版本号
fflush(stdout);
return ;
}
printf("\033[1;32m%s@%s\033[0m \033[1;34m %s\033[0m%s ",ptr->pw_name,hostname,dir,user_str);
fflush(stdout);
}
//分割命令
char *get_cmd(char* buff,char *myargv[])
{
if(buff==NULL||myargv==NULL)
{
return NULL;
}
char * s=strtok(buff," ");
int i=0;
while(s!=NULL)
{
myargv[i++]=s;
s=strtok(NULL," ");
}
return myargv[0];
}
void run_cmd(char *file,char* myargv[])
{
if(file==NULL||myargv==NULL)
{
return ;
}
pid_t pid=fork();
if(pid==-1)
{
printf("fork error");
return;
}
if(pid==0)
{
//替换
execvp(file,myargv);
//myargv[0]->path->cmd
//如果失败会执行下面代码:
perror("execvp error\n");
exit(0);//失败的话,子进程必须要退出
//因为子进程不退出会在main内执行程序打印提示符
}
else
{
wait(NULL);//防止僵死
}
}
int main()
{
while(1)
{
//把信息写死:printf("stu@stu-virtual-machine:~/桌面/223");
//把信息自动输出:
printf_info();
fflush(stdout);//刷新
//键盘输入命令
char buff[128];
fgets(buff,128,stdin);
buff[strlen(buff)-1]=0;//把最后一个置零
//因为输入命令之后再输入回车运行时,回车符号会被记录
//对命令进行识别
char *myargv[ARG_MAX]={0};
char *cmd=get_cmd(buff,myargv);//放在buff内
if(cmd==NULL)//敲回车,什么也不执行,进入下一次循环
{
continue;
}
//执行命令
//内置命令,在父进程执行
else if(strcmp(cmd,"exit")==0)
{
break;//退出大循环
}
else if(strcmp(cmd,"cd")==0)
{
if(myargv[1]!=NULL)//保证cd后面有东西 不能命令只有cd
{
if(chdir(myargv[1])==-1)//出错
{
perror("cd error");
}
}
}
else//普通命令,子进程内执行
{
//fork()+exec
run_cmd(cmd,myargv);
}
}
exit(0);
}
四、其他常用命令的实现
mybash.c执行时,命令是系统里面的,是在系统内部查找ps,ls,pwd等等命令,下面就讲解如何程序员自己写出ps,pwd等等这些常用命令。
1.myclear清屏的实现
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
int main()
{
printf("\033[2J");
}
清屏+移动光标
printf("\033[2J\033[0H");
2.mypwd的实现
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
int main()
{
char path[256]={0};
if(getcwd(path,256)==NULL)
{
perror("getcwd error");
exit(1);
}
printf("%s\n",path);
exit(0);
}
执行结果:
3.myls的实现
gcc -o ls ls.c
#include<dirent.h>
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
int main()
{
char path[256]={0};
if(getcwd(path,256)==NULL)
{
perror("getcwd error");
exit(1);
}
//得到目录流
DIR *pdir=opendir(path);
if(pdir==NULL)
{
perror("getcwd error");
exit(1);
}
//读目录流信息
/*读取目录流信息,指针s接收,打印 d->name文件名*/
struct dirent *s=NULL; //定义结构体指针s
while((s=readdir(pdir))!=NULL)
{
printf("%s ",s->d_name); //输出文件名
}
printf("\n");
closedir(pdir);
exit(0);
}
执行结果:
在执行myls时和系统的ls执行结果对比,在myls输出结果中会看到——当前位置"." 和上层位置" .."的两个隐藏文件(输入ls -a可以显示出隐藏文件)
如何在myls中也实现将这两个文件也隐藏起来?
答:如下代码所示
strncmp(s->d_name,".",1)==0)//比较s->d_name中第1个值与“.”相等,就不输入printf
{
continue;
}
#include<dirent.h>
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<string.h>
int main()
{
...
//读目录流信息
struct dirent *s=NULL; //定义结构体指针s
while((s=readdir(pdir))!=NULL)
{
strncmp(s->d_name,".",1)==0)
//比较s->d_name中第1个值与“.”相等,就不输入printf
{
continue;
}
printf("%s ",s->d_name); //输出文件名
}
printf("\n");
closedir(pdir);
exit(0);
}
运行结果
此时就没有再显示出隐藏文件了,但是myls和ls还有个不一样就是文件颜色,如何把myls的文件颜色也标记出来呢?
仔细观察ls标记的颜色可以发现,如下文件类型和颜色的关系:
目录文件-->蓝色;普通文件有执行权限-->绿色;普通文件没有执行权限-->黑色
使用stat进行文件类型判断
stat:
- 头文件:include<sys/stat.h>
是否是目录:S_ISDIR(m)
普通文件:S_ISREG(m)
因此在输出文件的时候,使用stat函数对文件类型进行判断,不同类型的文件用不同颜色输出。补充代码如下:
int main()
{
...
struct dirent *s=NULL; //定义结构体指针s
while((s=readdir(pdir))!=NULL)
{
...
//printf("%s ",s->d_name);//d_name是文件名
struct stat filestat;
stat(s->d_name,&filestat);
if(S_ISDIR(filestat.st_mode))//是目录文件,则打印蓝色,st_mode是结构体包含了一些文件信息
{
printf("\033[1;34m%s\033[0m ",s->d_name);
}
//普通带执行权限的文件
else
{
if(filestat.st_mode&(S_IXUSR|S_IXGRP|S_IXOTH))//状态信息,带执行权限
{
printf("\033[1;32m%s\033[0m ",s->d_name);
}
else
{
printf("%s ",s->d_name);
}
}
}
printf("\n");
closedir(pdir);
}
完整版本myls代码
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<string.h>
#include<dirent.h>
#include<sys/stat.h>
int main()
{
char path[256]={0};
if(getcwd(path,256)==NULL)
{
perror("getcwd error");
exit(1);
}
DIR* pdir=opendir(path);
if(pdir==NULL)
{
perror("opendir error");
exit(1);
}
struct dirent * s=NULL;
while((s=readdir(pdir))!=NULL)
{
if(strncmp(s->d_name,".",1)==0)
{
continue;
}
//printf("%s ",s->d_name);
struct stat filestat;
stat(s->d_name,&filestat);
if(S_ISDIR(filestat.st_mode))//是目录文件,则打印蓝色
{
printf("\033[1;34m%s\033[0m ",s->d_name);
}
//普通带执行权限的文件
else
{
if(filestat.st_mode&(S_IXUSR|S_IXGRP|S_IXOTH))//状态信息,带执行权限
{
printf("\033[1;32m%s\033[0m ",s->d_name);
}
else
{
printf("%s ",s->d_name);
}
}
}
printf("\n");
closedir(pdir);
}
运行结果
myls和系统的ls输出结果如下: