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

【Linux】模拟实现Shell(上)

1.能处理普通命令

1.1 打印命令行提示符

在我们自己的命令行中首先会显示用户名、主机名、当前所在的路径。

得到这些变量的方法很简单,通过环境变量获得,获取环境变量的函数就是getenv

#include <stdio.h>
#include <string.h>
#include <stdlib.h>const char* UserName() //获取用户名
{const char* user_name = getenv("USER");return user_name == nullptr ? "None" : user_name;
}const char* HostName() //获取主机名
{const char* host_name = getenv("HOSTNAME");return host_name == NULL? "None" : host_name;
}const char* GetPwd() //获取当前路径
{const char* pwd = getenv("PWD");return pwd == NULL ? "None" : pwd;
}

都获取到之后把这些内容打印出来,为了和原来的Shell命令行做一个区分,这里最后一个字符$用#代替。

int main()
{//打印命令行printf("%s@%s:%s# ",UserName(), HostName(),GetPwd());return 0;
}

1.2 获取命令行参数

当我们打印完命令行之后,就是等待用户输入,等待用户输入时我们不能选取scanf函数,因为这个函数是以空格作为分隔符的,比如说我们要输入ls -l -a这个命令,在Shell看来其实是"ls -l -a"这个字符串。

获取字符串有很多方法,这里我选择使用fgets函数。

fgets就是从指定的文件流中按行获取指定信息,缓冲区大小是size,获取成功时返回值的就是s的地址,失败是就是NULL,FILE*就是标准输入。

#define COMMAND_SIZE 1024
void GetArguments() //获取命令行参数
{char commandline[COMMAND_SIZE];char* arg = fgets(commandline, sizeof(commandline), stdin);if(arg == NULL) //获取失败 return;printf("%s\n", commandline);
}

我们把我们输入的内容用printf回显一下,然后看结果。

int main()
{//打印命令行printf("%s@%s:%s# ",UserName(), HostName(),GetPwd());//获取命令行参数GetArguments();return 0;
}

这里多打了一个换行,因为我们在输入结束的时候自己按了回车键,其实输入的是"ls -a -l\n", 如果不想让这里多一个换行,直接让commandline的最后一个字符设为0。

void GetArguments()
{char commandline[COMMAND_SIZE];char* arg = fgets(commandline, sizeof(commandline), stdin);if(arg == NULL) //获取失败 return;commandline[strlen(commandline)-1] = 0; //清理多余的\n                                                                                                               printf("%s\n", commandline);
}

当我们什么都没输入的时候这种写法会有越界问题吗?比如访问-1的位置?答案是不会,因为我们什么都没输入的时候,至少会按一次回车,按了回车commandline长度就是1,再-1就是0,把0位置赋值成0,此时这个字符串就是一个空串。

现在就没有多余的\n了,但是在Shell中我们直接按回车是什么都没有的,我们现在的Shell按回车会换行,而且我们的Shell只能执行一次,解决方法如下

  • 用while让我们自己的Shell一直执行
  • 让GetArguments函数返回值为bool类型,根据返回中在函数外部进行判断
