行业资讯

打造你的专属Shell:从零实现命令行解释器

发布时间:2026/8/13 23:23:39
打造你的专属Shell:从零实现命令行解释器 自定义shell命令行解释器实现命令行提示符我们要实现一个自定义的shell首先我们要实现一下命令行提示符吧我们知道这个提示符的内容为用户登录型号 工作路径我们学习过环境变量现在进程会继承bash的环境变量我们知道这些信息都可以在环境变量中获取这些数据再通过getenv系统调用来获取这些数据现在有了命令行提示符那么接下来要bash要做什么呢bash需要阻塞等待用户输入命令输入操作所以我们现在要定义一个字符数组等待用户输入可以用scanf输入吗可以但是不合适因为scanf是以空格为分隔符会把命令分隔开来ls -a -l而我们用户输入的命令对于shell来说是字符串ls -a -l所以最好用fget读一行、gets读取一行字符串这类我们代码是C/C混编使用char字符函数是为了解决一些字符串接口解释一下因为要讲解一下系统调用而系统调用是C语言编写的使用纯C可能会要去适配接口比如接口是指针上层是string这个时候接口就要适配所以讲讲C风格的字符串讲讲我们这里输入使用fgets(fgets是 C 语言里最安全、最常用的读取一行字符串函数专门用来从文件 / 标准输入读数据)这样就可以获得整个字符串的内容而不是字符串的一部分内容用fgets在标准输入读取一行字符串输入完后就要对输入结果进行判断c指针为NULL说明输入失败返回1输入成功我们就回显一下输入的数据这样就完成了bash输入的操作输入多余\n字符去除但是我们看到我们输出的结果还多了一个回车这是因为我们输入数据的时候是通过回车结束的此时字符串为ls\n所以我们要想办法去掉这个回车这样就清理掉了我们这里commandline长度减一会不会遇到越界情况答案是不会我们输入命令最少我们会按一个\n吧这样我们数组长度至少为1现在长度减一刚刚好为空没有影响优化代码这样写不太优雅我们来优化一下代码设置创建命令行提示符函数可以设置一个void Make_Cmd_Prompt (char cmd_prompt[],int size);函数这里snprintf是最安全、最常用的格式化拼接字符串函数用法和printf一样只是输出到字符串了snprintf(目标大小格式...)这样就创建了一个命令行提示符了设置打印命令行提示符函数打印命令行提示符外面先定义一个字符数组调用创建命令行提示符函数以此获得命令行提示符再printf输出命令行提示符我们要去输出命令行提示符不能换行所以释放缓冲区设置获取用户命令函数获取用户目录的操作也可以封装为一个函数优化后循环运行我们知道shell命令会一直运行永远不退出直到系统退出所以shell循环执行shell本质上是死循环会不断循环的接收用户的命令并执行分析命令行(strtok切割字符串)我们获得了命令我们学习过进程替换我们知道替换程序是需要获取以空格为分隔符获得的字符串字串所以需要分析命令行我们需要制作命令行参数列表方便快速的查找命令行参数ls -a -l---ls-a-l设置CommandParse函数将获取的命令行传入函数中通过函数解析命令行获取命令行参数列表和命令行参数个数传给子进程做准备我们分析字符串可以将指针指向第一个元素将空格替换成\0,在命令行参数列表结尾设为NULL可以实现但这里我们使用一个接口来实现strtokstr为被切割的字符串delim为分隔符注意事项字符串第一次切割要提供str地址但是如果想继续切str下次调用就不用提供地址g_argv和g_argc都是全局数据使用直接调用因为strtok的用法问题对同一个字符串切割第一次要传地址后面不需要传的用法我们先通过传地址首次切割字符串并将获得的字符串指针赋值到g_argv中并且g_argc做之后就可以循环切割并做赋值工作注意这里是用而不是着代表着循环条件为g_argv[g_argc]的数据当字符串被切割到最后返回为NULL时这个NULL会赋值到g_argv中以此循环结束我们也获得分析号的命令行参数我们来测试一下输出命令行参数看看结果如何可以看到成功实现了命令行分析但是我们发现g_argc的数量不太对呀命令行参数的数量为3个但是g_argc的数量为4因为strtok的时候NULL插入到g_argv的时候也算成一个参数了使用要进行修正这样的成功了执行命令创建子进程进行程序替换为了不妨碍父进程继续后续的工作我们要创建子进程让子进程完成命令的任务而父进程只需要waitpid等待子进程完成获取子进程运行结果回收子进程PCB完成父进程的操作接下来继续子进程的操作子进完成任务肯定要进行进程程序替换用哪个接口呢我们已经准备好了命令行参数列表g_argv我们并没要提供新的环境变量并且我们没有准确的程序文件路径所以我想提供文件名让编译器去环境变量找这个文件所以进程程序替换的接口在这里的最佳选择是execvp所以我们自定义的shell成功了我们将执行命令部分封装起来做路径切割我们通过GetPwd会获得工作目录但是我们想只留当前的目录这个时候我们就需要切割一下我们使用string通过rfind逆向去查找第一个/并把位置通过pos存储,找到后使用substr来获取pos1pos后面是/XXX后面的字符串cd这类内建命令未实现为什么呢首先我们实现shell功能的地方是在子进程我们cd也只是修改了子进程的cwd父进程的未被修改 但是这里cd需要更改的是父进程的cwd路径只有父进程的pwd修改了往后创建的子进程才会在新路径下运行因为所有子进程的环境变量都是拷贝父进程的所以cd操作不是让子进程来操作而是让父进程来亲自操作修改父进程的env之后的子进程cwd也会被修改这类需bash完成的命令叫做内建命令所以我们需要在shell中实现内建命令要做一定的判断实现cd先做对命令的判断我们判断命令是否是cd如果是就continue不需要执行下面的子进程实现命令的任务然后我们来解决如何在bash内实现cd命令我们来了解一个接口chdir(change dir)chdir()支持相对路径内核会自动根据进程当前的工作目录PCB 里的 CWD解析相对路径最终修改成功。我们来分析一下cd命令的不同情况我们使用cd命令可能会提供绝对路径或者相对路径或者cd后不带其他参数此时cd会默认回到用户家目录我们判断出命令为cd内建命令我们就在判断后就执行对应的命令考虑到cd单独命令的情况做一个判断将路径改为家目录参数数量大于1就直接cd命令的功能实现更新命令行提示符注意cd完后要更新命令行提示符不然不符合shell的功能我们要知道是先程序的路径改变再pwd改变所以我们现在是先改变了路径但是没有修改pwd环境变量所以要能够正确使用还需要去修改环境变量所以我们通过环境变量获取工作目录这个方法是不太好的我们有更好的方法使用getcwd()系统调用获取进程PCB内的cwd工作路径chdir修改工作目录时会修改PCB内的cwd我们命令行提示符成功变化路径但是我们发现环境变量里的pwd依旧没有改变我们bash是可以改变的所以我们在内部不仅要获得cwd还要修改环境变量pwd我们修改环境变量就使用系统调用putenv我们就在getpwd函数内修改环境变量设置两个字符串cwdcwdenvcwd获取工作路径的cwdenv是形成PWDcwd的字符串用于putenv去修改PWD的修改成功我们现在就成功实现了内建命令echo 的内建命令$?$env我们再实现一下echo $?的操作获取最近程序的退出结果其实再shell内有点命令会有两份命令可能有即是内建命令又是外部命令cd命令shell内部有内建命令再外部磁盘中也有cd程序这是为了能让子进程可以认识cd命令可以使用cd命令而设置的外部程序我们再实现一下echo $env这个功能吧添加环境变量表bash有两个表环境变量表和命令行参数表我们已经有了命令行参数表现在就需要添加环境变量环境变量表是bash在启动时自动使用环境变量表去初始化我们做不到从配置文件里去读取环境变量那个配置文件是一个shell脚本要读取这个脚本我们自己要新写门语言的所以我们现在就直接从父shell中获取环境变量我们现在就创建环境变量表我们通过environ来获取父shell提供的环境变量表我们调用env可以看到我们测试的环境变量也实现了环境变量的导入而argv和env都是全局表都可以被子进程继承而子进程做进程程序替换不保留命令行参数表和环境变量表所以子进程会共享alias内建命令使用unordered_map制作别名映射表综上bash说白了就是命令行解释器获取命令行解析命令行建立一个子进程替换子进程父进程等待子进程退出以此循环往复源码#include cstdio #include cstring #include cstdlib #include iostream #include unistd.h #include sys/types.h #include sys/wait.h #define COMMAND_SIZE 1024 #define FORMAT [%s%s:%s]# //命令行参数表 #define MAXARGC 128 char * g_argv[MAXARGC]; int g_argc; //环境变量表 #define MAX_ENVS 100 char * g_env[MAX_ENVS]; int g_envs 0; /// /// for test char cwd[1024]; char cwdenv[1024]; /// /// // last exit_code int last_code 0; //注 意 清 理 /// Get操 作 /// const char * GetUserName() { const char * ret getenv(USER); return ret NULL ? None : ret; } const char * GetHostName() { const char *ret getenv(HOSTNAME); return ret NULL ? None : ret; } const char * GetPwd() { //const char *ret getenv(PWD); const char *ret getcwd(cwd,sizeof(cwd)); if(ret ! NULL) { snprintf(cwdenv,sizeof(cwdenv),PWD%s,cwd); //上面这样做不是很好 //我们的环境变量cwdwnv不应该在全局空间中应该被维护在自己的环境变量表中这里更新环境变量应该是去环境变量表内查找对应环境变量存在则更新不存在则添加 putenv(cwdenv); } return ret NULL ? None : ret; } const char * GetHome() { const char *ret getenv(HOME); return ret NULL ? None : ret; } /// 获取环境变量表 /// void InitEnv() { extern char **environ; //environ指针是系统定义全局二级指针要通过extern告诉编译器 //初 始 化 环 境 变 量 表 memset(g_env,0,sizeof(g_env)); g_envs 0; // 本 来 要 从 配 置 文 件 中 获 取 的我 们 从 父 shell获 取 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] 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; } /// cd命令 /// bool Cd() { if(g_argc 1) { std::string home GetHome(); if(home.empty())return true; chdir(home.c_str()); } else if(g_argc 1){ std::string where g_argv[1]; //cd - cd ~ chdir(where.c_str()); } return true; } /// echo命令 /// bool Echo() { if(g_argc 2) { //echo hello world 不处理直接输出字符串 //echo $? //echo $PATH std::string opt g_argv[1]; if( opt $?) { std::cout last_codestd::endl; last_code 0; //清零 } else if(opt[0] ) { std::string env_name opt.substr(1); std::string env_value getenv(env_name.c_str()); if(env_value.c_str()){ std::cout env_value.c_str()std::endl; } } else { std::cout opt std::endl; } return true; } return false; } /// 获得目录 /// std::string DirName(const char *pwd) { #define SLASH / std::string dir pwd; if(dir SLASH) return SLASH; auto pos dir.rfind(SLASH); //在string中逆向查找 /s/a/ls if(pos std::string::npos)return BUG; return dir.substr(pos1); } /// 制作命令行提示符 /// void Make_Cmd_Prompt(char cmd_Prompt[],int size) { snprintf(cmd_Prompt,size,FORMAT,GetUserName(),GetHostName(),DirName(GetPwd()).c_str()); } /// 输出命令行提示符 /// void Print_Cmd_Prompt() { char prompt[COMMAND_SIZE]; Make_Cmd_Prompt(prompt,sizeof(prompt)); printf(%s,prompt); fflush(stdout); } /// 输入命令 /// bool Cin_CommandLine(char *out,int size) { 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; } /// 切割命令行 /// bool CommandParse(char *commandline) { #define SEP g_argc 0; g_argv[g_argc] strtok(commandline,SEP); //首 次 切 割 while((bool)(g_argv[g_argc] strtok(NULL,SEP))); //循 环 切 割 g_argc--; return g_argc 0 ?true:false; } /// /// fot test void PrintArgv() { for(int i 0; g_argv[i];i) { printf(argv[%d]-- %s\n,i,g_argv[i]); } printf(argc:%d\n,g_argc); } /// /// /// 普通命令执行 /// int Execute() { pid_t id fork(); if(id 0) { //child //进 程 替 换 execvp(g_argv[0],g_argv); exit(1); } // father int wstatus0; pid_t rid waitpid(id,wstatus,0); //(void)rid; //使用一下防止警告 if(rid 0) { last_code WEXITSTATUS(wstatus); } return 0; } /// 检测内建命令 /// bool CheckAndExecBuiltin() { //如何检测内建命令呢 std::string cmd g_argv[0]; if(cmd cd) { Cd(); return true; } else if(cmd echo) { return Echo(); } //else if(cmd export) //{ // Export();//分析export后面参数获取Key和 Value值去环境变量表中查找找不到就新增 //} return false; } /// main 主函数 /// int main() { //shell 启 动 的 时 候 从 系 统 中 获 取 环 境 变 量 // 我们的环境变量信息应该从父shell统一来 InitEnv(); while(1) { //1、输 出 命 令 行 提 示 符 Print_Cmd_Prompt(); //2、获 取 用 户 输 入 的 命 令 char commandline[COMMAND_SIZE]; if(!Cin_CommandLine(commandline,sizeof(commandline)))continue; //3、分 析 命 令 行 ls -a -l -- ls -a -l //全 局 的 命 令 行 参 数 表 if(!CommandParse(commandline))continue; //PrintArgv(); // 4、检 测 并 处 理 内 建 命 令 if(CheckAndExecBuiltin())continue; //不需要再让子进程创建程序完成命令 //5、执 行 命 令 //创建子进程 Execute(); } //cleanup(); }