Linux编写一个极简版本的Shell

news2024/11/26 1:44:13

Linux编写一个极简版本的Shell

📟作者主页:慢热的陕西人

🌴专栏链接:Linux

📣欢迎各位大佬👍点赞🔥关注🚓收藏,🍉留言

本博客主要内容在Linux环境下,简易实现了一个Shell,顺便讲解和实现了一些内建命令

文章目录

  • Linux编写一个极简版本的Shell
    • ①读取命令行
    • ②父子进程框架
    • ③切割命令行
    • ④子进程借用分割的结果来替换程序
    • ⑤优化:
    • ⑥内建命令(重要)

首先我们观察到:

bash的命令行提示符:[用户名@主机名 当前目录]

[mi@lavm-5wklnbmaja demo1]

image-20231109205329152

所以我们无限循环去打印这个命令行提示符

#include<stdio.h>    
#include<unistd.h>                                                                                                                                            
int main()    
{    
  while(1)    
  {    
    printf("[xupt@my_machine currpath]#");    
    //这里因为我们不能加换行,所以得刷新缓冲区    
    fflush(stdout);    
    sleep(1);    
  }    
    
  return 0;    
} 

运行效果:

image-20231109210135984

①读取命令行

接下来我们就要获取命令输入的命令行参数:

我们创建一个字符数组用来专门存放用户输入的命令行

#define MAX 1024  //因为命令行最长支持到1024
char commondstr[MAX] = {0};

我们用fgets来获取命令行

fgets(commondstr, sizeof(commondstr), stdin);  

我们测试一下:

结果正常,但是我们的命令重新被打印的时候多打印了一个换行符,因为fgets读取了换行符,并且存储到了commondstr中了.

[mi@lavm-5wklnbmaja demo1]$ ./myshell 
[xupt@my_machine currpath]#ls -a
ls -a

解决方案:

commondstr[strlen(commondstr) - 1] = '\0';//处理fget获取了换行符的问题 

运行结果:

image-20231109212142534

②父子进程框架

这个时候我们就需要用到子进程了,因为执行命令行的时候需要用到程序替换,那么如果我们用父进程的话,直接就全崩掉了。

每次输入命令,都把命令交给子进程去执行,而父进程去等待子进程就好了:

    pid_t id = fork();    
    
    assert(id >= 0);    
    (void) id; //和上面的处理原因一样    
    
    if(id == 0)    
    {    
      //child    
    }    
                                                                                                                                                             
    int status = 0;             
    waitpid(id, &status, 0);  

在子进程执行之前,我们先要将用户输入进来的命令行进行拆分

③切割命令行

切割的原理很简单,我们只需要把命令行中间的空格变成\0即可。

ls -a -l ----> ls\0-a\0-l;

这个时候我们要引入一个C库提供的函数strtok,它是一个专门用来分隔字符串的函数。

我们需要封装一下这个函数来达到为我们分割命令行的目的:

注意strtok函数第二次切割的时候只需要传入NULL即可。

int split(char* commondstr, char* argv[])    
{    
  assert(commondstr);    
  assert(argv);    
    
  argv[0] = strtok(commondstr, SEP);    
    
  int i = 1;    
  while((argv[i++] = strtok(NULL, SEP)));    
//  {    
//      argv[i] = strtok(NULL, SEP);    
//      if(argv[i] == NULL) break;    
//      i++;    
//  }                                                     
  //表示切割成功    
  return 0;    
}  

main函数内部这样去调用分割函数

    int n = split(commondstr, argv);
    //等于0表示切割成功
    if(n != 0) continue;
    //DebugPrint(argv);    

我们再设计一个函数来打印我们切割的结果,查看我们切割的结果是否正确:

void DebugPrint(char* argv[])
{
  for(int i = 0; argv[i]; ++i)
  {
    printf("%d : %s\n", i, argv[i]);
  }
}

运行结果:

image-20231109224418103

④子进程借用分割的结果来替换程序

因为我们用split函数将命令行分装到argv字符串指针数组内部了,所以我们只能用带v的加载函数。

另外因为我们不能固定路径,所以我们也只能用带p的。

所以综上:我们的加载函数就选择到了execvp函数:

在子进程内部调用:

    if(id == 0)    
    {    
      //child    
      execvp(argv[0], argv);    
      exit(0);    
    }    

那么这时候我们在运行一下:

image-20231109225424556


