🔥个人主页: Milestone-里程碑
❄️个人专栏: <<力扣hot100>> <<C++>><<Linux>>
🌟心向往之行必能至
目录
前言:分析shell的实现原理
例子
bash
[lcb@hcss-ecs-1cde ~]$ ls
1 code install.sh
[lcb@hcss-ecs-1cde ~]$ ll
total 12
drwxrwxr-x 3 lcb lcb 4096 Dec 13 23:21 1
drwxrwxr-x 3 lcb lcb 4096 Dec 13 23:23 code
-rw-rw-r-- 1 lcb lcb 827 Dec 12 17:02 install.sh
[lcb@hcss-ecs-1cde ~]$ ps
PID TTY TIME CMD
18153 pts/0 00:00:00 bash
18174 pts/0 00:00:00 ps
⽤下图的时间轴来表⽰事件的发⽣次序。其中时间从左向右。shell由标识为sh的⽅块代表,它随着时 间的流逝从左向右移动。shell从⽤⼾读⼊字符串"ls"。shell建⽴⼀个新的进程,然后在那个进程中运⾏ls程序并等待那个进程结束。

然后shell读取新的⼀⾏输⼊,建⽴⼀个新的进程,在这个进程中运⾏程序 并等待这个进程结束。
所以要写⼀个shell,需要循环以下过程:(使用子进程执行,与我们前面提到的王婆故事类似,保护shell父进程)
- 获取命令⾏
- 解析命令⾏
- 建⽴⼀个⼦进程(fork)
- 替换⼦进程(execvp)
- ⽗进程等待⼦进程退出(wait)
获取命令行
一.获取命令行的各个数据
bash
[lcb@hcss-ecs-1cde 6-myshell]$
仔细看这个,我们会发现,分为4部分,用户名+主机名+工作用户+$
根据我们前面学过的环境变量,学过了获取这个的接口,此处直接使用
bash
const char * GetHomeName()
{
22 char*homename = getenv("HOSTNAME");
23 return homename == NULL?"none":homename;
24 }
25 const char* GetUser()
26 {
27 char * user=getenv("USER");
28 return user==NULL?"none":user;
29 }
30 const char*GetPwd()
31 {
32 char*Pwd=getenv("PWD");
33 return Pwd==NULL?"node":Pwd;
34 }
我们将每个获取都写成一个函数,让所写代码逻辑更加清晰,且如果后续需要使用,直接调用即可,方便
二.打印命令行
此处需要注意,不能直接使用printf,而是使用snprintf
两个目的
1.杜绝缓冲区溢出
2.一次格式化多段信息,无需再strcat等等
bash
void MakeCommand( char*cmd_promt,int size)
45 {
47 snprintf(cmd_promt,size,FORMAT,GetHomeName(),GetUser(),GetPwd());
48 }
2. 打印命令行
50 void PrintCommandline()
51 {
52 char promt[MAXARGC];
53 MakeCommand(promt,sizeof(promt));
54 printf("%s",promt);
55 fflush(stdout);
56 }
此处的MAXARGC与FORMAT,我们直接宏定义,这样方便后续一键修改
bash
14 #define FORMAT "[%s@%s %s]#"
15 #define MAXARGC 128
打印结果
bash
[lcb@hcss-ecs-1cde 6-myshell]$ make
g++ -o myshell myshell.cc
[lcb@hcss-ecs-1cde 6-myshell]$ ./myshell
[hcss-ecs-1cde@lcb /home/lcb/code/linux/first/6-myshell]#ll
再仔细观看,会发现我们的工作目录都是绝对完善的,而Linux的工作目录只是当前的
优化:再对GerPwd的返回结果套上一层函数,进行切割
bash
string DirName(const char *pwd)
36 {
37 #define SLASH "/"
38 std::string dir = pwd;
39 if(dir == SLASH) return SLASH;
40 string::size_type pos = dir.rfind(SLASH);
41 if(pos == std::string::npos) return "BUG?";
42 return dir.substr(pos+1);
43 }
44 void MakeCommand( char*cmd_promt,int size)
45 {
46 snprintf(cmd_promt,size,FORMAT,GetHomeName(),GetUser(),DirName(GetPwd()).c_str());
47 // snprintf(cmd_promt,size,FORMAT,GetHomeName(),GetUser(),GetPwd());
48 }
bash
[lcb@hcss-ecs-1cde 6-myshell]$ ./myshell
[hcss-ecs-1cde@lcb 6-myshell]#ll
但为了我们分清后续的命令行是OS的shell还是我们自定义的shell,所以此处我们还是不对工作目录进行切割
三.得到命令符
如果命令符只是ls,那还好,但经常还会接入 -l -s等等,中间有空格隔开
我们知道scanf与cin会以空格为结束符,所以此处不可用,我们使用fgets
此处将argv与argc定义为全局变量,是为了后续的其他函数使用
bash
7 char * argv[MAXARGC];
18 int argc = 0;
bash
bool Getcommadchar(char*out,int size)
58 {
59 char *c =fgets(out,size,stdin);
60 if(c == NULL) return false;
61
62 if(strlen(out)==0) return false;
63 out[strlen(out)-1]=0;//输入会按回车,最后一个会换行符,变为0
64 return true;
65 }
bash
//3.得到命令符
98 char madchar[COMMAND_SIZE];
99 if(! Getcommadchar(madchar,MAXARGC))//没有输入
100 continue;
101 printf("%s\n",madchar);
使用结果
bash
[hcss-ecs-1cde@lcb 6-myshell]#ll
ll
[hcss-ecs-1cde@lcb 6-myshell]#\ls -l
\ls -l
四.分析命令符
上面的获取命令符已经达到预期了,那么就该分析执行了
与得到一样,我们全局变量的字符数组存储的命令也有空格,此时要进行切割
bash
bool Analysecommad(char*amchar)
67 {
68 //使用strtok
69 #define SEP " "
70 //第一次调用,防止\ 为根目录
71 argc = 0;
72 argv[argc++]=strtok(amchar,SEP);
73 while((bool)(argv[argc++]=strtok(NULL,SEP)));
74 --argc;
75 return true;
76 }
此处的argv获取strtok的切割字符,不强制bool值也行,但逻辑上不清晰,容易误认为是赋值操作,而不是逻辑判断
strtok使用用法

