My_shell模拟实现 在前面我们已经学习了进程替换进程控制等等的知识。这次我们就来模拟实现一个小小的项目做到实现自己的shell。环境初始化既然我们要制造自己的shell那我们肯定就要运行一些命令。那既然我们要运行命令我们肯定要有环境。因此我们第一步就是设置环境。首先我们先定义一个环境表用于存放我们的环境。接着我们需要知道的是当我们使用execvp进行操作的时候我们其实是不需要自己传环境的此时进行操作的使用的是我们的原进程的环境。这样做最大的问题就是如果我们想要在子进程里面export一个环境变量会导致我们去操作老的那个表。因此我们需要创造属于自己的环境变量表。首先我们先extern char **environ这个的意义是什么呢他主要是声明系统的全局环境变量表指针以便后续工作获取环境变量。第二我们就把我们自己创建的环境变量表给清空。直接使用menset进行清理。然后再确定一下g_envs 0。接着我们嗯就从系统环境变量获取环境变量首先是申请char*类型的空间然后需要比原本的环境变量大1这是为了在最后能放个NULL。接着我们引入自己的一个环境变量这个只是个测试的而已(char*)HAHAfor_test; 起到老表和新表的差异化。接着我们就在新表里面把环境变量给导入使用putenv这个实现的是“增加”而不是重置。然后我们就把当前的环境变量改为g_env该进程的信息就变啦。// 环境变量表 #define MAX_ENVS 100 char *g_env[MAX_ENVS]; int g_envs 0; void InitEnv() { extern char **environ; memset(g_env, 0, sizeof(g_env)); g_envs 0; //本来要从配置文件来 //1. 获取环境变量 for(int i 0; environ[i]; i) { // 1.1 申请空间 g_env[i] (char*)malloc(strlen(environ[i])1); strcpy(g_env[i], environ[i]); g_envs; } g_env[g_envs] (char*)HAHAfor_test; //for_test g_env[g_envs] NULL; //2. 导成环境变量 for(int i 0; g_env[i]; i) { putenv(g_env[i]); } environ g_env; }输出命令行提示符接下来就是制作我们输入命令前面的那一串提示符了包含主机名用户名当前目录等等。我们接下来分批讲解获取主机名和用户名家目录获取主机名使用一个函数进行封装然后返回得到的字符串。因为我们前面已经引入环境成功了因此我们直接去读环境的话是可以得到HOSTNAME的。我们直接看一下这个HOSTNAME是不是空如果是空返回Nome,如果不是空就返回这个hostname直接使用。用户名一样道理。家目录的返回值稍有差别如果是NULL就返回空表示没有家目录。const char *GetHostName() { const char *hostname getenv(HOSTNAME); return hostname NULL ? None : hostname; } const char *GetUserName() { const char *name getenv(USER); return name NULL ? None : name; } const char *GetHome() { const char *home getenv(HOME); return home NULL ? : home; }获取当前路径我们平时在shell输入pwd获取的就是当前的路径。而代码层级层级上获得当前路径有两个方法一个是getenv(PWD);另外一个是getcwd(cwd, sizeof(cwd));第一个是软件层级的可以通过系统或者当今系统变量里面叫做PWD的。但是他有个缺点当我们使用自己实现的cd去进行目录变换的时候PWD并不会更新因此我们使用第二种方法getcwd这个方法更为稳妥。他是从硬件层面去获得当前目录如果成功就返回这个路径的地址如果失败就返回NULL。在获得以后我们进行判断通过以后我们就进行安全数组打印。比如cwd拿到的是/home/yu它就负责在前面加上PWD拼成一串形如PWD/home/yu的完整键值对存在cwdenv数组里。然后return的时候盘点。如果NULL就返回None了。const char *GetPwd() { //const char *pwd getenv(PWD); const char *pwd getcwd(cwd, sizeof(cwd)); if(pwd ! NULL) { snprintf(cwdenv, sizeof(cwdenv), PWD%s, cwd); putenv(cwdenv); } return pwd NULL ? None : pwd; }提取最后一个目录名该代码的作用主要是使用rfind进行提取。首先我们先定义我们要用的符号是/,然后设置一个标准库里面的字符串dir是pwd接着我们先保证这个dir不是根目录如果是根目录直接返回“/”。然后我们就使用rfind函数进行倒着查。该函数会倒着查dir这个字符串直到找到了/符号然后返回这个位置的下标。如果发现pos的位置竟然在npos(-1)我们就认为出现bug了。如果没bug我们就返回dir的最后一个目录注意这里pos1因为我们不想要前面还有一个/。std::string DirName(const char *pwd) { #define SLASH / std::string dir pwd; if(dir SLASH) return SLASH; auto pos dir.rfind(SLASH); if(pos std::string::npos) return BUG?; return dir.substr(pos1); }正式打印提示符我们首先定义一个格式我们要求提示符必须长这样[%s%s %s]# 。接着我们定义cmd_prompt然后往这个数组里面写文字写的是就是我们前面得到的种种const char*数据然后我们在PrintCommandPrompt()中调用这个MakeCommandLine函数得到我们要的字符串以后打印出来再强行刷新缓冲区。#define FORMAT [%s%s %s]# #define COMMAND_SIZE 1024 void MakeCommandLine(char cmd_prompt[], int size) { snprintf(cmd_prompt, size, FORMAT, GetUserName(), GetHostName(), DirName(GetPwd()).c_str()); //snprintf(cmd_prompt, size, FORMAT, GetUserName(), GetHostName(), GetPwd()); } void PrintCommandPrompt() { char prompt[COMMAND_SIZE]; MakeCommandLine(prompt, sizeof(prompt)); printf(%s, prompt); fflush(stdout); }获取用户输入的命令这个代码稍微简单一些其主要是定义了一个字符串用于存放命令然后我们去调用一个命令获取函数把得到的命令存入commandline中其中使用到了fgets函数这个函数获取了我们输入的信息stdin然后先确定没有大于COMMAND_SIZE当一切正常以后就返回一个字符串给c如果c是null那就认为我们啥也没打印返回false。因为我们的主函数是一个while无限循环因此他就会跳过剩下的代码直接重新运行一遍while。如果不是null那就清理\n再判断一下是不是空。通过以后返回true。char commandline[COMMAND_SIZE]; if(!GetCommandLine(commandline, sizeof(commandline))) continue; bool GetCommandLine(char *out, int size) { // ls -a -l ls -a -l\n 字符串 char *c fgets(out, size, stdin); if(c NULL) return false; out[strlen(out)-1] 0; // 清理\n if(strlen(out) 0) return false; return true; }重定向分析该代码主要是实现了终端的重定向输入。这份代码从后面往前面找内容。首先先保证redir是0.让清理掉filename这个字符串保证他也是空的。接着定义start和end。进入循环当endstart的时候就不断循环。当检测到cmd[end] 或者cmd[end] 的时候就进入判断语句。首先是cmd[end] 此时我们就让当前位置为空然后end就自然而然的让cmd[end]指向了file的位置。如果这两步之间有很多空格就使用TrimSpace进行快速定位。TrimSpace使用的是引用传参这代表TrimSpace里对end的改变会影响到外面的end.接着就把redir INPUT_REDIR;设置为输入模式。然后filenamecmdend这样就是可以快速的得到filename了。然后是cmd[end] 在进入以后进行第二次判断如果发现前面再一位也是,那么就认为是append模式。如果没有那就是输出模式。后续的工作和输入模型就差不多了。#define NONE_REDIR 0 #define INPUT_REDIR 1 #define OUTPUT_REDIR 2 #define APPEND_REDIR 3 int redir NONE_REDIR; std::string filename; void TrimSpace(char cmd[], int end) { while(isspace(cmd[end])) { end; } } void RedirCheck(char cmd[]) { redir NONE_REDIR; filename.clear(); int start 0; int end strlen(cmd)-1; //ls -a -l file.txt while(end start) { if(cmd[end] ) { cmd[end] 0; TrimSpace(cmd, end); redir INPUT_REDIR; filename cmdend; break; } else if(cmd[end] ) { if(cmd[end-1] ) { // cmd[end-1] 0; redir APPEND_REDIR; } else { // redir OUTPUT_REDIR; } cmd[end] 0; TrimSpace(cmd, end); filename cmdend; break; } else { end--; } } }命令行分析这几句语句主要是实现对命令行进行拆分按照空格进行拆分。首先先进行第一次拆分当strtok在命令中找到第一个空格的时候就返回给g_argv[0]位置。然后进入一个while循环。这个while循环实现了四个功能1:切分。他会遍历这个命令行找到所有的命令进行切分。2自增。再每次遍历找到空格之后他都会对g_argc进行,以便下一个数据的插入。3赋值strtok切出来字符串以后就赋值给g_argv.4.条件判断当g_argv[g_argc]为0的时候就是因为自增导致到了一个没有数据的空间然后strtok已经把能拆了都拆完了也不能返回一个数据给他了此时while就结束了。在结束以后如果g_argc是大于0的那就返回true如果是小于等于0的。那就返回false了。如果用户什么都没打或者只输入了一堆空格导致CommandParse连一个有效的单词都切不出来g_argc 0函数就会返回false让用户重新输入。bool CommandParse(char *commandline) { #define SEP g_argc 0; // 命令行分析 ls -a -l - ls -a -l g_argv[g_argc] strtok(commandline, SEP); while((bool)(g_argv[g_argc] strtok(nullptr, SEP))); g_argc--; return g_argc 0 ? true:false; } if(!CommandParse(commandline)) continue;内键命令首先我们需要对命令行的第一个元素进行一个判断判断他到底是cd还是echo还是alias等等。再判断结束以后就开始直接在父进程执行相关的命令。这个命令是不可以在子进程中执行的因为我们的命令行提示符一直都是主程序的。bool CheckAndExecBuiltin() { //如果内键命令做重定向更改shell的标准输入输出错误 std::string cmd g_argv[0]; if(cmd cd) { Cd(); return true; } else if(cmd echo) { Echo(); return true; } else if(cmd export) { } else if(cmd alias) { // std::string nickname g_argv[1]; // alias_list.insert(k, v); } return false; }Cd命令Cd命令就是模仿linux的来实现。当我们的g_argc是1即命令行中只有“cd”,后面没东西的时候我们就认为是要去家目录。如果g_argc1,则认为不是要干这个然后再继续判断where就认为是我们要移动去的目录。如果这个where是~就还是去家目录。如果是“-”就认为我们要去上一次去的目录当然前提是上一次的路径存在。如果没有以上要素那就是普通的路径记录了。接下来就是使用chdir进行路径传递。std::string g_oldpwd ; // 全局变量用来记下“前任”目录的路径 bool Cd() { std::string target_path ; // 最终我们要去的目标路径 // 1. 判定用户到底想去哪把目标路径给确定下来 if(g_argc 1) { target_path GetHome(); // 没传参数目标是家目录 } else { std::string where g_argv[1]; if(where ~) { target_path GetHome(); // 敲了 ~目标也是家目录 } else if(where -) { if(g_oldpwd.empty()) { std::cout myshell: cd: OLDPWD not set std::endl; return true; } target_path g_oldpwd; // 敲了 -目标是上一次的老路 std::cout target_path std::endl; // 规范cd - 会把新路径打印出来 } else { target_path where; // 正常路径如 ../ or /etc目标就是它 } } // 2. 准备开始传送 if(!target_path.empty()) { // 在抬脚走人之前先把现在的脚下目录用 getcwd 记录下来作为下一次的“老路” char current_cwd[1024]; if(getcwd(current_cwd, sizeof(current_cwd))) { g_oldpwd current_cwd; } // 核心一步正式传送到目标路径 int ret chdir(target_path.c_str()); if(ret 0) { perror(cd error); // 如果路径不存在报错提示 } } return true; }EchoEcho主要是是实现两个功能如果是$?那就打印上一次的退出码如果第一个数据是$那就裁掉 $拿到 PATH然后去环境变量表里面查。实现一个比较简单的功能。int lastcode 0; void Echo() { if(g_argc 2) { // echo hello world // echo $? // echo $PATH std::string opt g_argv[1]; if(opt $?) { std::cout lastcode std::endl; lastcode 0; } else if(opt[0] $) { std::string env_name opt.substr(1); const char *env_value getenv(env_name.c_str()); if(env_value) std::cout env_value std::endl; } else { std::cout opt std::endl; } } }执行命令这是my_shell实现的关键步骤。首先当执行到这里的时候直接创建一个子进程并且在子进程中进行操作。接下来是三个重定向的代码。fd是文件描述符也可以认为是管道他是按顺序排布的0是标准输入1是标准输出2是错误输出。当我们接收open的返回值的时候fd会被分配一个最小的没用的数字他的指针就会指向磁盘上的file.txt。然后dup2(fd,0);的功能是什么呢我们前面说了0,1,2的管道是被系统锁死的dup2的功能就是让0号管道在这个进程中指向fd号管道指向的文件后续我们如果想要从屏幕内获取信息就会从该文件里面去提取。open的三个标志位O_CREAT文件不存在就自动创建。O_WRONLY以只写Write Only模式打开。O_TRUNC清空Truncate。如果文件原来有内容瞬间全擦掉。这就是覆盖写入的底气。0666创建新文件时的默认权限可读可写。接下来就是execvp(g_argv[0], g_argv);这个就是进行进程替换执行我们原本要求的命令。然后父进程id0就是死等等到子进程完成以后获得子进程的返回值并更新last_code。然后return 0结束这一轮的工作。int Execute() { pid_t id fork(); if(id 0) { int fd -1; // 子进程检测重定向情况 if(redir INPUT_REDIR) { fd open(filename.c_str(), O_RDONLY); if(fd 0) exit(1); dup2(fd,0); close(fd); } else if(redir OUTPUT_REDIR) { fd open(filename.c_str(), O_CREAT | O_WRONLY | O_TRUNC, 0666); if(fd 0) exit(2); dup2(fd, 1); close(fd); } else if(redir APPEND_REDIR) { fd open(filename.c_str(), O_CREAT | O_WRONLY | O_APPEND, 0666); if(fd 0) exit(2); dup2(fd, 1); close(fd); } else {} // 进程替换会影响重定向的结果吗不影响 //child execvp(g_argv[0], g_argv); exit(1); } int status 0; // father pid_t rid waitpid(id, status, 0); if(rid 0) { lastcode WEXITSTATUS(status); } return 0; }