⑤优化:

我们看到我们在用bash提供的ls的时候,它产生的结果是带有颜色的。

image-20231110135556660

但是我们自己实现的简易Shell是没有颜色的,那么这到底是为什么?

我们which ls查看一下,原来系统在ls后边面追加了一个参数--color==auto;

image-20231110135703672

那么我们也可以对我们的简易Shell进行一些优化让他支持这样的显示:

我们只需要在代码中特判一下即可:

    if(strcmp(argv[0], "ls") == 0)    
    {    
      //先找到末尾    
      int pos  = 0;    
      while(argv[pos]) pos++;    
      //追加color参数    
      argv[pos] = (char*)"--color=auto";    
    
      //安全处理    
      pos++;    
      argv[pos] = NULL;                                                                                                                
    }

运行效果:

image-20231110140325861


⑥内建命令(重要)

(1)内建命令的概念:

—>首先我们先明确一下内建命令/内置命令的概念,就是让我们bash自己执行的命令,我们称之为内建命令/内置命令。

(2)cd命令

当我们在我们的简易Shell中切换目录时:

我们发现不论我们怎么切换目录,结果都是目录没有变化,**原因是我们是在子进程中运行这些命令行的,**进程具有独立性。其实我们切换目录是切换了子进程的目录,但是父进程也就是我们pwd显示的目录却没有任何变化,并且这里其实pwd的也是子进程的当前目录,但是因为子进程在执行完cd命令后,就被exit了。当我们再执行pwd的时候是一个新的子进程在帮我们完成这个命令,因为我们之前cd没有改变父进程的当前目录,那么新创建的子进程的目录也就变成了和父进程一样的,所以看起来我们就是没有改变当前目录一样。

image-20231110140734849

所以这里的cd命令,我们要在父进程中交给一个函数chdir()来让我们的bash来执行:

代码:

    //当我们输入cd命令的时候    
    if(strcmp(argv[0], "cd") == 0)    
    {    
      if(argv[1] != NULL) chdir(argv[1]);                                                                                              
      continue;    
    }  

运行结果:

image-20231110142305250

(3)export命令

此外不止我们的cd,包括我们当时去在bash中执行我们的export添加环境变量的时候,实际上是添加到我们的bash内部的,那么如果我们的简易Shell去把这个命令交给我们的子进程去执行了,那么就不太合适了,应该让我们的父进程自行去执行这个命令!

所以我们依旧采用内建命令的方式:

    //当我们输入export命令时    
    if(strcmp(argv[0], "export") == 0)    
    {    
      //我们把这个环境变量存储在我们自己设定的数组内部    
      if(argv[1] != NULL)    
      {    
        strcpy(myenv[env_index], argv[1]);    
        //再将数组内部的环境变量放到父进程的环境变量中    
        putenv(myenv[env_index++]);    
      }    
    }  

我们尝试测试一下:

image-20231110151609156

最终我们找到了

image-20231110151622993

但是我们的env打印的好像是子进程的环境变量,这似乎不是我们想要的,我们应该想要的是父进程的环境变量,所以我们再做一下处理:

我们自行实现一个函数去打印我们的环境变量:

    void PrintEnv()
	{
  		extern char **environ;
  		for(int i = 0; environ[i]; ++i)
  		{
    		printf("%d:%s\n",i, environ[i]);
  		}
	}
	//当我们查看环境变量的时候
    if(strcmp(argv[0], "env") == 0)
    {
      PrintEnv();
      continue; 
    }                                                                                     

运行效果:

image-20231110152530084

image-20231110152557349

所以其实我们之前学习的几乎所有的环境变量,相关的命令都是内建命令

我们在将echo支持成内建命令:

    //当我们echo的时候
    if(strcmp(argv[0], "echo") == 0)
    {
      //先确认一下echo后面第一个跟的是$
      if(argv[1][0] == '$')
      {
        char* env_ret = getenv(argv[1] + 1);                                                                                           
        if(env_ret != NULL)
        {
          printf("%s=%s\n", argv[1] + 1, env_ret);
        }
      }
      
      continue;
    }

运行结果:

image-20231110155212975

