Linux实现自主Shell命令行解释器

张开发
2026/4/4 7:21:25 15 分钟阅读
Linux实现自主Shell命令行解释器
1. 获取用户名的接口通过环境变量来获取我们需要用到的接口getenv1234567891011121314151617181920//获取用户名constchar* GetUserName(){constchar* name getenv(USER);returnname NULL ?None: name;}//获取主机名constchar* GetHostName(){constchar* hostname getenv(HOSTNAME);returnhostname NULL ?None: hostname;}//获取当前路径constchar* GetPwd(){constchar* pwd getenv(PWD);returnpwd NULL ?None: pwd;}2. 等待用户输入接口当我们没有输入时我们会发现命令行会卡在这里等待我们输入我们也让我们自己的命令行能等待输入我们可以采用fgets以文件形式读取一行也可以使用gets读取一行字符串我们接下来进行C/C混编的方式因为我们后面会用到系统调用而这些系统调用都是用C写的如果我们纯用C来实现的话可能会要适配某些接口。我们下来用fgets实现效果展示我们会发现最后多了一个空行这里为什么会多一个空行呢因为我们在输入完字符串后还按了一次回车我们不想让它有这一行空行该怎么办我们在输入字符串后后面还会有个\n,比如我们输入的是ls -a -l最后再按一次回车就变成了ls -a -l \n,我们只需要输入完之后把最后的\n置为0就好了效果展示小tips:这里会不会求出的字符串长度为0然后再-1发生越界呢答案是不会的因为我们最后至少还要敲一次回车键所以这个字符串的最小长度为1。3. 将上述代码进行面向对象式的封装我们先认识一个新的接口snprintf1234567891011121314151617181920212223242526272829303132333435363738394041//制作命令行提示符voidMakeCommandline(charcom_prompt[],intsize){snprintf(com_prompt, size, FORMAT, GetUserName(), GetHostName(), GetPwd());//我们想让[%s%s %s]# 以后能随便调整所以我们define一下}//打印命令行提示符voidPrintCommandline(){charprompt[COMMAND_SIZE];MakeCommandline(prompt,sizeof(prompt));//先制作printf(%s, prompt);//再打印fflush(stdout);}//获取用户输入boolGetCommandline(char* out,intsize){//ls -a -l ls -a -l \nconstchar* c fgets(out, size, stdin);//从标准输入里获取放到out当中if(c NULL)return1;out[strlen(out) - 1] 0;//清理\nif(strlen(out) 0)returnfalse;//对于我们用户来说有可能获取到的字符串的长度为0为0直接return falsereturntrue;//否则return true}intmain(){//printf([%s%s %s]# ,GetUserName(),GetHostName(),GetPwd());//1. 打印命令行提示符PrintCommandline();//2.获取用户输入charcommandline[COMMAND_SIZE];//定义一个数组if(GetCommandline(commandline,sizeof(commandline)))//如果获取成功{printf(echo %s\n, commandline);//回显一下我们输入的内容用作测试}return0;}我们在shell中可以一直输入我们的程序输入一次就结束了所以shell永远不退出。我们应当不断地获取用户输入。4. 命令行解析我们在传字符串的时候不能“ls -a -l”整体传入我们要将传入的字符串进行变形将这一个字符串拆成“ls” -a -l。而且我们的命令行也不能在shell中直接替换而要创建子进程。我们将字符串切成这样那么如何快速的找到每一个元素呢命令行参数表将打散的字符串以NULL结尾放到g_argv[]里面。在这里又来认识一个新的接口strtok这个接口第一次切的时候第一个参数传的是要分割的字符串的起始地址如果要接着切的话第一个参数就必须传NULL当切除完毕返回值就为空表示没有字符串了。分隔符是const char*所以我们不能传单引号而要传双引号123456789101112131415161718192021222324//命令行分析boolCommandParse(char* commandline){#define SEP g_argc 0;//每次进来初始化为0//ls -a -l ls -a -lg_argv[g_argc] strtok(commandline, SEP);while(g_argv[g_argc] strtok(nullptr, SEP));//再次想切的话传commandline就不对了再要切历史字符串就得把它设为nullptr,再次切的话分隔符依旧是SEP,如果切成了返回的就是下一个字符串的起始地址//为什么可以这样切呢因为再次切字串时它一直切一直切最后就会会变成NULL,切成NULL首先会把g_argv数组置为NULL符合命令行参数表的设定NULL也会作为while的条件判断最后就直接结束了//并且g_argc也会统计出命令行参数有多少个g_argc--;//因为NULL也被统计到了里面returntrue;}//测试形成的表结构voidPrintArgv(){for(inti 0; i g_argc; i){printf(argv[%d]-%s\n, i, g_argv[i]);}printf(argc:%d, g_argc);}5. 执行命令由于我们当前的进程还有自己的任务所以我们将执行命令交给子进程来完成那么就需要程序替换execvp1234567891011121314//执行命令intExecute(){pid_t id fork();if(id 0){//子进程execvp(g_argv[0], g_argv);exit(1);}//父进程pid_t rid waitpid(id, nullptr, 0);return0;}6. 路径切割系统的路径名只有一个我们自己写的会跟一长串所以我们对路径进行切割。C中有个命令rfind从后向前找substr截字符串12345678910//路径切割std::string DirName(constchar* pwd){#define SLASH /std::string dir pwd;if(dir SLASH)returnSLASH;auto pos dir.rfind(SLASH);if(pos std::string::npos)returnBUG;//这里表示没有找到/returndir.substr(pos 1);}7. 解决cd命令路径不变到目前我们会发现我们执行cd命令时路径不发生改变因为目前所有的命令都是子进程执行的子进程改变路径时改的是自己的pwd父进程bash的环境变量并没有改变我们真正要改的是父进程的路径因为把父进程的路径改了往后再创建子进程所有的子进程就会在新的路径下执行因为所有的子进程的PCB都是拷贝父进程的PCB,因此cd这样的命令不能让子进程去执行而要让父进程亲自执行这种命令叫做内建命令如何让bash亲自去执行呢我们先来认识一个新的接口chdir1234567891011121314151617181920212223242526272829303132333435363738//处理cd命令boolcd(){if(g_argc 1)//表明只是cd,没有带任何参数{std::string home GetHome();//将home的路径拿过来if(home.empty())returntrue;//如果是空就相当于环境变量获取失败了chdir(home.c_str());//走到这里不为空就把当前路径切换成家路径了}else{std::string where g_argv[1];//cd - / cd ~if(where -){}elseif(where ~){}else{chdir(where.c_str());}}}//检测并处理内建命令boolCheckAndExecBuiltin(){std::string cmd g_argv[0];if(cmd cd)//如果是内建命令{cd();returntrue;//是内建命令}returnfalse;//否则不是内建命令}8. 解决cd后环境变量未发生变化我们再切换路径后会发现路径变了但是环境变量中的路径并没有变。原因是路径发生变化后环境变量没有进行刷新所以我们要将新的路径更新到环境变量中。这里我们来认识一个系统调用getcwd获取当前进程的工作路径。123456789101112//获取当前路径constchar* GetPwd(){//const char* pwd getenv(PWD);constchar* pwd getcwd(cwd,sizeof(cwd));if(pwd ! NULL){snprintf(cwdenv,sizeof(cwdenv),PWD%s, cwd);//获得环境变量putenv(cwdenv);//将环境变量导给当前进程}returnpwd NULL ?None: pwd;}9. echo命令echo命令也是内建命令我们可以用它echo hello在屏幕上打印,echo $?查看上个进程的退出码echo $PATH查看环境变量。123456789101112131415161718192021222324252627//处理echo命令voidEcho(){if(g_argc 2)//意思是echo后面必须得跟东西{//echo heool world//echo $?//echo $PATHstd::string opt g_argv[1];if(opt $?)//输出上一个程序退出的退出码{std::cout lastcode std::endl;lastcode 0;//lastcode清零}elseif(opt[0] $)//如果第一个字符时是$那么说明查环境变量的内容了{std::string env_name opt.substr(1);//去掉$后的内容就是环境变量的名字constchar* env_value getenv(env_name.c_str());if(env_value)std::cout env_value std::endl;}else{std::cout opt std::endl;}}}10. 获取环境变量shell启动时需要从系统中获取环境变量,但是我们还做不到从配置文件中读今天我们直接从父shell中拿就可以了,我们自己维护一张环境变量表然后将表导进环境变量空间就行。我们又用到一个接口environ1234567891011121314151617181920212223242526//初始化环境变量表voidInitEnv(){externchar** environ;//声明一个环境变量所对应的信息memset(g_env, 0,sizeof(g_env));//将表中的信息全部置为0g_envs 0;//本来要从配合文件中来//今天从父shell中来//1. 获取环境变量for(inti 0; environ[i]; i){g_env[i] (char*)malloc(strlen(environ[i]) 1);//1.2拷贝strcpy(g_env[i], environ[i]);//把父进程环境变量里的值拷贝给g_envg_envs;}g_env[g_envs] NULL;//2.导入环境变量for(inti 0; g_env[i]; i){putenv(g_env[i]);}environ g_env;//3.clean清理}11. 总结用下图的时间轴来表示事件的发生次序。其中时间从左向右。shell由标识为sh的方块代表它随着时间的流逝从左向右移动。shell从用户读入字符串ls。shell建立一个新的进程然后在那个进程中运行ls程序并等待那个进程结束。

更多文章