Linux实现简易版Shell的代码详解
一、程序流程分析我们日常使用Bash时通过输入命令执行相应的操作比如那么Bash是如何进行工作的呢观察一下就会发现首先Bash会打印命令行提示符包括当前用户、主机名以及路径。之后会等待我们输入相关命令然后根据命令执行相应程序。程序执行结束后就会再次打印命令行提示符等待我们再次输入指令…很明显是一个死循环。总结一下Bash的大体工作流程是1.打印命令行提示符包括当前用户名、主机名、路径2.获取用户输入的命令行3.解析命令行4.执行命令5.继续打印命令行提示符…注意当Bash执行非内建命令时会创建一个子进程由子进程完成相应的工作Bash自己等待子进程工作结束。而对于内建命令如cdecho需要Bash自己执行任务。二、代码实现接下来我们开始按照上述工作流程一步步实现我们的简易Shell。1. 打印命令行提示符CentOS的命令行提示符主要包含三个内容当前用户名、主机名和当前所在路径。之前学习Linux环境变量时我们了解到环境变量USER、HOSTNAME、PWD中存储着这些内容。所以我们使用getenv函数获取环境变量相应的值。代码实现12345678910111213141516171819202122232425262728#include iostream#include cstdio#include cstring#include cstdlib#include unistd.h#include sys/types.h#include sys/wait.h//获取当前用户名constchar* GetUserName(){constchar* name getenv(USER);returnname nullptr ?none: name;}//获取当前主机名constchar* GetHostName(){constchar* name getenv(HOSTNAME);returnname nullptr ?none: name;}//获取当前工作路径constchar* GetPwd(){constchar* pwd getenv(PWD);returnpwd nullptr ?none: pwd;}接下来调用这些函数形成一串命令行提示符并打印12345678//创建并打印命令行提示符voidPrintCommandPrompt(){charCommandPrompt[1024];//这里为了区分Bash用大括号snprintf(CommandPrompt,sizeofCommandPrompt,{%s%s %s} , GetUserName(), GetHostName(), GetPwd());std::cout CommandPrompt std::flush;// 打印并刷新缓冲区}注意这里打印结束后由于没有换行所以缓冲区可能不会刷新导致命令行提示符没有出现在屏幕上。因此这里我们需要主动刷新缓冲区。进行测试12345intmain(){PrintCommandPrompt();return0;}运行结果可以看到用户名和主机名都正常打印但在我们写的shell中当前所在路径是绝对路径太过冗长所以可以对生成的路径进行一些处理1234567891011121314//创建并打印命令行提示符voidPrintCommandPrompt(){charCommandPrompt[1024];//处理当前工作路径std::string pwd GetPwd();if(pwd !/)// 如果是根目录则直接输出pwd pwd.substr(pwd.rfind(/) 1);// 查找最后一个/并从下一个位置开始分割//这里为了区分Bash用大括号snprintf(CommandPrompt,sizeofCommandPrompt,{%s%s %s} , GetUserName(), GetHostName(), pwd.c_str());std::cout CommandPrompt std::flush;// 打印并刷新缓冲区}运行结果2. 获取用户输入的命令行在Bash输入命令时往往会带上一些选项并用空格隔开。因此使用scanf或cin读入时会以空格作为分隔符达不到想要的效果。这里我们选择用fgets进行读取。另外主函数当中命令的全部处理应该放在一个死循环当中这样才能完成用户多次派发的任务。1234567891011121314151617181920212223242526272829303132333435//获取用户输入的命令行boolGetCommandLine(char* command,intsize){//从键盘读取命令if(fgets(command, size, stdin) nullptr){returnfalse;}//注意清理末尾的\nif(strlen(command) 0)returnfalse;command[strlen(command) - 1] \0;returntrue;}intmain(){while(true){//打印命令行提示符PrintCommandPrompt();//读入命令charcommand[1024];if(!GetCommandLine(command,sizeofcommand))// 若读取错误就continue重新读取{std::cout 读取错误 std::endl;continue;}//打印输入的命令行std::cout command std::endl;}return0;}注意使用fgets从键盘读取字符串时会附带末尾’\n’需要进行处理。运行测试可以看到程序成功读入了我们的命令包括空格并且将命令回显出来。3. 命令行解析命令行解析的过程中需要将用户读入的命令行进行分割提取出要执行的程序名以及选项。这里我们创建两个全局变量g_argc和g_argv分别存储解析到的命令行参数以及参数个数方便后续指令的执行。注这里的命令行分割操作由strtok函数完成。代码实现12345678910111213141516171819202122232425262728293031323334353637383940414243444546//全局变量存储命令行参数及其个数intg_argc 0;char* g_argv[128];//命令行解析boolCommandParse(char* command){g_argc 0;for(char* p strtok(command, ); p ! nullptr; p strtok(nullptr, )){g_argv[g_argc] p;}returng_argc 0 ?false:true;}intmain(){while(true){//打印命令行提示符PrintCommandPrompt();//读入命令charcommand[1024];if(!GetCommandLine(command,sizeofcommand))// 若读取错误就continue重新读取{std::cout 输入错误 std::endl;continue;}// //打印输入的命令行// std::cout command std::endl;//命令行解析if(!CommandParse(command)){std::cout 命令行解析失败 std::endl;continue;}//打印解析结果for(inti 0; i g_argc; i){std::cout g_argv[i] std::endl;}}return0;}测试结果程序成功地按照空格将我们输入的命令行参数提取了出来。接下来根据提取到的参数就可以执行相关指令了。4. 执行命令对于非内建命令Bash会创建子进程并让子进程执行而对于内建命令则是由Bash自己执行。因此执行命令之前需要先判断该命令是否是内建命令然后进行相应的操作。为什么会有内建命令效率执行内建命令通常比执行外部命令更快因为避免了创建新进程的开销。Shell 功能许多内建命令直接操作 Shell 的内部状态例如改变当前工作目录 (cd)、设置环境变量 (export)、控制 Shell 行为等这些功能如果作为外部命令实现会更加复杂或不可能。基本操作一些非常基础和常用的操作需要作为内建命令提供以确保 Shell 的基本功能可用内建命令的处理在Bash当中可以使用type命令判断一个命令是否是内建命令例如当然除了cd和echo命令还有printf、help等内建命令。本次实现中为了能够让大家深刻理解shell运行原理同时降低实现难度博主就只针对cd和echo这两个内建命令进行简易实现。内建命令的检查和处理12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758//内建命令处理boolCheckAndExecBuiltin(){//取出命令行参数表的首元素判断是否为内建命令如果是则直接执行std::string str g_argv[0];if(str cd){Cd();returntrue;}elseif(str echo){Echo();returntrue;}//else if(...)returnfalse;// 不是内建命令直接返回}intmain(){while(true){//打印命令行提示符PrintCommandPrompt();//读入命令charcommand[1024];if(!GetCommandLine(command,sizeofcommand))// 若读取错误就continue重新读取{std::cout 输入错误 std::endl;continue;}// //打印输入的命令行// std::cout command std::endl;//命令行解析if(!CommandParse(command)){std::cout 命令行解析失败 std::endl;continue;}// //打印解析结果// for(int i 0; i g_argc; i)// {// std::cout g_argv[i] std::endl;// }//内建命令的处理if(CheckAndExecBuiltin()){continue;// 是内建命令执行完毕就回去重新打印提示符}//不是内建命令由子进程处理}return0;}cd的简易实现cd的功能是改变当前工作路径。如果创建子进程其只能修改它自己的工作路径而无法修改Bash的工作路径。因此cd操作需要Bash亲自完成。我们获取到命令行参数后可以通过调用chdir函数实现1234567891011121314voidCd(){std::string dst;if(g_argc 1 || g_argv[1] std::string(~))// 处理进入家目录的情况{dst GetHome();if(dst )return;}else{dst g_argv[1];}chdir(dst.c_str());}测试结果可以看到使用cd后当前路径貌似并没有发生改变。为什么呢实际上chdir确实起到了效果但是我们的命令行提示符中当前工作路径是从环境变量中获取的环境变量中的PWD并没有发生改变。因此修改当前工作路径之后要顺带着修改环境变量PWD的值。其次当前工作路径改变后修改环境变量之前要获取到当前工作路径就需要使用getcwd函数。进行键值处理后使用putenv修改环境变量。
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2481010.html
如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!