既然支持了环境变量的查询,我们再来顺便支持一下进程退出码的支持,也就是我们的echo $?

    //当我们echo的时候    
    if(strcmp(argv[0], "echo") == 0)    
    {    
      //先确认一下echo后面第一个跟的是$                                                                                                
      if(argv[1][0] == '$')    
      {    
        if(argv[1][1] == '?')    
        {    
          printf("%d\n", last_exit);      
          continue;    
        }    
        else    
        {    
          char* env_ret = getenv(argv[1] + 1);    
          if(env_ret != NULL)  printf("%s=%s \n", argv[1] + 1, env_ret);    
        }    
      }  
        
        
    int status = 0;
    pid_t ret  = waitpid(id, &status, 0);
    if(ret > 0)
    {                                                                                                                                  
      last_exit = WEXITSTATUS(status);//last_exit我们放在main函数里但不要放在循环里,他要长期保留。
    } 

测试结果:

image-20231110160243796

⑦代码汇总:

#include<stdio.h>
#include<unistd.h>
#include<assert.h>
#include<string.h>
#include<stdlib.h>
#include<sys/types.h>
#include<sys/wait.h>

//因为命令行最长支持到1024
#define MAX 1024
//限制最多切割为64段
#define ARGC 64

#define SEP " "


int split(char* commondstr, char* argv[])                                                           
{
  assert(commondstr);
  assert(argv);

  argv[0] = strtok(commondstr, SEP);
    
  int i = 1;
  while((argv[i++] = strtok(NULL, SEP)));
//  {
//      argv[i] = strtok(NULL, SEP);                                                                                                   
//      if(argv[i] == NULL) break;
//      i++;
//  }

  //表示切割成功
  return 0;
}

void PrintEnv()
{
  extern char **environ;
  for(int i = 0; environ[i]; ++i)
  {
    printf("%d:%s\n",i, environ[i]);
  }
}

void DebugPrint(char* argv[])
{
  for(int i = 0; argv[i]; ++i)
  {                                                                                    
    printf("%d : %s\n", i, argv[i]);
  }
}

int main()
{
  int last_exit = 0; //存储上一个进程的退出码
  int env_index = 0; //环境变量数组的下标
  char myenv[32][64];
  while(1)
  {
    //每次进来都初始化一下
    char commondstr[MAX] = {0};
    char* argv[ARGC] = {NULL};
    printf("[xupt@my_machine currpath]#");
    fflush(stdout);
    //这里因为我们不能加换行,所以得刷新缓冲区
    char* s = fgets(commondstr, sizeof(commondstr), stdin);                                                                            
    
    assert(s);
    (void)s;//保证在release发布的时候,因为assert去掉,而导致s没有被使用过而产生的告警,什么都没做,充当一次使用
    
    commondstr[strlen(commondstr) - 1] = '\0'; //解决了fgets读入换行符的问题
      
    int n = split(commondstr, argv);
    //等于0表示切割成功
    if(n != 0) continue;
    //DebugPrint(argv);
    
    //当我们输入export命令时
    if(strcmp(argv[0], "export") == 0)
    {
      //我们把这个环境变量存储在我们自己设定的数组内部
      if(argv[1] != NULL)
        strcpy(myenv[env_index], argv[1]);                                                                                             
        //再将数组内部的环境变量放到父进程的环境变量中
        putenv(myenv[env_index++]);
      }
    }

    //当我们查看环境变量的时候
    if(strcmp(argv[0], "env") == 0)
    {
      PrintEnv();
      continue; 
    }
    
    //当我们echo的时候
    if(strcmp(argv[0], "echo") == 0)
    {
      //先确认一下echo后面第一个跟的是$
      if(argv[1][0] == '$')
      {                                                                                                                                
        if(argv[1][1] == '?')
        {
          printf("%d\n", last_exit);  
          continue;
        }
        else
        {
          char* env_ret = getenv(argv[1] + 1);
          if(env_ret != NULL)  printf("%s=%s \n", argv[1] + 1, env_ret);
        }
      }
      
      continue;
    }

    //当我们输入cd命令的时候
    if(strcmp(argv[0], "cd") == 0)
    {
      if(argv[1] != NULL) chdir(argv[1]);
      continue;
    }                                                                                                                                  
    
    //当我们输入ls命令的时候
    if(strcmp(argv[0], "ls") == 0)
    {
      //先找到末尾
      int pos  = 0;
      while(argv[pos]) pos++;
      //追加color参数
      argv[pos] = (char*)"--color=auto";

      //安全处理
      pos++;
      argv[pos] = NULL;
    }

    pid_t id = fork();
    assert(id >= 0);
    (void) id; //和上面的处理原因一样

    if(id == 0)                                                                                                                        
    {
      //child
      execvp(argv[0], argv);
      exit(0);
    }

    int status = 0;
    pid_t ret  = waitpid(id, &status, 0);
    if(ret > 0)
    {
      last_exit = WEXITSTATUS(status);
    } 

   // printf("%s\n", commondstr);
  }                         
  return 0;
}        

