• [Linux]编写一个极简版的shell(版本1)


    [Linux]编写一个极简版的shell-version1

    本文能够帮助Linux系统学习者通过代码的角度更好地理解命令行解释器的实现原理。

    命令行提示符打印

    Linux操作系统运行时,就需要shell进程进行命令行解释,然后让系统完成对应的命令,因此打印命令行提示符时要采用死循环打印的方式,具体的代码逻辑如下:

    int main()
    {
      while(1)
      {
        printf("[%s@%s %s]$$ ", getenv("USER"), getenv("HOSTNAME"), getpath(getenv("PWD")));
        fflush(stdout);
        //sleep(200); -- 当前阶段用于演示效果
      }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 为了和系统的shell进行区分因此打印两个$符号
    • 命令行提示符打印的信息都是通过环境变量得来的,因此只需要调用系统接口获取相应的环境变量即可
      • USER环境变量 – 当前使用的用户名
      • HOSTNAME环境变量 – 当前的主机名称
      • PWD环境变量 – 当前用户所处绝对路径
    • 由于显示器采用的是行缓冲的策略,因此需要手动刷新缓冲区才能让命令行提示符显式到屏幕上

    由于环境变量PWD是当前用户所处的绝对路径,因此需要编写一个函数getpath来获取当前所处的目录名称,具体的代码逻辑如下:

    const char* getpath(char* path)
    {
      int length = strlen(path);
      if(length == 1) return "/"; //根目录
      int i = length - 1;
      while((path[i] != '/')) i--;
      return path+i+1;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 如果绝对路径的字符串长度为1,表示所处为根目录,返回"/"
    • 其余路径下都要将绝对路径里当前目录前的所有路径分割掉,包括路径分隔符,譬如当前记录绝对路径环境变量为PWD=/home/qxm/linux-warehouse/review/mybash/version1getpath的返回值为verson1字符串的首地址

    效果演示:

    image-20230904145622641

    接收命令行参数

    接受命令行参数只需要设置一个字符串数组,将用户的输入接收即可,具体的代码逻辑如下:

    #define MAX 1024
    
    int main()
    {
      while(1)
      {
    	char commandstr[MAX] = { 0 }; //接收命令行参数
        printf("[%s@%s %s]$$ ", getenv("USER"), getenv("HOSTNAME"), getpath(getenv("PWD")));
        fflush(stdout);
        char* s = fgets(commandstr, sizeof(commandstr), stdin);
        commandstr[strlen(commandstr) - 1] = '\0'; //去除输入时末尾的'\n'
        //printf("%s\n", commandstr); -- 当前阶段用于演示效果
      }
      return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 由于用户输入时会按下回车也就是会在输入时在末尾出现一个'\n',命令行参数使用时需要将其去除掉,譬如用户输入ls -a -l\n,命令行参数只需要ls -a -l

    效果演示:

    image-20230904151119734

    将命令行参数进行解释

    获取用户输入的命令行参数后,需要将命令行参数根据输入的空格将字符串分割为多个字串,譬如用户输入ls -a -l,要分割为ls, -a, -l,具体的代码逻辑如下:

    #define MAX 1024
    #define ARGC 64
    #define SEP " "
    
    int split(char commandstr[], char* argv[])//命令行参数解释
    {
      argv[0] = strtok(commandstr, SEP);
      if (argv[0] == NULL) return -1; //字符串为空
      int i = 1;
        while(1)
        {
            argv[i] = strtok(NULL, SEP);
            if(argv[i] == NULL) break;
            i++;
        }
      return 0;
    }
    
    void debugPrint(char* argv[])//--当前阶段用于演示效果
    {
      int i = 0;
      for (i = 0; argv[i] != NULL; i++)
      {
        printf("%s\n", argv[i]);
      }
    }
    
    int main()
    {
      while(1)
      {
        char commandstr[MAX] = { 0 }; //接收命令行参数
        char* argv[ARGC] = { NULL }; //存储命令行参数
        printf("[%s@%s %s]$$ ", getenv("USER"), getenv("HOSTNAME"), getpath(getenv("PWD")));
        fflush(stdout);
        char* s = fgets(commandstr, sizeof(commandstr), stdin);
        commandstr[strlen(commandstr) - 1] = '\0'; //去除输入时末尾的'\n'
        
        int n = split(commandstr, argv);
        if(n != 0) continue; //用户输入空串
        //debugPrint(argv);  -- 当前阶段用于演示效果
      }
      return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 调用C语言库函数strtok获取分割空格字符后字串的首地址,并且将空格置为'\0',并将字串的首地址存储起来

    效果演示:

    image-20230904154551480

    执行用户命令

    创建子进程,进程进程程序替换,让子进程完成用户所输入的命令,具体的代码逻辑如下:

    #define MAX 1024
    #define ARGC 64
    #define SEP " "
    
    const char* getpath(char* path)
    {
      int length = strlen(path);
      if(length == 1) return "/"; //根目录
      int i = length - 1;
      while((path[i] != '/')) i--;
      return path+i+1;
    }
    
    int split(char commandstr[], char* argv[])
    {
      argv[0] = strtok(commandstr, SEP);
      if (argv[0] == NULL) return -1; //字符串为空
      int i = 1;
        while(1)
        {
            argv[i] = strtok(NULL, SEP);
            if(argv[i] == NULL) break;
            i++;
        }
      return 0;
    }
    
    int main()
    {
      while(1)
      {
        char commandstr[MAX] = { 0 }; //接收命令行参数
        char* argv[ARGC] = { NULL };  //存储命令行参数
        printf("[%s@%s %s]$$ ", getenv("USER"), getenv("HOSTNAME"), getpath(getenv("PWD")));
        fflush(stdout);
        char* s = fgets(commandstr, sizeof(commandstr), stdin);
        commandstr[strlen(commandstr) - 1] = '\0'; //去除输入时末尾的'\n'
        int n = split(commandstr, argv);
        if(n != 0) continue; //用户输入空串
          
        pid_t id = fork();//创建子进程完成命令执行
        if (id == 0)
        {
          //子进程
          execvp(argv[0], argv); //进程程序替换
          exit(0);
        }
        
        int status = 0;
        waitpid(id, &status, 0);//回收子进程
      }
      return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 由于实现的shell用于执行系统命令,并且获取了记录命令行参数的数组,因此采用execvp进程程序替换

    效果演示:

    shell-1演示

    完整代码

    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    
    #define MAX 1024
    #define ARGC 64
    #define SEP " "
    
    const char* getpath(char* path)
    {
      int length = strlen(path);
      if(length == 1) return "/"; //根目录
      int i = length - 1;
      while((path[i] != '/')) i--;
      return path+i+1;
    }
    
    int split(char commandstr[], char* argv[])
    {
      assert(commandstr);
      assert(argv);
        
      argv[0] = strtok(commandstr, SEP);
      if (argv[0] == NULL) return -1; //字符串为空
      int i = 1;
        while(1)
        {
            argv[i] = strtok(NULL, SEP);
            if(argv[i] == NULL) break;
            i++;
        }
      return 0;
    }
    
    int main()
    {
      while(1)
      {
        char commandstr[MAX] = { 0 }; //接收命令行参数
        char* argv[ARGC] = { NULL };  //存储命令行参数
        printf("[%s@%s %s]$$ ", getenv("USER"), getenv("HOSTNAME"), getpath(getenv("PWD")));
        fflush(stdout);
        char* s = fgets(commandstr, sizeof(commandstr), stdin);
        assert(s); //对fgets函数的结果断言
        (void)s;//保证在release方式发布的时候,因为去掉assert了,所以s就没有被使用,而带来的编译告警, 什么都没做,但是充当一次使用
        commandstr[strlen(commandstr) - 1] = '\0'; //去除输入时末尾的'\n'
        int n = split(commandstr, argv);
        if(n != 0) continue; //用户输入空串
        pid_t id = fork();
        assert(id >= 0);
        (void)id;
        if (id == 0)
        {
          //子进程
          execvp(argv[0], argv); 
          exit(0);
        } 
        int status = 0;
        waitpid(id, &status, 0);
      }
      return 0;
    }
    );
        if(n != 0) continue; //用户输入空串
        pid_t id = fork();
        assert(id >= 0);
        (void)id;
        if (id == 0)
        {
          //子进程
          execvp(argv[0], argv); 
          exit(0);
        } 
        int status = 0;
        waitpid(id, &status, 0);
      }
      return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82

    说明: 该版本shell只能够执行系统命令,并且不能够支持所有系统命令比如cd命令。

  • 相关阅读:
    win10家庭版安装Docker
    移动端上拉分页加载更多(h5,小程序)
    探索Python中的装饰器
    Hutool集合相关工具类CollUtil常用方法
    Linux之shell脚本初始
    activeMq将mqtt发布订阅转成消息队列
    【算法刷题】—7.25 Dijkstra 算法应用
    树莓派传输图片到服务器处理(一)
    网络编程基础
    分享:大数据信用报告查询的价格一般要多少钱?
  • 原文地址:https://blog.csdn.net/csdn_myhome/article/details/132671651