【Linux篇】自主Shell命令行解释器

06-01 1368阅读

📌 个人主页: 孙同学_

🔧 文章专栏:Liunx

💡 关注我,分享经验,助你少走弯路!

【Linux篇】自主Shell命令行解释器

文章目录

    • 1. 获取用户名的接口
    • 2. 等待用户输入接口
    • 3. 将上述代码进行面向对象式的封装
    • 4. 命令行解析
    • 5. 执行命令
    • 6. 路径切割
    • 7. 解决cd命令路径不变
    • 8. 解决cd后环境变量未发生变化
    • 9. echo命令
    • 10. 获取环境变量
    • 11. 总结
    • 12.代码实现

      1. 获取用户名的接口

      通过环境变量来获取

      我们需要用到的接口getenv

      【Linux篇】自主Shell命令行解释器

      【Linux篇】自主Shell命令行解释器

      【Linux篇】自主Shell命令行解释器

      //获取用户名
      const char* GetUserName()
      {
      	const char* name = getenv("USER");
      	return name == NULL ? "None" : name;
      }
       //获取主机名
      const char* GetHostName()
      {
      	const char* hostname = getenv("HOSTNAME");
      	return hostname == NULL ? "None" : hostname;
      }
       //获取当前路径
      const char* GetPwd()
      {
      	const char* pwd = getenv("PWD");
      	return pwd == NULL ? "None" : pwd;
      }
      

      2. 等待用户输入接口

      当我们没有输入时,我们会发现命令行会卡在这里等待我们输入

      【Linux篇】自主Shell命令行解释器

      我们也让我们自己的命令行能等待输入

      【Linux篇】自主Shell命令行解释器

      我们可以采用fgets以文件形式读取一行,也可以使用gets读取一行字符串

      我们接下来进行C/C++混编的方式,因为我们后面会用到系统调用,而这些系统调用都是用C写的,如果我们纯用C++来实现的话可能会要适配某些接口。

      我们下来用fgets实现

      【Linux篇】自主Shell命令行解释器

      【Linux篇】自主Shell命令行解释器

      【Linux篇】自主Shell命令行解释器

      效果展示:

      【Linux篇】自主Shell命令行解释器

      我们会发现最后多了一个空行,这里为什么会多一个空行呢?因为我们在输入完字符串后还按了一次回车,我们不想让它有这一行空行该怎么办?我们在输入字符串后后面还会有个\n,比如我们输入的是"ls -a -l"最后再按一次回车就变成了"ls -a -l \n",我们只需要输入完之后把最后的\n置为0就好了

      【Linux篇】自主Shell命令行解释器

      效果展示:

      【Linux篇】自主Shell命令行解释器

      🔖小tips: 这里会不会求出的字符串长度为0,然后再-1发生越界呢?答案是不会的,因为我们最后至少还要敲一次回车键,所以这个字符串的最小长度为1。

      3. 将上述代码进行面向对象式的封装

      我们先认识一个新的接口snprintf

      【Linux篇】自主Shell命令行解释器

      【Linux篇】自主Shell命令行解释器

      //制作命令行提示符
      void MakeCommandline(char com_prompt[], int size)
      {
      	snprintf(com_prompt, size, FORMAT, GetUserName(), GetHostName(), GetPwd());//我们想让"[%s@%s %s]# "以后能随便调整,所以我们define一下
      }
      //打印命令行提示符
      void PrintCommandline()
      {
      	char prompt[COMMAND_SIZE];
      	MakeCommandline(prompt, sizeof(prompt));//先制作
      	printf("%s", prompt);//再打印
      	fflush(stdout);
      }
       //获取用户输入
      bool GetCommandline(char* out, int size)
      {
      	//"ls -a -l "=>"ls -a -l \n"
      	const char* c = fgets(out, size, stdin);//从标准输入里获取,放到out当中
      	if (c == NULL) return 1;
      	out[strlen(out) - 1] = 0;//清理\n 
      	if (strlen(out) == 0) return false; //对于我们用户来说,有可能获取到的字符串的长度为0,为0直接return false 
      	return true; //否则return true
      }
      int main()
      {
      	//printf("[%s@%s %s]# ",GetUserName(),GetHostName(),GetPwd());
          //1. 打印命令行提示符
      	PrintCommandline();
      	//2.获取用户输入
      	char commandline[COMMAND_SIZE];//定义一个数组
      	if (GetCommandline(commandline, sizeof(commandline)))//如果获取成功
      	{
      		printf("echo %s\n", commandline);//回显一下我们输入的内容,用作测试
      	}
      	return 0;
      }
      

      我们在shell中可以一直输入,我们的程序输入一次就结束了,所以shell永远不退出。我们应当不断地获取用户输入。

      【Linux篇】自主Shell命令行解释器

      4. 命令行解析

      我们在传字符串的时候不能“ls -a -l”整体传入,我们要将传入的字符串进行变形,将这一个字符串拆成“ls” "-a" "-l"。而且我们的命令行也不能在shell中直接替换,而要创建子进程。我们将字符串切成这样那么如何快速的找到每一个元素呢?命令行参数表

      将打散的字符串以NULL结尾放到g_argv[]里面。在这里又来认识一个新的接口strtok

      【Linux篇】自主Shell命令行解释器

      这个接口第一次切的时候第一个参数传的是要分割的字符串的起始地址,如果要接着切的话第一个参数就必须传NULL,当切除完毕返回值就为空表示没有字符串了。

      分隔符是const char*所以我们不能传单引号,而要传双引号

      【Linux篇】自主Shell命令行解释器

      //命令行分析
      bool CommandParse(char* commandline)
      {
      #define SEP " "
      	g_argc = 0; //每次进来初始化为0
      	//"ls -a -l" => "ls" "-a" "-l"
      	g_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也被统计到了里面
      	return true;
      }
       //测试形成的表结构
      void PrintArgv()
      {
      	for (int i = 0; i %s\n", i, g_argv[i]);
      	}
      	printf("argc:%d", g_argc);
      }
      

      5. 执行命令

      由于我们当前的进程还有自己的任务,所以我们将执行命令交给子进程来完成,那么就需要程序替换,execvp

      【Linux篇】自主Shell命令行解释器

      //执行命令
      int Execute()
      {
      	pid_t id = fork();
      	if (id == 0)
      	{
      		//子进程
      		execvp(g_argv[0], g_argv);
      		exit(1);
      	}
      	//父进程
      	pid_t rid = waitpid(id, nullptr, 0);
      	return 0;
      }
      

      6. 路径切割

      系统的路径名只有一个,我们自己写的会跟一长串

      【Linux篇】自主Shell命令行解释器

      所以我们对路径进行切割。 C++中有个命令rfind从后向前找,substr截字符串

      【Linux篇】自主Shell命令行解释器

      //路径切割
      std::string DirName(const char* pwd)
      {
      #define SLASH "/"
      	std::string dir = pwd;
      	if (dir == SLASH) return SLASH;
      	auto pos = dir.rfind(SLASH);
      	if (pos == std::string::npos) return "BUG";//这里表示没有找到/
      	return dir.substr(pos + 1);
      }
      

      7. 解决cd命令路径不变

      到目前我们会发现我们执行cd命令时路径不发生改变,因为目前所有的命令都是子进程执行的,子进程改变路径时改的是自己的pwd,父进程bash的环境变量并没有改变,我们真正要改的是父进程的路径,因为把父进程的路径改了往后再创建子进程所有的子进程就会在新的路径下执行,因为所有的子进程的PCB都是拷贝父进程的PCB,因此cd这样的命令不能让子进程去执行,而要让父进程亲自执行,这种命令叫做内建命令

      如何让bash亲自去执行呢?我们先来认识一个新的接口chdir

      【Linux篇】自主Shell命令行解释器

      【Linux篇】自主Shell命令行解释器

      【Linux篇】自主Shell命令行解释器

       //处理cd命令
      bool cd()
      {
      	if (g_argc == 1) //表明只是cd,没有带任何参数
      	{
      		std::string home = GetHome();//将home的路径拿过来
      		if (home.empty())  return true;//如果是空就相当于环境变量获取失败了
      		chdir(home.c_str());//走到这里不为空,就把当前路径切换成家路径了
      	}
      	else
      	{
      		std::string where = g_argv[1];
      		//"cd -" / "cd ~"
      		if (where == "-")
      		{
      		}
      		else if (where == "~")
      		{
      		}
      		else
      		{
      			chdir(where.c_str());
      		}
      	}
      }
       //检测并处理内建命令
      bool CheckAndExecBuiltin()
      {
      	std::string cmd = g_argv[0];
      	if (cmd == "cd")//如果是内建命令
      	{
      		cd();
      		return true;//是内建命令
      	}
      	return false;//否则不是内建命令
      }
      

      8. 解决cd后环境变量未发生变化

      我们再切换路径后会发现路径变了,但是环境变量中的路径并没有变。原因是路径发生变化后环境变量没有进行刷新,所以我们要将新的路径更新到环境变量中。这里我们来认识一个系统调用getcwd,获取当前进程的工作路径。

      【Linux篇】自主Shell命令行解释器

      【Linux篇】自主Shell命令行解释器

       //获取当前路径
      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;
      }
      

      9. echo命令

      echo命令也是内建命令,我们可以用它echo "hello"在屏幕上打印,echo $?查看上个进程的退出码,echo $PATH查看环境变量。

      【Linux篇】自主Shell命令行解释器

      【Linux篇】自主Shell命令行解释器

      //处理echo命令
      void Echo()
      {
      	if (g_argc == 2)//意思是echo后面必须得跟东西
      	{
      		//echo "heool world"
      		   //echo $?
      			//echo $PATH
      		std::string opt = g_argv[1];
      		if (opt == "$?")//输出上一个程序退出的退出码
      		{
      			std::cout 
      			std::string env_name = opt.substr(1);//去掉$后的内容就是环境变量的名字
      			const char* env_value = getenv(env_name.c_str());
      			if (env_value)
      				std::cout 
      			std::cout 
      	extern char** environ;//声明一个环境变量所对应的信息
      	memset(g_env, 0, sizeof(g_env));//将表中的信息全部置为0
      	g_envs = 0;
      	//本来要从配合文件中来
      	//今天从父shell中来
      	//1. 获取环境变量
      	for (int i = 0; environ[i]; i++)
      	{
      		g_env[i] = (char*)malloc(strlen(environ[i]) + 1);
      		//1.2拷贝
      		strcpy(g_env[i], environ[i]);//把父进程环境变量里的值拷贝给g_env
      		g_envs++;
      	}
      	g_env[g_envs] = NULL;
      	//2.导入环境变量
      	for (int i = 0; g_env[i]; i++)
      	{
      		putenv(g_env[i]);
      	}
      	environ = g_env;
      	//3.clean清理
      }
      
          const char* name = getenv("USER");
          return name == NULL ? "None" : name;
      }
      //获取主机名
      const char* GetHostName()
      {
          const char* hostname = getenv("HOSTNAME");
          return hostname == NULL ? "None" : hostname;
      }
      //获取当前路径
      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;
      }
      //获取家目录
      const char* GetHome()
      {
          const char* home = getenv("HOME");
          return home == NULL ? "" : home;
      }
      //初始化环境变量表
      void InitEnv()
      {
          extern char** environ;//声明一个环境变量所对应的信息
          memset(g_env, 0, sizeof(g_env));//将表中的信息全部置为0
          g_envs = 0;
          //本来要从配合文件中来
          //今天从父shell中来
          //1. 获取环境变量
          for(int i = 0;environ[i];i++)
          {
              //1.1申请空间
              g_env[i] = (char*)malloc(strlen(environ[i])+1);
              //1.2拷贝
              strcpy(g_env[i],environ[i]);//把父进程环境变量里的值拷贝给g_env
              g_envs++;
          }
          g_env[g_envs] = NULL;
          //2.导入环境变量
          for(int i = 0;g_env[i];i++)
          {
              putenv(g_env[i]);
          }
          environ = g_env;
          //3.clean清理
      }
      //处理cd命令
      bool cd()
      {
          if(g_argc == 1) //表明只是cd,没有带任何参数
          {
              std::string home = GetHome();//将home的路径拿过来
              if(home.empty())  return true;//如果是空就相当于环境变量获取失败了
              chdir(home.c_str());//走到这里不为空,就把当前路径切换成家路径了
          }
          else
          {
              std::string where = g_argv[1];
              //"cd -" / "cd ~"
              if(where == "-")
              {
              
              }
              else if(where == "~")
              {
              
              }
              else
              {
                  chdir(where.c_str());
              }
          }
          return true;
      }
      //处理echo命令
      void Echo()
      {
          if(g_argc == 2)//意思是echo后面必须得跟东西
          {
              //echo "heool world"
              //echo $?
              //echo $PATH
              std::string opt = g_argv[1];
              if(opt == "$?")//输出上一个程序退出的退出码
              {
                  std::cout 
                  std::string env_name = opt.substr(1);//去掉$后的内容就是环境变量的名字
                  const char* env_value = getenv(env_name.c_str());
                  if(env_value)
                      std::cout 
                  std::cout 
      #define SLASH "/"
          std::string dir = pwd;
          if(dir == SLASH) return SLASH;
          auto pos = dir.rfind(SLASH);
          if(pos == std::string::npos) return "BUG";//这里表示没有找到/
          return dir.substr(pos+1);
      }
      //制作命令行提示符
      void MakeCommandline(char com_prompt[],int size)
      {
          //snprintf(com_prompt, size, FORMAT,GetUserName(),GetHostName(),GetPwd());//我们想让"[%s@%s %s]# "以后能随便调整,所以我们define一下
          snprintf(com_prompt, size, FORMAT,GetUserName(),GetHostName(),DirName(GetPwd()).c_str());//我们想让"[%s@%s %s]# "以后能随便调整,所以我们define一下
      }
      //打印命令行提示符
      void PrintCommandline()
      {
          char prompt[COMMAND_SIZE];
          MakeCommandline(prompt,sizeof(prompt));//先制作
          printf("%s",prompt);//再打印
          fflush(stdout);
      }
      //获取用户输入
      bool GetCommandline(char* out,int size)
      {
          //"ls -a -l "="ls -a -l \n"
          const char *c = fgets(out,size,stdin);//从标准输入里获取,放到out当中
          if(c == NULL) return 1;
          out[strlen(out)-1] = 0;//清理\n 
          if(strlen(out) == 0) return false; //对于我们用户来说,有可能获取到的字符串的长度为0,为0直接return false 
          return true; //否则return true
      }
      //命令行分析
       bool CommandParse(char* commandline)
      {
      #define SEP " "
          g_argc = 0; //每次进来初始化为0
          //"ls -a -l" = "ls" "-a" "-l"
          g_argv[g_argc++] = strtok(commandline,SEP);
          
         while((bool)(g_argv[g_argc++] = strtok(nullptr,SEP)));//再次想切的话传commandline就不对了,再要切历史字符串就得把它设为nullptr,再次切的话分隔符依旧是SEP,如果切成了返回的就是下一个字符串的起始地址
         //为什么可以这样切呢?因为再次切字串时,它一直切一直切最后就会会变成NULL,切成NULL首先会把g_argv数组置为NULL,符合命令行参数表的设定,NULL也会作为while的条件判断,最后就直接结束了
         //并且g_argc也会统计出命令行参数有多少个
         g_argc--;//因为NULL也被统计到了里面
         return g_argc  0 ? true : false;
      }
      //测试形成的表结构
      void PrintArgv()
      {
          for(int i = 0;i 
免责声明:我们致力于保护作者版权,注重分享,被刊用文章因无法核实真实出处,未能及时与作者取得联系,或有版权异议的,请联系管理员,我们会立即处理! 部分文章是来自自研大数据AI进行生成,内容摘自(百度百科,百度知道,头条百科,中国民法典,刑法,牛津词典,新华词典,汉语词典,国家院校,科普平台)等数据,内容仅供学习参考,不准确地方联系删除处理! 图片声明:本站部分配图来自人工智能系统AI生成,觅知网授权图片,PxHere摄影无版权图库和百度,360,搜狗等多加搜索引擎自动关键词搜索配图,如有侵权的图片,请第一时间联系我们。

相关阅读

目录[+]

取消
微信二维码
微信二维码
支付宝二维码