#define COMMAND_SIZE 1024
bool GetArguments() //获取命令行参数
{char commandline[COMMAND_SIZE];char* arg = fgets(commandline, sizeof(commandline), stdin);if(arg == NULL) //获取失败 return false;commandline[strlen(commandline)-1] = 0; //清理多余的\nif(strlen(commandline) == 0) //如果是直接回车的情况return false;return true;
}int main()
{while(1){//打印命令行提示符PrintCmdPrompt();//获取命令行参数if(!GetArguments()) //获取失败continue;printf("%s\n", commandline); //获取成功就回显}return 0;
}

但是此时的commandline是函数里的一个局部变量,我们要把它放在全局,以参数的形式传给GetArguments函数,这样外面就能拿到了。

#define COMMAND_SIZE 1024
bool GetArguments(char* commandline, int size) //获取命令行参数
{char* arg = fgets(commandline, size, stdin);if(arg == NULL) //获取失败 return false;commandline[strlen(commandline)-1] = 0; //清理多余的\nif(strlen(commandline) == 0) //如果是直接回车的情况return false;return true;
}int main()
{while(1){//打印命令行提示符PrintCmdPrompt();//获取命令行参数char commandline[COMMAND_SIZE];if(!GetArguments(commandline, sizeof(commandline))) //获取失败continue;printf("%s\n", commandline); //获取成功就回现}return 0;
}

现在我们只按回车就什么也没显示了,并且可以多次对我们自己的Shell进行操作。

1.3 解析命令行参数

我们输入的是一个字符串,比如 "ls -l -a",但是我们要把这个字符串分解成"ls"  "-l"  "-a"的形式,并且存放在一个数组里,这个数组以NULL结尾,它就是我们自己的命令行参数表

这里我选择用strtok函数分割这个字符串,str是要切割的字符串,delim是要分割的字符,这里我们就是以空格做分隔符。

  • 首次调用时,它会从传入的字符串中分割出第一个子字符串,并返回指向该子字符串的指针
  • 后续调用时,第一个参数传入 NULL,它会继续从上次分割的位置向后处理,返回下一个子字符串的指针
  • 当没有更多子字符串可分割时,返回 NULL

例如,用 strtok("a,b,c", ",")逗号为分隔符,首次调用会返回指向 "a" 的指针,再次调用(第一个参数为 NULL)返回指向 "b" 的指针,第三次调用返回指向 "c" 的指针,第四次调用返回 NULL。

定义全局的argc和argv,因为命令函参数表以NULL结尾,而strtok分割完之后的返回值就是NULL,并且以NULL结尾还可以作为while循环退出的条件。

#define DELIM " " //分隔符要用双引号
char *g_argv[128];
int g_argc = 0;
bool CommandParse(char *commandline)
{g_argc = 0;//每次都要置0,否则argc会被累加g_argv[g_argc++] = strtok(commandline, DELIM);while( g_argv[g_argc++] = strtok(nullptr, DELIM) );g_argc--; //后置++导致argc多加了一次,这里要-1return g_argc > 0 ? true : false; //返回时判断一下 
}

如果argc的数量等于0的话,就证明我们什么也没输入,就是按了一个回车,这种情况下也就不用往后执行了,返回false,如果大于0返回true,再往后执行。

这样命令就被我们分割并且放在以NULL结尾的argv数组里了,我们可以把argv和argc都打印出来看看。

void PrintArg()
{for(int i = 0; g_argv[i]; i++){printf("argv[%d]:%s\n", i, g_argv[i]);}printf("argc:%d\n", g_argc);
}int main()
{while(1){//打印命令行提示符PrintCmdPrompt();//获取命令行参数char commandline[COMMAND_SIZE];if(!GetArguments(commandline, sizeof(commandline))) //获取失败continue;//解析命令行参数if(!CommandParse(commandline))continue;PrintArg();}return 0;
}

1.4 执行命令

我们输入了命令就要执行,但是不能直接在这个进程里执行,我们要创建子进程,然后进行进程的替换,程序替换相关的函数如下。

因为我们前面已经得到参数列表的数组,而我们的程序又是不带路径的,让函数自己从环境变量里找,目前我们也不需要传环境变量,所以这里要选择execvp函数。

进程等待的函数选择waitpid

  • 当正常返回的时候waitpid返回收集到的⼦进程的进程ID
  • 如果设置了选项WNOHANG,⽽调⽤中waitpid发现没有已退出的⼦进程可收集,则返回0
  • 如果调⽤中出错,则返回-1,这时errno会被设置成相应的值以指⽰错误所在;

  • pid: Pid=-1,等待任⼀个⼦进程。与wait等效,Pid>0.等待其进程ID与pid相等的⼦进程。
  • wstatus:(是输出型参数):WIFEXITED(status): 若为正常终⽌⼦进程返回的状态,则为真(查看进程是否是正常退出) ;WEXITSTATUS(status): 若WIFEXITED⾮零,提取⼦进程退出码(查看进程的退出码)
  • options:默认为0,表⽰阻塞等待,WNOHANG非阻塞等待
int last_exit_code = 0;
int Execute()
{pid_t id = fork();if(id == 0) //子进程:程序替换{execvp(g_argv[0], g_argv);}//父进程:阻塞等待int status;pid_t rid = waitpid(id, &status, 0);if(rid > 0){if(WIFEXITED(status)) //正常退出{last_exit_code = WEXITSTATUS(status); //获取退出码}elselast_exit_code = 100;}return 0;
}
int main()
{while(1){//1.打印命令行提示符PrintCmdPrompt();//2.获取命令行参数char commandline[COMMAND_SIZE];if(!GetArguments(commandline, sizeof(commandline))) //获取失败continue;//3.解析命令行参数if(!CommandParse(commandline))continue;//4.执行命令Execute();}return 0;
}

这样一个能执行普通命令的Shell就初步实现好了,随便输入几个命令试试。

但是我们执行cd命令切换路径的时候,发现并不起作用。

因为每个进程都有自己的当前路径,此时cd是子进程在执行,无法改变父进程的路径,我们要影响的是父进程。

2.能处理内建命令

2.1 cd

像cd这样的命令,就是内建命令,所以我们在执行命令之前还要检测这个命令是否为内建命令。检查的方法就是if判断一下。

bool CheckAndExeBuiltinCmd()
{std::string cmd = g_argv[0];if(cmd == "cd")return true;return false;
}

cd这个命令后面可以加相对路径也可以加绝对路径,也可以什么都不跟,什么都不跟的时候就是去到家目录。

这里的cd命令要父进程执行,需要用到一个函数chdir,更改进程的工作路径,谁调用它就更改谁的。

  • cd什么都不加的话,argc就为1,我们就要将路径改到家目录,而家目录我们也要获取,直接通过环境变量获得即可
  • cd后面跟路径,就直接chdir到这个路径
const char* GetHome() //获取家目录
{const char* home = getenv("HOME");return home == NULL ? "None" : home;
}
void Cd()
{if(g_argc == 1) //cd后什么都不加,回到家目录{std::string home = GetHome();if(home == "None") //没获取到家目录直接返回return; chdir(home.c_str());}else //cd后加路径{std::string targ_dir = g_argv[1];chdir(targ_dir.c_str()); //直接去目标路径}return;
}bool CheckAndExeBuiltinCmd() //检查并执行内建命令
{std::string cmd = g_argv[0];if(cmd == "cd"){Cd();return true;}return false;
}

内建命令在父进程执行,不用再往后进行Execute的步骤了,因为Execute里是创建子进程执行的,如果是内建命令,执行完直接continue就好了。

int main()
{while(1){//1.打印命令行提示符PrintCmdPrompt();//2.获取命令行参数char commandline[COMMAND_SIZE];if(!GetArguments(commandline, sizeof(commandline))) //获取失败continue;//3.解析命令行参数if(!CommandParse(commandline))continue;//4.检测是否为内建命令,是内建命令就执行if(CheckAndExeBuiltinCmd())continue; //5.执行命令Execute();}return 0;
}

我们再用cd命令时,就可以进行路径的切换了。

但是我们会发现新问题出现了,命令行提示符的路径怎么没变?并且思考一个问题,环境变量先变还是当前进程的工作路径先变?

肯定是进程的路径先变了,然后shell更新环境变量时,环境变量才改变,这个更新的工作要shell来做,但是我们的这个shell没有这个步骤,所以我们在打印命令行提示符的时候,拿环境变量获取的都是旧的路径数据。

所以此时获取路径要用系统调用的函数getcwd获取当前工作路径。

char cwd[1024]; //先定义一个全局的cwd
const char* GetPwd() //获取当前路径
{//const char* pwd = getenv("PWD"); //环境变量获取const char* pwd = getcwd(cwd, sizeof(cwd)); //系统调用获取return pwd == NULL ? "None" : pwd;
}

此时我们命令行提示符的路径也就跟着改变了。

但是我们查看环境变量的时候,环境变量依旧没有更新。

所以我们需要再更新一下环境变量,这里会用到两个函数snprintfputenv

  • snprintf:用于将格式化的数据写入字符串。这个函数会根据指定的格式将数据转换成字符串,并将结果保存到提供的缓冲区中。
  • putenv:如果环境变量name已经存在,则其值会被更新为value;如果name不存在,则会在环境中新建这一环境变量

先用snprintf对字符串进行格式化,放进cwdenv里,然后用putenv把这个格式化好的内容导入到环境变量里。

char cwd[1024]; //先定义一个全局的cwd
char cwdenv[1024]; //全局的cwd环境变量
const char* GetPwd() //获取当前路径
{//const char* pwd = getenv("PWD"); //环境变量获取const char* pwd = getcwd(cwd, sizeof(cwd)); //系统调用获取if(pwd != NULL){snprintf(cwdenv, sizeof(cwdenv), "PWD=%s", cwd); //格式化putenv(cwdenv);}return pwd == NULL ? "None" : pwd;
}

现在我们切换路径后,再看这个环境变量就会跟着变了。

2.2 echo

echo这个命令也是内建命令,echo可以打印字符串,可以打印最近一个程序的退出码,也可以打印环境变量。

bool CheckAndExeBuiltinCmd() //检查并执行内建命令
{std::string cmd = g_argv[0];if(cmd == "cd"){Cd();return true;}else if(cmd == "echo"){//echo实现逻辑return true;}return false;
}

先实现用 echo $? 打印程序退出码的代码。

int last_exit_code = 0;
void Echo()
{//只输入了echo,不做处理直接返回 if(g_argc == 1) return;std::string opt = g_argv[1];if(opt == "$?") //打印最近一个程序的退出码{std::cout << last_exit_code << std::endl;last_exit_code = 0; //置0}return;
}
bool CheckAndExeBuiltinCmd() //检查并执行内建命令
{std::string cmd = g_argv[0];if(cmd == "cd"){Cd();return true;}else if(cmd == "echo"){Echo();return true;}return false;
}

用 echo $环境变量名,可以打印环境变量,要先获取一下环境变量,获取环境变量用函数getenv

void Echo()
{//只输入了echo,不做处理直接返回 if(g_argc == 1) return;std::string opt = g_argv[1];if(opt == "$?") //打印最近一个程序的退出码{std::cout << last_exit_code << std::endl;last_exit_code = 0; //置0}else if(opt[0] == '$') //如 echo $PATH ,打印环境变量PATH{std::string env_name = opt.substr(1); //env_name里存的就是$后的内容const char* env_value =  getenv(env_name.c_str());if(env_value) std::cout << env_value << std::endl; //打印环境变量的值}else //直接打印字符串{std::cout << opt << std::endl;}return;
}

echo就实现好了,如果还想设计别的功能就直接else if添加就行。

还有一些别的内建命令,在函数CheckAndExeBuiltinCmd里直接添加就行了。

3.环境变量表

shell有两张表,一张命令行参数表,一张环境变量表,命令行参数表就是argv,现在要把环境变量表在整理出来。

在shell启动时,需要从系统的配置文件中获取环境变量,但是我们现在实现的shell做不到这一点,我们就直接从我们自己实现的shell的父进程里拿环境变量,拿到环境变量表之后还要维护这个表,还要导到自己的环境变量空间中

char *g_env[100]; //指向环境变量的指针数组
int g_env_num = 0; //环境变量的个数
void EnvInit()
{//1.先获取环境变量//2.把得到的环境变量导到自己的进程地址空间里}

先定义一个大小为100的指针数组,从父进程获取环境变量,要用到一个全局的外部变量environ,通过计算每一个环境变量的长度,动态的申请空间

char *g_env[100]; //指向环境变量的指针数组
int g_env_num = 0; //环境变量的个数
void EnvInit()
{//1.先获取环境变量extern char ** environ; //外部全局变量memset(g_env, 0, sizeof(g_env)); //清0,可有可无for(int i = 0; environ[i]; i++){g_env[i] = (char*)malloc(strlen(environ[i]) + 1); //申请空间if(g_env[i] == NULL) {perror("malloc fail!\n");return;}}//2.把得到的环境变量导到自己的进程地址空间里
}

然后边申请空间边拷贝父进程的环境变量到g_env里,环境变量表以NULL结尾,所以最后一个位置要为NULL。

char *g_env[100]; //指向环境变量的指针数组
int g_env_num = 0; //环境变量的个数
void EnvInit()
{//1.先获取环境变量extern char ** environ; //外部全局变量memset(g_env, 0, sizeof(g_env)); for(int i = 0; environ[i]; i++){g_env[i] = (char*)malloc(strlen(environ[i]) + 1); //申请空间if(g_env[i] == NULL) {perror("malloc fail!\n");return;}strcpy(g_env[i], environ[i]); //拷贝父进程的环境变量g_env_num++;}g_env[g_env_num] = NULL; //环境变量表以NULL结尾//2.把得到的环境变量导到自己的进程地址空间里
}

然后通过putenv把环境变量导到自己的进程地址空间里。

char *g_env[100]; //指向环境变量的指针数组
int g_env_num = 0; //环境变量的个数
void EnvInit()
{//1.先获取环境变量extern char ** environ; //外部全局变量memset(g_env, 0, sizeof(g_env)); for(int i = 0; environ[i]; i++){g_env[i] = (char*)malloc(strlen(environ[i]) + 1); //申请空间if(g_env[i] == NULL) {perror("malloc fail!\n");return;}strcpy(g_env[i], environ[i]); //拷贝父进程的环境变量g_env_num++;}g_env[g_env_num] = NULL; //环境变量表以NULL结尾//2.把得到的环境变量导到自己的进程地址空间里for(int i = 0; g_env[i]; i++){putenv(g_env[i]);}}

这样我们就得到了一张环境变量表。

int main()
{EnvInit(); //初始化环境变量while(1){//1.打印命令行提示符PrintCmdPrompt();//2.获取命令行参数char commandline[COMMAND_SIZE];if(!GetArguments(commandline, sizeof(commandline))) //获取失败continue;//3.解析命令行参数if(!CommandParse(commandline))continue;//PrintArg();//4.检测是否为内建命令,是内建命令就执行if(CheckAndExeBuiltinCmd())continue; //5.执行命令Execute();}return 0;
}

本次分享就到这里,我们下篇见~

http://www.dtcms.com/a/359802.html

相关文章:

  • 分享一个实用的B站工具箱(支持音视频下载等功能)
  • 【Canvas技法】绘制横向多色旗和竖向多色旗
  • 008.LangChain 输出解析器
  • 备份压缩存储优化方案:提升效率与节省空间的完整指南
  • 新手首次操作SEO核心要点
  • 线程池常见面试问答
  • 【Java实战⑩】Java 集合框架实战:Set与Map的奇妙之旅
  • 基于三维反投影矫正拼接视频
  • 数据结构(04)—— 栈和队列
  • 使用node-red+opencv+mqtt实现相机图像云端查看
  • 零基础入门AutoSar中的ARXML文件
  • Dify 从入门到精通(第 67/100 篇):Dify 的高可用性部署(进阶篇)
  • 从零开始写个deer-flow-mvp-第一天
  • 【C++】类和对象(一)
  • 【全功能图片处理工具详解】基于Streamlit的现代化图像处理解决方案
  • 二.Shell脚本编程
  • 微软开源TTS模型VibeVoice,可生成 90 分钟4人语音
  • 李宏毅NLP-13-Vocoder
  • 中级统计师-统计实务-第四章 专业统计
  • FPGA入门指南:从零开始的可编程逻辑世界探索
  • 【Proteus仿真】点亮小灯系列仿真——小灯闪烁/流水灯/交通灯
  • 常用的20个c++函数
  • 11.1.5 实现文件删除,共享和共享下载排行榜
  • 【GaussDB】排查应用高可用切换出现数据库整体卡顿及报错自治事务无法创建的问题
  • Photoshop - Ps 裁剪并拉直图片
  • 【C++详解】C++11(二) lambda表达式、类型分类、引⽤折叠、完美转发
  • 计算机视觉(四):二值化
  • 如何重置SVN被保存的用户名和密码
  • 基于路测点云标注生成OpenDrive地图的全流程解析
  • TI-92 Plus计算器:函数图像功能介绍