news 2026/8/13 23:23:34

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

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
打造你的专属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可以实现
但这里我们使用一个接口来实现!strtok

str为被切割的字符串,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来获取pos+1(pos后面是/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函数内修改环境变量,设置两个字符串,cwd,cwdenv,cwd获取工作路径的!cwdenv是形成PWD="cwd"的字符串,用于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++] = "HAHA=for_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_code<<std::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(pos+1); } /// 制作命令行提示符 /// 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 wstatus=0; 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(); }
版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/8/13 23:22:43

AI Agent上下文窗口管理:突破内存墙的智能工作台架构与实践

1. 项目概述&#xff1a;当AI Agent遇上“内存墙”最近和几个做AI应用的朋友聊天&#xff0c;大家不约而同地提到了同一个“甜蜜的烦恼”&#xff1a;模型能力越来越强&#xff0c;但上下文窗口&#xff08;Context Window&#xff09;的“内存墙”也越来越明显。无论是调用GPT…

作者头像 李华
网站建设 2026/8/13 23:22:18

惠州网站建设效果怎么衡量?揭秘低成本高转化的实操干货与避坑指南

在惠州这片热土上打拼的企业家们,每天都在为生计奔波,也为企业的未来焦虑。特别是在如今这个移动互联网高度发达的时代,大家都有一个共识:公司得有个网站。但说实话,很多老板在网上搜了一圈,看到报价从几千到几万不等,心里全是问号:这玩意儿到底有没有用?做出来的网站…

作者头像 李华
网站建设 2026/8/13 23:21:39

从零基础到月薪过万:普通人如何通过网站建设与运营就业逆袭

说实话,每次看到后台收到那些私信,问我“现在入行晚不晚”、“零基础能不能干”的时候,我心里总是五味杂陈。大家焦虑,因为大环境在变,互联网的风口好像一夜之间就转了方向。但如果你把目光放长远一点,把“网站建设与运营就业”这六个字嚼碎了看,你会发现,这不仅仅是一…

作者头像 李华
网站建设 2026/8/13 23:21:37

寻找专业合川网站建设公司?这些坑你必须避开,否则预算白扔还耽误生意

在重庆的大西北,有一个充满魅力和历史底蕴的地方,那就是合川。这里三江汇流,风景如画,更有一群敢想敢干、务实肯干的老板们。在这个互联网深度渗透每个行业的今天,很多合川的企业家都在问我同一个问题:“我想做个网站,到底该找谁?是不是找家便宜的随便弄弄就行?”说实…

作者头像 李华
网站建设 2026/8/13 23:20:45

深度解析上海华谊集团建设有限公司网站:揭秘基建背后的硬核实力与未来蓝图

最近好多朋友在后台私信我,问起一个老牌子的问题:上海华谊集团建设有限公司。说实在的,听到这个名字,很多在上海或者江浙沪地区做过工程的朋友,心里可能都会咯噔一下,然后泛起一丝复杂的情绪。那是一种夹杂着敬畏、熟悉,又带着点“懂的都懂”的默契。毕竟在建筑这行混了…

作者头像 李华
网站建设 2026/8/13 23:19:52

深度解析七冶建设集团网站江苏:一站式服务入口与企业形象展示窗口

在这个数字化浪潮席卷一切的今天,我们似乎已经习惯了隔着屏幕去认识一个品牌,去感知一家企业的温度。对于许多在长三角地区奔波的同行、客户或者是刚刚踏入建筑行业的新人来说,“七冶建设集团网站 江苏”这四个字,可能不仅仅是一串搜索关键词,它更像是一把钥匙,开启了一扇…

作者头像 李华