到这本篇博客的内容就到此结束了。
如果觉得本篇博客内容对你有所帮助的话,可以点赞,收藏,顺便关注一下!
如果文章内容有错误,欢迎在评论区指正

在这里插入图片描述

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1193350.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

二十五、W5100S/W5500+RP2040树莓派Pico<Modebus TCP Server示例>

文章目录 1 前言2 简介2 .1 什么是Modbus TCP&#xff1f;2.2 Modbus TCP指令介绍2.3 请求数据过程2.4 Modbus TCP协议优点2.5 Modbus TCP应用场景 3 WIZnet以太网芯片4 Modbus TCP示例概述以及使用4.1 流程图4.2 准备工作核心4.3 连接方式4.4 主要代码概述4.5 结果演示 5 注意…

nacos做服务配置和服务器发现

一、创建项目 1、创建一个spring-boot的项目 2、创建三个模块file、system、gateway模块 3、file和system分别配置启动信息,并且创建一个简单的控制器 server.port9000 spring.application.namefile server.servlet.context-path/file4、在根目录下引入依赖 <properties&g…

Maven-依赖管理机制

一、背景和起源 依赖管理是Maven的一个核心功能。管理单个模块项目的依赖相对比较容易&#xff0c;但是如果是多模块项目或者有几百个模块的项目就是一个巨大的挑战。 如果手动构建项目&#xff0c;那么就先需要梳理各个模块pom中定义的依赖和版本&#xff0c;然后进行下载到本…

C++模拟实现set和map

1.看源码&#xff0c;简单了解原码的set和map类的结构 1.看类的私有成员和类模板的参数&#xff1a; 看下面我画的一些框&#xff0c;再结合上面的看一下&#xff0c;会有什么疑惑呢&#xff1f; 一般我们知道编译器底层的代码会很简洁&#xff0c;不会多创建无意义的内容&am…

【使用教程】在Ubuntu下PMM60系列一体化伺服电机通过PDO跑循环同步位置模式详解

本教程将指导您在Ubuntu操作系统下使用PDO来配置和控制PMM60系列一体化伺服电机以实现循环同步位置模式。我们将介绍必要的步骤和命令&#xff0c;以确保您能够成功地配置和控制PMM系列一体化伺服电机。 一、准备工作 在正式介绍之前还需要一些准备工作&#xff1a;1.装有lin…

环保壁炉:酒精壁炉的生态优势

环保已经成为一个备受重视的话题。我们都希望采用更环保的能源&#xff0c;以减少对地球的影响。而酒精壁炉作为一种新型的取暖方式&#xff0c;正受到越来越多人的喜爱&#xff0c;因为它们代表了一种清洁能源的选择。 酒精壁炉的独特之处在于它们使用酒精作为燃料。这种酒精…

NFT数字藏品(交易平台)系统开发

随着数字技术和区块链技术的发展&#xff0c;NFT数字藏品交易平台系统开发逐渐成为了一个热门话题。NFT&#xff0c;即非同质化代币&#xff0c;可以用来代表独一无二的数字资产&#xff0c;如图片、音频、视频等&#xff0c;在数字世界中具有极高的价值。本文将介绍NFT数字藏品…

11.10论文写作与格式

格式 文章题目&#xff1a;&#xff08;三号、黑体、加粗&#xff0c;居中&#xff09; 摘要&#xff1a;这两个大字要&#xff08;黑体、小四、加粗&#xff0c;左对齐&#xff09;&#xff1b;内容为(宋体、小四) 关键词&#xff1a;三个字为(黑体、小四、加粗&#xff0c…

动作捕捉系统通过SDK与LabVIEW通信

运动分析、VR、机器人等应用中常使用LabVIEW对动作捕捉数据进行实时解算。NOKOV度量动作捕捉系统支持通过SDK与LabVIEW进行通信&#xff0c;将动作数据传入LabVIEW。 一、软件设置 1、形影软件设置 1、将模式切换到后处理模式 2、加载一个刚体数据 3、打开软件设置 4、选择网…

