—— 手搓自主shell)
学习Linux到目前为止我们都知道命令是由shell执行的但是具体如何执行的我们看不到因此我们今天来自己写一个shell来执行我们的指令让大家对shell的底层有一个进阶的理解文章的最后会给出完整代码喔~目录一、打印命令行提示符二、获取键盘输入三、解析字符串四、执行指令五、增加内建命令1.cd 路径的改变2.echo 退出码六、总结与源码一、打印命令行提示符知识前置shell本质上是一个死循环因为要不断地处理一条又一条指令因此编写的shell功能全部都要放进一个死循环中直到我们主动退出才结束命令行提示符的格式为 [用户名主机名 路径]因此要打印出来就必须获取这三个数据很显然它们都属于环境变量分别对应USER、HOSTNAME和PWD要获取环境变量用getenv函数即可为了方便管理我们将打印命令行提示符封装成一个函数//1.打印命令行提示符 PrintCommandLine();void PrintCommandLine() { printf([%s%s %s]# , GetUserName(), GetHostName(), GetPwd()); //用户名主机名 当前路径 }const char *GetUserName() { char *name getenv(USER); if(name NULL) { return None; } return name; } const char *GetHostName() { char *hostname getenv(HOSTNAME); if(hostname NULL) { return None; } return hostname; } const char *GetPwd() { char *pwd getenv(PWD); if(pwd NULL) { return None; } return pwd; }至此命令行提示符的打印就完成了但是shell本质是一个死循环它还得需要一个等待我们输入指令的功能否则就会一直打印导致满屏的命令行提示符二、获取键盘输入键盘输入本质就是输入一个字符串需要用一个字符数组来接收#define MAXSIZE 128 //... char command_line[MAXSIZE] {0};对于指令的输入我们通常存在两种情况一种是输入指令后回车一种是啥也不输入直接回车因此要获取键盘输入我们需要用到系统调用 fgets 啥也不输入的时候返回NULL 我们依旧对获取键盘输入的功能封装一个函数且函数的返回值为输入的指令字符串长度当返回0的时候命中我们刚刚说的第二种情况直接continue即可否则我们尝试打印出刚刚输入的指令做一个验证测试看看是否输出的与我们输入的一致但同时要注意的是无论哪一种情况我们都要至少输入一次回车键回车键相当于换行符\n因此为了保证字符串和输出的正确性我们需要将最后的换行符改成\0//2.获取键盘输入 if(GetCommand(command_line, sizeof(command_line)) 0) continue; printf(%s\n, command_line);int GetCommand(char commandline[], int size) { if(fgets(commandline, size, stdin) NULL) return 0; //用户输入的时候至少会按一次回车\n改\0 commandline[strlen(commandline)-1] \0; return strlen(commandline); }运行结果如下三、解析字符串前面我们输入进去的指令是一整个字符串我们要把它们拆分“ls -a -l” - “ls” “-a” -l 并放入命令行参数表中对于字符串的拆分C语言中有一个封装好的函数strtok第一个参数为要拆分的字符串第二个参数为拆分符号遇到该符号就进行拆分对于同一个字符串的第二次拆分则将第一个参数设为NULL否则会一直拆分第一个而后面的不拆分具体代码演示如下#includestdio.h #includestring.h int main() { char str[] aaa bbb ccc ddd; const char* sep ; char *p strtok(str, sep); printf(%s\n, p); while(p) { p strtok(NULL, sep); if(p NULL) { break; } printf(%s\n,p); } return 0; }拆分的字符串我们要放到全局的环境变量表中这是shell内部要维护的第一张表同时设置一下切割分隔符#define MAXARGS 32 //shell内部维护的第一张表命令行参数表 char *gargv[MAXARGS]; int gargc 0; const char *sep ;将解析字符串封装函数//3.解析字符串 ParseCommand(command_line);int ParseCommand(char commandline[]) { //输入新的指令要重置命令行参数表 gargc 0; memset(gargv, 0, sizeof(gargv)); //分割字符串 gargv[0] strtok(commandline, sep); while((gargv[gargc] strtok(NULL, sep))); //打印测试 printf(gargc: %d\n, gargc); int i 0; for(; gargv[i]; i) printf(gargv[%d]: %s\n, i, gargv[i]); return 0; }我们打印出分割结果测试一下效果四、执行指令前面我们完成了指令的输入和解析还差一个执行要执行指令就需要fork子进程来进行程序替换//4.执行指令 ExcuteCommand();int ExcuteCommand() { pid_t id fork(); if(id 0) return -1; else if(id 0) { //子进程 程序替换 execvp(gargv[0], gargv); exit(1); } else{ //父进程 int status 0; pid_t rid waitpid(id, status, 0); if(rid 0) { printf(wait child process success!\n); } } }有了命令行参数表gargv我们就可以用程序替换函数execvp了对于程序替换有问题的可以回看博主的文章《进程程序替换》【Linux】进程控制三——进程程序替换-CSDN博客我们来看看运行结果结果如我们所料我们把所有的测试打印全部注释再试试看到这里为止shell的基本框架搭成了五、增加内建命令刚刚我们输入的指令都如期由子进程替换执行成功了但是我们来看下面这种情况这里演示了两种无法成功执行的情况echo $?是输出上一个程序执行结束的退出码这里没有打印出来cd ..是返回上一个目录但是我们用pwd查看却发现路径没有变化。原因就是它们都属于内建命令内建命令的特点是由Shell自身解析执行不需要创建新进程再替换接下来一一解决这两个问题执行命令前先写一个函数判断是否是内建命令如果是则直接执行并返回1如果不是则返回0并创建子进程替换执行命令1.cd 路径的改变要改变路径可以使用chdir函数直接将gargv[1]放入参数即可因为gargv[0]是cd后面一个必跟路径//4.执行指令 if(CheckBuiltinExcute() 0) continue; ExcuteCommand();int CheckBuiltinExcute() { if(strcmp(gargv[0], cd) 0) { //内建命令 if(gargc 2) { //新的目标路径 chdir(gargv[1]); } return 1; } return 0; }试一下执行结果可以发现路径改变了但是为什么命令行提示符的路径却没有改变呢回看我们之前获取路径的函数const char *GetPwd() { char *pwd getenv(PWD); if(pwd NULL) { return None; } return pwd; }会发现我们获取的是环境变量的PWD记录但是环境变量的这个值是静态的即使我们用chdir切换了目录但是没更新PWD环境变量它依旧会返回旧路径不准要实时改变这个路径就不能依赖环境变量而需要一个能直接与内核交互的系统调用 getcwd这个系统调用的第一个参数属于典型的输出型参数我们提供数组参数它会将实时路径给我传递到数组中那我们就创建一个接收的数组//我们shell所处的工作路径 char cwd[MAXSIZE];利用getcwd系统调用优化Getpwd函数const char *GetPwd() { //char *pwd getenv(PWD); char *pwd getcwd(cwd, sizeof(cwd)); if(pwd NULL) { return None; } return cwd; }试试优化后的效果达到了我们的预期但到这里还剩下最后一个问题Linux命令行提示符中的路径只保留了最后一个而我们是直接显示出了一长串的绝对路径这是过于冗余的我们接下来就是要解决这个问题只取它的最后一个“/”后的路径这里我用c来实现static std::string rfindDir(const std::string p) { if(p /) return p; const std::string psep /; auto pos p.rfind(psep); if(pos std::string::npos) { return std::string(); } return p.substr(pos1); }在打印的位置将本来要传的绝对路径先传入这个函数当中最后截取结束后再打印出来void PrintCommandLine() { printf([%s%s %s]# , GetUserName(), GetHostName(), rfindDir(GetPwd()).c_str()); //用户名主机名 当前路径 }记得对rfindDir的返回值还要加一个c_str()这是C为了兼容C语言的打印而设计的接口来看看成果为了区分原shell和我们自己写的shell我们的分隔符是不一样的前者是$我的是#到这里就解决了cd路径的改变问题所以其实真正的shell还是非常复杂的我这仅仅是极简版本还有很多内建命令和其他各种快捷键功能没实现我们接下来再解决一下echo问题2.echo 退出码echo依旧是内建命令承接实现“cd”的代码继续扩充。首先先解决获取退出码的问题先定一个全局变量Last_Exitcode//上一个进程结束的退出码 int Last_ExitCode 0;编写输入指令为echo的情况并在每个指令执行结束的地方重置退出码int CheckBuiltinExcute() { if(strcmp(gargv[0], cd) 0) { //内建命令 if(gargc 2) { //新的目标路径 chdir(gargv[1]); Last_ExitCode 0; } return 1; } else if(strcmp(gargv[0], echo) 0) { if(gargc 2) { if(gargv[1][0] $) { if(strcmp(gargv[1]1, ?) 0) { printf(lastcode:%d\n, Last_ExitCode); } Last_ExitCode 0; } return 1; } } return 0; }//父进程 int status 0; pid_t rid waitpid(id, status, 0); if(rid 0) { //获取退出码 Last_ExitCode WEXITSTATUS(status); //printf(wait child process success!\n); }来看看实现成果六、总结与源码至此一个简易的shell被我们手搓出来了独立完成其实非常考验知识储备和代码能力可作为一个教学意义极高的训练典例其实除了命令行参数表以外shell内部还管理了另一张表就是环境变量表它也承担着非常重要的角色我将手搓的源码放在下面供大家自行在此基础拓展#includestdio.h #includestdlib.h #includestring.h #includeunistd.h #includesys/types.h #includesys/wait.h #includeiostream #includestring #define MAXSIZE 128 #define MAXARGS 32 //shell内部维护的第一张表命令行参数表 char *gargv[MAXARGS]; int gargc 0; const char *sep ; //我们shell所处的工作路径 char cwd[MAXSIZE]; //上一个进程结束的退出码 int Last_ExitCode 0; static std::string rfindDir(const std::string p) { if(p /) return p; const std::string psep /; auto pos p.rfind(psep); if(pos std::string::npos) { return std::string(); } return p.substr(pos1); } const char *GetUserName() { char *name getenv(USER); if(name NULL) { return None; } return name; } const char *GetHostName() { char *hostname getenv(HOSTNAME); if(hostname NULL) { return None; } return hostname; } const char *GetPwd() { //char *pwd getenv(PWD); char *pwd getcwd(cwd, sizeof(cwd)); if(pwd NULL) { return None; } return cwd; } void PrintCommandLine() { printf([%s%s %s]# , GetUserName(), GetHostName(), rfindDir(GetPwd()).c_str()); //用户名主机名 当前路径 } int GetCommand(char commandline[], int size) { if(fgets(commandline, size, stdin) NULL) return 0; //用户输入的时候至少会按一次回车\n改\0 commandline[strlen(commandline)-1] \0; return strlen(commandline); } int ParseCommand(char commandline[]) { //输入新的指令要重置命令行参数表 gargc 0; memset(gargv, 0, sizeof(gargv)); //分割字符串 gargv[0] strtok(commandline, sep); while((gargv[gargc] strtok(NULL, sep))); // printf(gargc: %d\n, gargc); // int i 0; // for(; gargv[i]; i) // printf(gargv[%d]: %s\n, i, gargv[i]); return 0; } int CheckBuiltinExcute() { if(strcmp(gargv[0], cd) 0) { //内建命令 if(gargc 2) { //新的目标路径 chdir(gargv[1]); Last_ExitCode 0; } return 1; } else if(strcmp(gargv[0], echo) 0) { if(gargc 2) { if(gargv[1][0] $) { if(strcmp(gargv[1]1, ?) 0) { printf(lastcode:%d\n, Last_ExitCode); } Last_ExitCode 0; } return 1; } } return 0; } int ExcuteCommand() { pid_t id fork(); if(id 0) return -1; else if(id 0) { //子进程 程序替换 execvp(gargv[0], gargv); exit(1); } else{ //父进程 int status 0; pid_t rid waitpid(id, status, 0); if(rid 0) { //获取退出码 Last_ExitCode WEXITSTATUS(status); //printf(wait child process success!\n); } } } int main() { char command_line[MAXSIZE] {0}; while(1) { //1.打印命令行提示符 PrintCommandLine(); //2.获取键盘输入 if(GetCommand(command_line, sizeof(command_line)) 0) continue; // printf(%s\n, command_line); //3.解析字符串 ParseCommand(command_line); //4.执行指令 if(CheckBuiltinExcute() 0) continue; ExcuteCommand(); } return 0; }