当前位置: 首页 > news >正文

打造你的专属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(); }
http://www.cnnetsun.cn/news/4009643.html

相关文章:

  • AI Agent上下文窗口管理:突破内存墙的智能工作台架构与实践
  • 惠州网站建设效果怎么衡量?揭秘低成本高转化的实操干货与避坑指南
  • 从零基础到月薪过万:普通人如何通过网站建设与运营就业逆袭
  • 寻找专业合川网站建设公司?这些坑你必须避开,否则预算白扔还耽误生意
  • 深度解析上海华谊集团建设有限公司网站:揭秘基建背后的硬核实力与未来蓝图
  • 深度解析七冶建设集团网站江苏:一站式服务入口与企业形象展示窗口
  • 建设电商网站所需硬件全解析:从服务器选型到网络架构的深度避坑指南
  • 移动应用测试实战:从功能到安全,构建O2O应用质量护城河
  • 【RT-DETR涨点改进】TGRS 2025 | 全网独家创新、Conv卷积改进篇 | 利用LLSKM可学习的内核卷积,含二次创新,助力红外小目标检测,遥感目标检测任务,有效涨点改进点
  • 为什么你做的陕西网站建设设计公司网站没人看?揭秘背后那些被忽视的运营真相与SEO优化细节
  • 中山网站建设gdyouzi揭秘:如何从零打造高转化率的官方网站?
  • CSDN 付费专栏连载:雷达脉冲压缩与匹配滤波完整原理、工程实现、国产雷达应用全解
  • 哈尔滨网站建设1元钱是真的吗?揭秘行业真相与避坑指南
  • 绍兴网站建设报价全解析:从几百到几万,到底哪个才是你的真需求?
  • 网站建设需求文档模板:从0到1打造高转化官网的终极避坑指南
  • 拒绝套路与浮夸中山最好的网站建设实战指南:从底层逻辑到最终交付的全过程深度复盘
  • 企业TikTok营销如何避坑?多国家账号矩阵的音乐版权管理方法与落地执行流程
  • 永乐网站建设怎么做?从零开始打造高转化企业官网的实战避坑指南
  • 色无极网站正在建设中:一场关于深度与温度的数字化重构之旅
  • Qwen与Grok更新解析:从模型微调到AI代码助手的工程实践
  • 揭秘那些功能多免费网站建设平台,小白也能轻松搭建高逼格网站
  • 珠海图远建设公司网站揭秘:一家老牌建设企业如何在数字化浪潮中坚守初心与匠心
  • 深入调研信息化建设现状:基于多地区乡镇网站的调查报告与优化建议
  • 无构建开发实践:在单HTML页面中快速集成Vue3与Element Plus
  • 从零到一打造你的数字名片:深入解析24小时学会网站建设 pdf下载资源的核心价值与实操指南
  • 深入探讨电子商务网站建设的意义,为什么它对企业生存至关重要
  • 深度解析网站建设开发文档:从零基础到资深工程师的必经之路
  • DBeaver 新手入门指南(超详细图文流程版)
  • SPT-AKI存档编辑器完整上手指南:5分钟救活你的塔科夫单机档
  • 数据出境安全评估新规落地,跨境数据流动迎强监管