【Linux】编写简易shell 深度理解命令行解释器 环境变量 内建命令
🌻个人主页:路飞雪吖~
🌠专栏:Linux
目录
主体:
一、命令行提示符
二、获取用户命令
三、分析命令
四、执行命令
五、内建命令
🌠 小贴士:
六、环境变量
七、总结
八、完整代码
如若对你有帮助,记得关注、收藏、点赞哦~ 您的支持是我最大的动力🌹🌹🌹🌹!!!
若有误,望各位,在评论区留言或者私信我 指点迷津!!!谢谢 ヾ(≧▽≦*)o \( •̀ ω •́ )/
主体:
int main(){while(true){PrintCommandLine(); // 1. 命令行提示符GetCommandLine(); // 2. 获取用户命令AnalysisCommandLine(); // 3. 分析命令ExecuteCommand(); // 4. 执行命令}return 0;}
一、命令行提示符
命令行提示符一般由:用户名+主机名+当前工作路径 组成。我们如何获取呢?这就不得不提到我们之前提到过的 环境变量表 了,我们知道每个子进程都会继承父进程的环境变量表,所以我们可以从环境变量表来获取用户名、主机名、当前工作路径。
1 #include <iostream>2 #include <cstdio>3 #include <cstdlib>4 #include <cstring>5 #include <unistd.h>6 #include <sys/types.h>7 #include <wait.h>8 9 using namespace std; 10 11 int basesize = 1024; 12 13 string GetUserName() 14 { 15 string username = getenv("USER"); 16 return username.empty() ? "None" : username; 17 } 18 19 string GetHostName() 20 { 21 string hostname = getenv("HOSTNAME"); 22 return hostname.empty() ? "None" : hostname; 23 } 24 25 string GetPWD() 26 { 27 string pwd = getenv("PWD"); 28 return pwd.empty() ? "None" : pwd; 29 } 30 31 string MakeCommandLine() 32 { 33 char command_line[basesize]; 34 snprintf(command_line, basesize, "[%s@%s %s]# ",\ 35 GetUserName().c_str(), GetHostName().c_str(), GetPWD().c_str()); 36 return command_line; 37 38 } 39 void PrintCommandLine() // 1. 命令行提示符 40 { 41 printf("%s",MakeCommandLine().c_str()); 42 fflush(stdout); 43 } 44 45 int main() 46 {47 while(true)48 {49 PrintCommandLine(); // 1. 命令行提示符50 printf("\n");51 sleep(1);52 //GetCommandLine(); // 2. 获取用户命令53 54 //ParseCommandLine(); // 3. 分析命令55 56 //ExecuteCommand(); // 4. 执行命令57 }58 return 0;59 }
二、获取用户命令
• 从键盘当中获取出来的字符串,放到command_buffer缓冲区里面
• 将用户输入的命令行,当作完整的字符串
• "ls -a -l -n" 包括空格
• fgets(获取字符串, 指定大小, 从特定的文件流当中获取)
• 获取失败返回null,获取成功返回获取成功的字符串的起始地址
46 bool GetCommandLine(char command_buffer[], int size) // 2. 获取用户命令47 { 48 char *result = fgets(command_buffer, size, stdin);49 if(!result) 50 { 51 return false; 52 } 53 command_buffer[strlen(command_buffer)-1] = 0; // 把最后输入的回车字符改为0,即纯净版输入 54 if(strlen(command_buffer) == 0) return false;55 return true; 56 } 57 58 int main() 59 { 60 char command_buffer[basesize]; 61 while(true) 62 { 63 PrintCommandLine(); // 1. 命令行提示符64 65 if(!GetCommandLine(command_buffer, basesize)) // 2. 获取用户命令66 { // 依次获取每个子字符串 67 continue; // 获取到空格继续 68 } 69 printf("%s\n",command_buffer);70 71 //ParseCommandLine(); // 3. 分析命令 72 73 //ExecuteCommand(); // 4. 执行命令74 }75 return 0;76 }
三、分析命令
• 解析出来的命令,放到自己构建的全局的环境变量表当中
• strtok(一个字符串分多个,分隔符) 返回值:按照该分隔符从源字符串里切出来的第一个区域
• strtok(str, " "); 如果切割有效的字符串,返回具体的字符串的第一个地址,
• strtok(nullptr, " "); 否则,返回nullptr,即切到最后没有了,自动返回nullptr
const int argvnum = 64;const int envnum = 64;char *gargv[argvnum];// 全局的环境变量表char gargc = 0; // 记录环境变量的个数65 void ParseCommandLine(char command_buffer[], int len) // 3. 分析命令66 {67 (void)len;68 memset(gargv, 0, sizeof(gargv)); // 初始化/清空环境变量表里面的内容69 gargc = 0;70 71 const char *sep = " ";72 73 gargv[gargc++] = strtok(command_buffer, sep); // 放到全局的环境列表里面74 75 // 循环放入环境变量表中76 while((bool)(gargv[gargc++] = strtok(nullptr, sep)));77 gargc--;78 }79 80 void debug()81 {82 printf("argc: %d\n",gargc);83 for(int i=0; gargv[i]; i++)84 {85 printf("argv[%d]: %s\n", i, gargv[i]);86 }87 }88 89 int main()90 {91 char command_buffer[basesize];92 while(true)93 {94 PrintCommandLine(); // 1. 命令行提示符95 96 if(!GetCommandLine(command_buffer, basesize)) // 2. 获取用户命令97 {98 continue;99 }100 printf("%s\n",command_buffer);101 102 ParseCommandLine(command_buffer, strlen(command_buffer)); // 3. 分析命令103 debug();104 105 //ExecuteCommand(); // 4. 执行命令106 }107 return 0;108 }
四、执行命令
到底是谁在执行命令?是shell自己执行吗?
shell不能自己执行命令,如果shell自己可以自行命令,当命令错误的时候,整个程序就会全挂掉了。shell是一个单进程。shell 创建子进程来执行命令。
在命令行中输入命令,是如何解析的?谁解析的?如何传递给目标子进程的?
shell来帮助我们做解析,形成一张表,然后在进行程序替换的时候,直接调用系统的execvp()函数来执行我的程序,并把这张表传递给我自己对应的进程。
89 bool ExecuteCommand() // 4. 执行命令90 {91 pid_t id = fork(); // 创建子进程92 if(id < 0) return false;93 if(id == 0)94 {95 // child96 // 1.执行命令 调用系统函数97 execvp(gargv[0], gargv);98 // 2.退出99 exit(1);100 }101 // father102 int status = 0;103 pid_t rid = waitpid(id, &status, 0); 104 if(rid > 0)105 {106 return true;107 }108 return false;109 }
五、内建命令
每个进程都会有一个当前路径的概念,改变当前路径【chdir】,[cd ..] 就会改变当前工作路径【改变子进程的工作路径】。
当我们在自己写的shell中,我们 [cd ..] 应该改变的是子进程的路径还是自己写的shell进程的路径?
改变shell的工作路径【shell本身就是父shell的一个子进程】,因为子进程执行完命令就会退出,shell当前的工作路径根本就没有改变;改变shell的工作路径,之后执行的命令都会使用shell当前的路径。即:在shell中,有些命令必须由子进程来执行,有些命令不能由子进程执行,要由shell自己执行 --- 内建命令。
我们自己所在的cwd是在进程的PCB当中的,pwd查找的时候也是在PCB里面查找的,PWD是一个环境变量,所以当路径发生变化的时候,要更新一下环境变量。
114 void AddEnv(const char *item)115 {116 int index = 0;117 while(genv[index])118 {119 index++;120 }121 // 指向为空的下标122 genv[index] = (char*)malloc(strlen(item)+1);// 不能使用局部变量,要重新申请 123 strncpy(genv[index], item, strlen(item)+1); 124 125 genv[++index] = nullptr; 126 } 127 128 bool CheckAndExecBuiltCommand() 129 { 130 if(strcmp(gargv[0], "cd") == 0) 131 { 132 if(gargc == 2) 133 { 134 chdir(gargv[1]); 135 } 136 return true; 137 } 138 else if(strcmp(gargv[0], "export") == 0)// 导入环境变量 139 { 140 if(gargc == 2) 141 { 142 AddEnv(gargv[1]); 143 } 144 return true; 145 } 146 else if(strcmp(gargv[0], "env") == 0) 147 { 148 for(int i=0; genv[i]; i++) 149 { 150 printf("%s\n",genv[i]);151 } 152 return true;153 } 154 return false;155 }
我们虽然改变了工作路径,但是命令行提示符并没有发生改变,这是为什么呢?
🌠 小贴士:
• 环境变量是由shell自己维护的,即当路径发生变化时,环境变量表需要由shell自己取更新。
• 我们可以更改环境变量的原因:shell支持用户自己去更改,shell本来就是为用户服务的。
shell不是从0开始读配置文件的,它是从系统额shell直接启动的,所以我们写的shell启动的时候,它默认继承的是系统所对应的环境变量。
• ./myshell 它的环境变量,根本就没有维护环境变量表,它的环境变量其实是从(父进程)系统的shell直接继承的。
• 环境变量表的指针数组,默认是在系统的shell的全局数组给我们维护好的,它也是一张全局的表。
• putenv() 实则就是在父进程所对应的全局指针数组里面,找到一个没有使用的位置,把这个环境变量加进来。
• 导入到系统的shell里面,并不是子进程修改父进程的表,因为当发生修改时,会发生写时拷贝,当子进程不修改,父子进程就是共享的,一旦子进程进行修改,操作系统就会对父shell的指针数组进行写诗拷贝【把整张表全部拷贝一份给子进程】,此时父子进程的环境变量表就是分开的。
• getcwd(字符串,大小);获取当前工作路径【系统级接口,直接从进程的PCB里面拿】。
• 不能用子进程来导入环境变量,子进程不能影响当前的shell。
• 内建命令无法被子进程继承,环境变量可以,所以环境变量具有全局性。
36 string GetPWD()37 { 38 //string pwd = getenv("PWD");39 //return pwd.empty() ? "None" : pwd;40 41 // 系统调用42 // 1. 获取当前工作路径43 if(getcwd(pwd, sizeof(pwd)) == nullptr) return "None";44 45 // 2. 更新环境变量46 snprintf(pwdenv, sizeof(pwdenv), "PWD=%s", pwd);47 putenv(pwdenv);// PWD=XXXX48 return pwd; 49 }
六、环境变量
我们在自己写的shell里面创建子进程,这个子进程继承的环境变量表是继承谁的?我自己写的shell的环境变量表?
自己写的shell 和 自己写的shell创建的子进程 的环境变量表,都是继承父shell的。
那我们如何让自己写的shell 创建的子进程继承的环境变量表是自己写的shell的环境变量表呢?
父shell的环境变量表是从系统的配置文件里面来的【shell脚本】,我们从系统里把系统的环境变量拷一份到我们自己的shell里面,模拟一下从配置文件里面去读。
const int envnum = 64;
// 自己维护的环境变量表char *genv[envnum];void InitEnv(){// 从父shell获取环境变量extern char **environ;int index = 0;while(environ[index]){// 导入环境变量,实则就是向shell自己的环境变量表当中,进行插入一个新的环境变量// 即 再malloc一段空间,让指针数组指向它对应的那个位置genv[index] = (char*)malloc(strlen(environ[index]+1));strncpy(genv[index], environ[index], strlen(environ[index])+1);index++;}genv[index] = nullptr;}
所以当我们执行命令的时候,我们要把自己的环境变量参数传给系统去执行,因此 在执行一个新的程序时,把命令行参数表、环境变量表都传给execvpe() 系统调用,程序执行时,就能获得这两个参数。
101 bool ExecuteCommand() // 4. 执行命令102 {103 pid_t id = fork(); // 创建子进程104 if(id < 0) return false;105 if(id == 0)106 {107 // child108 // 1.执行命令 调用系统函数109 //execvp(gargv[0], gargv);110 execvpe(gargv[0], gargv, genv); 111 // 2.退出 112 exit(1); 113 } 114 // father 115 int status = 0; 116 pid_t rid = waitpid(id, &status, 0);117 if(rid > 0) 118 { 119 return true; 120 } 121 return false; 122 }
七、总结
1、在命令行中,一个命令是如何执行的?对于普通命令来讲,就是解析命令行,然后通过exec*系列的函数接口,进行fork()程序替换。
2、命令行参数是从命令行依次获取的,被shell自己解析自己维护。
3、环境变量表是从系统的配置文件读取出来的,由shell自己维护,维护好就是一个全局的指针数组,通过 execvpe() 接口函数来调用,这个系统的调用接口把环境变量传递给所有的子进程。
4、echo命令,是内建命令。系统除了维护环境变量表、命令行参数表,还维护了一张本地变量表,这张本地变量表无法通过 exec* 这样的接口传递给子进程,所以子进程看不到。在本地变量的这张表,让shell自己去维护,在echo的时候,就去打印。本地变量表属于在shell自己内部维护的全局数据当中的一个字符串。
八、完整代码
1 #include <iostream>2 #include <cstdio>3 #include <cstdlib>4 #include <cstring>5 #include <string>6 #include <unistd.h>7 #include <sys/types.h>8 #include <wait.h>9 10 using namespace std;11 12 const int basesize = 1024;13 const int argvnum = 64;14 const int envnum = 64;15 16 // 全局的命令行参数表17 char *gargv[argvnum];// 全局的环境变量表18 int gargc = 0; // 记录环境变量的个数19 20 // 自己维护环境变量表21 char *genv[envnum];22 23 // 系统的环境变量默认是从配置文件来的【shell脚本】,24 // 从系统里把系统环境变量拷一份到我的环境变量里面,模拟一下从配置文件里面去读25 26 // 全局的当前shell工作路径 27 char pwd[basesize];28 char pwdenv[basesize];29 30 string GetUserName()31 {32 string user = getenv("USER");33 return user.empty() ? "None" : user;34 }35 36 string GetHostName()37 {38 string hostname = getenv("HOSTNAME");39 return hostname.empty() ? "None" : hostname;40 } 41 42 string GetPwd()43 { 44 // 获取当前的工作路径不能从环境变量里面获取,因为环境变量要更新45 // string pwd = getenv("PWD"); 46 // return pwd.empty() ? "None" : pwd;47 48 // 系统调用49 50 // 直接从系统里面获取环境变量51 // 获取完之后,把环境变量更新 52 // getcwd(字符串,大小) 获取当前工作路径【系统级接口,直接从进程的PCB里面拿】被封装的接 口gunc封装的接口53 // 失败返回值为NULL,成功就把当前工作路径写到buff里面54 55 // 1、获取当前工作路径56 if(nullptr == getcwd(pwd, sizeof(pwd))) return "None"; // 获得当前工作路径57 58 // 2、更新环境变量59 snprintf(pwdenv, sizeof(pwdenv), "PWD=%s", pwd);60 putenv(pwdenv); // PWD=XXXX61 return pwd;62 63 // 环境变量是由shell自己维护的,即当路径发生变化时,环境变量表需要由shell自己去更新64 // 我们可以更改环境变量的原因:shell支持用户自己去更改,shell本来就是为用户服务的65 // shell不是从0开始读配置文件的,它时从系统的shell直接启动的,66 // 所以我们写的shell启动的时候,它默认继承的是系统所对应的环境变量67 // ./myshell它自己的环境变量,根本就没有维护环境变量表,68 // 它的环境变量其实是从(父进程)系统的shell直接继承69 //70 // getenv 71 //72 // 环境变量表的指针数组,默认是在系统的shell的全局数组给我们维护好的,它也是一张全局的表73 // putenv()实则就是在父进程所对应的全局指针数组里面,74 // 找一个没有使用的位置,然后把这个环境变量加进来75 //76 // 导入到系统额shell里面,并不是子进程修改父进程的表,77 // 因为发生修改时,会发生写时拷贝机制,78 // 当子进程不修改,父子进程时共享的,一旦子进程进行修改,79 // 操作系统就会对父shell的指针数组进行写时拷贝。【把整张表全部拷贝一份给子进程】80 // 此时父子进程的环境变量表就是分开的81 82 }83 84 string MakeCommandLine()85 {86 char command_line[basesize];// 输入的命令行长度87 // snprintf(写入到指定的缓冲区里,指定长度,按照指定的格式)88 snprintf(command_line, basesize, "[%s@%s %s]# ",\89 GetUserName().c_str(), GetHostName().c_str(), GetPwd().c_str());// c_str() 为C风格的字符串90 return command_line;91 }92 93 void PrintCommandLine() // 1.命令行提示符94 {95 printf("%s",MakeCommandLine().c_str());96 fflush(stdout); 97 }98 99 bool GetCommandLine(char command_buffer[], int size) // 2.获取用户命令
100 {
101 // 从键盘当中获取出来的字符串,放到command_buffer缓冲区里面
102 // 将用户输入的命令行,当作完整的字符串
103 // "ls -a -l -n" 包括空格
104 // fgets(获取字符串, 指定大小, 从特定的文件流当中获取) 获取失败返回null,获取成功返回获 取成功的字符串的起始地址
105 char *result = fgets(command_buffer, size, stdin);
106 if(!result)
107 {
108 return false;
109 }
110 command_buffer[strlen(command_buffer)-1] = 0;// 最后输入的回车,把回车的字符改为0,字符 串为空,即输入就是纯净版的没有换行
111 if(strlen(command_buffer) == 0) return false; // 若输入为空,就终止
112 return true;
113 }
114
115 void ParseCommandLine(char command_buffer[], int len) // 3.分析命令
116 {
117 (void)len;
118 // 解析出来的命令,放到全局的环境变量表当中
119 // strtok(一个字符串分多个,分隔符) 返回值:按照该分隔符从源字符串里切出来的第一个区域
120 // strtok(str, " "); 如果切割有效的字符串,返回具体的字符串的第一个地址,
121 // strtok(nullptr, " "); 否则,返回nullptr,即切到最后没有了,自动返回nullptr
122 // "ls -a -l -n"
123 memset(gargv, 0 , sizeof(gargv)); // 初始化/清空环境变量表里面的内容
124 gargc = 0;
125
126 const char *sep = " ";
127 // 把首个子字符串拿出来
128 gargv[gargc++] = strtok(command_buffer, sep);// 放到全局的环境列表里面
129
130 // =是刻意写的 循环放入环境变量表中
131 while((bool)(gargv[gargc++] = strtok(nullptr, sep)));
132 gargc--;
133 }
134
135 void debug()
136 {
137 printf("argc: %d\n",gargc);
138 for(int i=0; gargv[i]; i++)
139 {
140 printf("argv[%d]: %s\n", i, gargv[i]);
141 }
142 }
143
144 bool ExecuteCommand() // 4.执行命令
145 {
146 // shell 不能自己执行这个命令,如果shell可以自己执行命令,当命令错误的时候就全挂掉了,
147 // shell是一个单进程的。
148 // 让子进程执行命令
149 pid_t id = fork();// 创建子进程
150 if(id < 0) return false;
151 if(id == 0)
152 {
153 // child
154 // 1. 执行命令
155 // exec*
156 // execvp(gargv[0], gargv);
157 // 在命令行当中输入的命令,是如何解析的?谁解析的?如何传递给目标子进程的?
158 // shell来帮我们做解析,形成一张表,然后在进行程序替换的时候,
159 // 直接以execvp的形式执行起来我的程序,并把这张表传递给对应的自己的进程
160
161 // 如果我们在自己写的shell里面创建子进程,
162 // 那这个子进程继承的环境变量表是父shell的,自己写的shell也是继承父shell的环境表
163 // 那我们如何让我们自己写的shell,创建的子进程继承的环境变量表是我自己写的shell的环 境变量表呢?
164 execvpe(gargv[0], gargv, genv);// 把自己写的环境变量参数传给它
165 // 在执行一个新的程序时,把命令行参数表、环境变量表都传给execvpe系统,
166 // 所以自己的程序是父进程通过系统调用,把这两个参数交给了你的程序,
167 // 程序执行时,就能获得这两个参数
168
169
170
171 // 2. 退出
172 exit(1);
173 }
174
175 // father
176 int status = 0;
177 pid_t rid = waitpid(id, &status, 0);
178 if(rid > 0)
179 {
180 // Do Nothing
181 return true;
182 }
183 return false;
184 }
185
186 void AddEnv(const char *item)
187 {
188 int index = 0;
189 while(genv[index])
190 {
191 index++;
192 }
193 // 指向为空的下标
194 genv[index] = (char*)malloc(strlen(item)+1);// 不能使用局部的变量,要重新申请
195 strncpy(genv[index], item, strlen(item)+1);
196
197 genv[++index] = nullptr;
198 }
199
200 // shell自己执行命令,本质是shell调用自己的函数
201 bool CheckAndExexcBuiltCommand() // shell调用自己的内部函数,把自己的路径发生变化
202 {
203 if(strcmp(gargv[0], "cd") == 0)
204 {
205 // 内建命令
206 if(gargc == 2) //
207 {
208 // 系统接口
209 chdir(gargv[1]); // 切换路径
210 }
211 return true;
212 }
213 else if(strcmp(gargv[0], "export") == 0)
214 {
215 // export也是内建命令
216 if(gargc == 2)
217 {
218 AddEnv(gargv[1]);
219 }
220 return true;
221 }
222 else if(strcmp(gargv[0], "env") == 0)
223 {
224 for(int i=0; genv[i]; i++)
225 {
226 printf("%s\n",genv[i]);
227 }
228 return true;
229 }
230 return false;
231 }
232
233 // 从系统里把系统环境变量拷一份到我的环境变量里面,模拟一下从配置文件里面去读
234 // 作为一个shell,获取环境变量应该从系统的配置文件来获取
235 // 我们现在是直接从父shell中获取环境变量
236 void InitEnv()
237 {
238 // 从父进程获取环境变量
239 extern char **environ;
240 int index = 0;
241 while(environ[index])
242 {
243 // 导入环境变量,实则就是向shell自己的环境变量表当中,进行插入一个新的环境变量
244 // 即 再malloc 一段空间,让指针数组指向它对应的那个位置
245 genv[index] = (char*)malloc(strlen(environ[index]+1));
246 strncpy(genv[index], environ[index], strlen(environ[index])+1);
247 index++;
248 }
249 genv[index] = nullptr;
250 }
251
252 int main()
253 {
254 InitEnv();
255 char command_buffer[basesize];// 命令行缓冲区大小
256 while(true)
257 {
258 PrintCommandLine(); // 1.命令行提示符
259
260 // command_buffer --> 输出型参数
261 if(!GetCommandLine(command_buffer, basesize)) // 2.获取用户命令
262 { // 依次获取每个子字符串
263 continue;// 获取到空格继续获取
264 }
265 //printf("%s\n",command_buffer);
266
267 // "ls -a -l -n" --> "ls" "-a" "-l" "-n" 拆成一个一个的子字符串
268 ParseCommandLine(command_buffer, strlen(command_buffer)); // 3.分析命令
269 //debug();
270
271 // 先判断该命令是否是内建命令
272 if(CheckAndExexcBuiltCommand())
273 {
274 continue;
275 }
276
277 ExecuteCommand(); // 4.执行命令
278
279 }
280
281 return 0;
282 }