五.执行命令符
根据我们前面学过的进程替换,可以知道,直接系统调用exec即可,而我们在获取命令符时,使用的是全局变量的字符数据来进行存储,此处刚好可以调用execpv
根据我们前面分析shell的实现原理得知,我们需要创建子进程来执行命令
bash
int Docommad()
79 {
80 pid_t id =fork();
81 if(id==0)
82 {
83 //child
84 execvp(argv[0],argv);
85 exit(1);
86 }
87 //father
88 pid_t ret = waitpid(id,NULL,0);
89 (void) ret;//使用,防止报错
90 return 0;
91 }
效果
bash
6-myshell makefile myshell myshell.cc shell.cc
[hcss-ecs-1cde@lcb /home/lcb/code/linux/first/6-myshell]#pwd
pwd
/home/lcb/code/linux/first/6-myshell
我们发现确实执行了,但一次就结束了,而系统的shell会不断执行,这不就是死循环吗?
因此,我们给自己的shell也加上个死循环,直到异常退出(ctrl+c)才结束
bash
int main()
93 {
94 while(true)//shell其实也是如此,死循环
95 {
96 PrintCommandline();
97 //3.得到命令符
98 char madchar[COMMAND_SIZE];
99 if(! Getcommadchar(madchar,MAXARGC))//没有输入
100 continue;
101 printf("%s\n",madchar);
102 //4.命令符分析
103 Analysecommad(madchar);
104 //5.执行命令符
105 Docommad();
106 }
107 return 0;
108 }
但这里我们基本的要求就已经完成了
六.实现内建命令--cd
bash
[hcss-ecs-1cde@lcb /home/lcb/code/linux/first/6-myshell]#cd
cd
[hcss-ecs-1cde@lcb /home/lcb/code/linux/first/6-myshell]#cd
cd
我们发现,当我们的程序执行内建命令时,无结果
原因在于
- 子进程是独立的进程,它的工作目录修改仅在自身生效,子进程退出后,父进程(你的 shell 主进程)的工作目录不会被改变。
- 而系统 Shell(如 bash)的
cd是内建命令(直接在 Shell 主进程中执行),所以能真正修改 Shell 的工作目录
解决办法,在创建子进程执行命令之前,对命令符进行判断,如果为内建命令,则不再创建子进程