NFT Insider112:Gucci Cosmos LAND亮相 The Sandbox,和YGG一起探索Web3增长新方式

引言&#xff1a;NFT Insider由NFT收藏组织WHALE Members(https://twitter.com/WHALEMembers)、BeepCrypto&#xff08;https://twitter.com/beep_crypto&#xff09;联合出品&#xff0c;浓缩每周NFT新闻&#xff0c;为大家带来关于NFT最全面、最新鲜、最有价值的讯息。每期周…

实现智慧工地的高效建筑管理,数据分析起着关键作用!

智慧工地是利用物联网、云计算、大数据等技术&#xff0c;实现对建筑工地实时监测、管理和控制的一种新型建筑管理方式。 智慧工地架构&#xff1a; 1、终端层&#xff1a;充分利用物联网技术、移动应用、智能硬件设备提高现场管控能力。通过RFID、传感器、摄像头、手机等终端…

KiB、MiB与KB、MB的区别

KiB、MiB与KB、MB的区别

vue3 + antd 图片上传 (精简篇)cv即可

使用antd组件库里的 a-upload 上传图片 template代码&#xff1a; <a-upload name"idCardzm" list-type"picture-card" class"avatar-uploader" :show-upload-list"false":before-upload"beforeUpload" :customRequest…

喜报!华为云金融PaaS3.0荣获“2023年应用现代化典型案例”称号

中国软件行业协会近期启动了2023“应用现代化产业实践”优秀案例征集活动&#xff0c;旨在加快推动应用现代化发展与推广应用&#xff0c;形成行业应用带动和示范作用&#xff0c;打造应用现代化软件名企、名品&#xff0c;凝聚行业资源&#xff0c;助力我国行业应用现代化高质…

mac使用VMware Fusion安装Centos 7系统

mac主机芯片&#xff1a;Apple M2 Pro VMware-Fusion&#xff1a;13.5 centos&#xff1a;7 第一次操作&#xff1a; 按步骤选择操作系统 在选择虚拟启动虚拟机没有安装centos的界面 而是下图 改动&#xff1a;把UEFI换成BIOS ——>无果 第二次操作&#xff1a; 直接…

国际阿里云:无法ping通ECS实例公网IP的排查方法!!!

无法ping通ECS实例的原因较多&#xff0c;您可以参考本文进行排查。 问题现象 本地客户端无法ping通目标ECS实例公网IP&#xff0c;例如&#xff1a; 本地客户端为Linux系统&#xff0c;ping目标ECS实例公网IP时无响应&#xff0c;如下所示&#xff1a; 本地客户端为Windo…

实时疫情地图及全国监测动态大屏可视化【可视化项目案例-02】

🎉🎊🎉 你的技术旅程将在这里启航! 🚀🚀 本文选自专栏:可视化技术专栏100例 可视化技术专栏100例,包括但不限于大屏可视化、图表可视化等等。订阅专栏用户在文章底部可下载对应案例源码以供大家深入的学习研究。 🎓 每一个案例都会提供完整代码和详细的讲解,不…

【poi导出excel模板——通过建造者模式+策略模式+函数式接口实现】

poi导出excel模板——通过建造者模式策略模式函数式接口实现 poi导出excel示例优化思路代码实现补充建造者模式策略模式 poi导出excel示例 首先我们现看一下poi如何导出excel&#xff0c;这里举个例子&#xff1a;目前想要导出一个Map<sex,List>信息&#xff0c;sex作为…

竞赛选题 深度学习疲劳驾驶检测 opencv python

文章目录 0 前言1 课题背景2 实现目标3 当前市面上疲劳驾驶检测的方法4 相关数据集5 基于头部姿态的驾驶疲劳检测5.1 如何确定疲劳状态5.2 算法步骤5.3 打瞌睡判断 6 基于CNN与SVM的疲劳检测方法6.1 网络结构6.2 疲劳图像分类训练6.3 训练结果 7 最后 0 前言 &#x1f525; 优…

RFID智慧物流设计解决方案

物流行业需求 物流是将物质资料从供应者运送到需求者的物理运动过程&#xff0c;涉及运输、保管、包装、装卸、流通加工、配送以及信息等多个基本活动的统一整合&#xff0c;在经济全球化和电子商务的推动下&#xff0c;快递物流和医药物流成为现代物流的两大重要产业。随着智…