【Linux取经路】探寻shell的实现原理

打印 上一主题 下一主题

主题 523|帖子 523|积分 1569



  
一、打印下令行提示符

  1. const char* getusername() // 获取用户名
  2. {
  3.     return getenv("USER");
  4. }
  5. const char* gethostname() // 获取主机名
  6. {
  7.     return getenv("HOSTNAME");
  8. }
  9. const char* getpwd() // 获取当前所处的目录
  10. {
  11.     char* pos = strrchr(getenv("PWD"), '/'); // 查找最后一个 ‘/’
  12.     if(*(pos+1) != '\0') return pos+1; // 说明不是根目录,返回最后一个文件夹
  13.     return pos;
  14. }
  15. void tooltip() // 打印命令行提示框
  16. {
  17.     printf(LEFT "%s@%s %s" RIGHT PROMPT" ", getusername(), gethostname(), getpwd());
  18. }
复制代码

代码分析:获取基础信息本质上是通过调用 getenv 接口来获取对应环境变量的值。借助 strrchr 函数来查找当前路径中的末了一个文件分隔符 /,它有大概是文件分隔符也有大概是根目录因此要单独判断。
二、读取键盘输入的指令

  1. char command[1024]; // 存储键盘输入的指令
  2. int getcommand(char* command, int size) // 读取指令
  3. {
  4.     memset(command, '\0', size);
  5.     char* ret = fgets(command, size, stdin); // 这里 ret 一定不为空,因为至少会输入一个回车,fgets 可以读取回车
  6.     assert(ret != NULL);
  7.     (void)ret;// “假装使用一下ret,防止有些编译器警告”
  8.     // aaabc\n\0
  9.     command[strlen(command)-1] = '\0'; // 去掉结尾的 \n
  10.     return 1;
  11. }
  12. int interact(char* command, int size) // 交互
  13. {
  14.     tooltip();
  15.     while(getcommand(command, size) && (strlen(command) == 0))
  16.     {
  17.         tooltip();
  18.     }
  19. }
  20. int main()
  21. {
  22.     interact(command, sizeof(command)); // 交互
  23.     printf("echo: %s\n", command);
  24.     return 0;
  25. }
复制代码

代码分析:键盘输入的指令本质上就是一串字符串,这里不能用 scanf 来获取字符串,由于 scanf 是不会读取空格和回车的(碰到空格和回车就制止读取),而我们一般的指令都是带选项的,指令和选项之间一般会用空格隔开,用 scanf 会导致我们指令读不全。这里使用 fgets 函数来读取键盘输入,其第一参数是存储指令的空间的首地址;第二个参数是空间的大小;第三个参数是从哪个文件流中读取,一个 C/C++ 步调默认会打开三个文件流 stdin、stdout、stderr,这里选择从 stdin 中读取,也就是从标准输入中读取。gets 函数会在结尾自动帮我们添加 \0,而且当读取的字符个数大于存储容量时,该函数会自动在结尾放 \0,因此我们可以不消考虑为 \0 预留空间或者认为的在字符串结尾加 \0。其次该函数读取成功返回 command 的首地址,否则返回 NULL,在当前场景下,除非读取错误,否则至少都会读入一个 \n,一般我们输入完指令就是敲回车,什么指令不输也敲回车,因此正常环境下 ret 不大概为 NULL。这里还要考虑删撤除读取到的 \n,由于我们不需要它,我们只要完整的指令。
三、指令切割

  1. #define SEPARATOR " " // 指令分隔符
  2. char* argv[ARGC_LONG] = {NULL}; // 存储指令和选项的起始地址
  3. void commandcut(char* command, char** argv, int argvsize) // 指令切割
  4. {
  5.     memset(argv, 0, argvsize); // 清空
  6.     char cop_command[COMMAND_LONG] = {'\0'}; // 保证 command 串不被改变
  7.     for(int i = 0; command[i] != '\0'; i++)
  8.     {
  9.         cop_command[i] = command[i];
  10.     }
  11.     // 开始切割子串
  12.     char* ret = strtok(cop_command, SEPARATOR);
  13.     int i = 0;
  14.     while(ret != NULL)
  15.     {
  16.         argv[i++] = ret;
  17.         ret = strtok(NULL, " ");
  18.     }
  19. }
  20. int main()
  21. {
  22.     while(1)
  23.     {
  24.         // 1、交互获取命令行参数
  25.         interact(command, sizeof(command)); // 交互
  26.         // 到这里说明指令已经获取到了,接下来将指令打散
  27.         // 2、指令切割
  28.         commandcut(command, argv, sizeof(argv));
  29.         for(int i = 0; argv[i]; i++)
  30.         {
  31.             printf("[%d]: %s\n", i, argv[i]);
  32.         }
  33.         printf("echo: %s\n", command);
  34.     }
  35.     return 0;
  36. }
复制代码

代码分析:这一步重要是借助 strtok 函数将获取到的指令切割成一个一个的子串,将全部子串的起始地址存储在 argv 内里。注意 strtok 函数会改变原空间的内容,因此创建了一段临时的空间 cop_command。
四、平凡下令的实行

  1. void normalcommandexecution(char** _argv, int* _lastcode) // 普通命令的执行
  2. {
  3.     pid_t id = fork();
  4.     if(id < 0)
  5.     {
  6.         perror("fork");
  7.     }
  8.     else if(id == 0)
  9.     {
  10.         // child
  11.         int ret = execvp(_argv[0], _argv);
  12.         if(ret == -1)
  13.         {
  14.             perror("exeecp");
  15.             exit(EXIT_CODE);
  16.         }
  17.     }
  18.     else
  19.     {
  20.         // father
  21.         int status;
  22.         pid_t ret = waitpid(id, &status, 0); // 阻塞等待
  23.         if(ret == id)
  24.         {
  25.             *_lastcode = WEXITSTATUS(status);
  26.         }
  27.     }
  28. }
  29. int main()
  30. {
  31.     while(1)
  32.     {
  33.         // 1、交互获取命令行参数
  34.         interact(command, sizeof(command)); // 交互
  35.         // 到这里说明指令已经获取到了,接下来将指令打散
  36.         // 2、指令切割
  37.         commandcut(command, argv, sizeof(argv));
  38.         
  39.         // 3、普通命令执行
  40.         normalcommandexecution(argv, &lastcode);
  41.     }
  42.     return 0;
  43. }
复制代码

代码分析:对于 ls 这种平凡指令(非内建指令),先通过 fork 创建子进程,然后再调用 execvp 接口进行步调更换,去实行输入的指令。
五、内建指令实行

5.1 cd指令

  1. bool isnormalcommand(char **_argv) // 指令判断
  2. {
  3.     if (strcmp(_argv[0], "cd") == 0)
  4.         return false;
  5.     return true;
  6. }
  7. void changpwd(char** _argv) // 更改当前工作目录
  8. {
  9.     chdir(_argv[1]); // 更改当前工作目录
  10.     // getpwd(pwd, sizeof(pwd));
  11.     sprintf(getenv("PWD"), "%s", getcwd(pwd, sizeof(pwd))); // 修改环境变量
  12. }
  13. void builtincommand(char **_argv) // 内建命令执行
  14. {
  15.     if (strcmp(_argv[0], "cd") == 0)
  16.     {
  17.         changpwd(_argv);
  18.     }
  19. }
  20. int main()
  21. {
  22.     while (1)
  23.     {
  24.         // 1、交互获取命令行参数
  25.         interact(command, sizeof(command)); // 交互
  26.         // 到这里说明指令已经获取到了,接下来将指令打散
  27.         // 2、指令切割
  28.         commandcut(command, argv, sizeof(argv));
  29.         // 3、指令判断
  30.         // 3、普通命令执行
  31.         if (isnormalcommand(argv)) // 普通指令
  32.             normalcommandexecution(argv, &lastcode);
  33.         else // 内建指令
  34.             builtincommand(argv);
  35.     }
  36.     return 0;
  37. }
复制代码

代码分析:要考虑内建指令,那在指令切割之后要先对指令进行判断。内建指令不需要创建子进程去实行,而是直接由当前的 bash 进程去实行。比如说 cd 指令,实行完 cd 指令后,我们要让当前的 bash 更改工作目录,而不是让其创建子进程去实行 cd 指令,那样改变的就是子进程的工作目录。可以发现,一个指令实行完后,如果会对 bash 产生影响,那么它就必须是内建指令。其次关于 cd 指令,它改变了当前的工作目录,这一点该如何理解呢?我 myshell 就是一个可实行步调,我的源代码和编译得到的可实行文件始终都放在 /home/wcy/linux-s/2023-10-28a/myshell 目录下,你 cd 下令凭什么能改变我的工作目录?着实并否则,这里改变工作目录是:一个可实行步调在变成进程产生 PCB 对象后,PCB 内里维护了一个属性就叫做当前可实行步调的工作目录,cd 指令改变的着实就是这一属性,并不是改变 myshell 步调的存储位置,我们通过调用 chdir 体系调用来修改这一属性。末了,由于我们前面是通过环境变量来获取当前工作目录,而环境变量在被当前 myshell 进程从父进程继续下来后是不会自动发生改变的,因此在实行完 cd 指令后,我们要对 PWD 环境变量进行修改,环境变量本质上就是存储在内存中的一段字符串信息,因此我们可以采用 sprintf 函数对该字符串信息进行修改。

5.2 export指令

  1. #define USER_ENV_SIZE 100  // 允许用户添加的环境变量个数
  2. #define USER_ENV_LONG 1024 // 用户一个环境变量的最大长度
  3. char userenv[USER_ENV_SIZE][USER_ENV_LONG]; // 保存用户添加的环境变量
  4. int userenvnum = 0;                         // 当前用户输入的环境变量个数
  5. void exportcommand(char** _argv, char(*_userenv)[USER_ENV_LONG], int* _userenvnum)
  6. {
  7.     // 将用户输入的环境变量存储起来
  8.     strcpy(_userenv[*_userenvnum], _argv[1]);
  9.     int ret = putenv(_userenv[(*_userenvnum)++]);
  10.     if (ret == 0)
  11.         perror("putenv");
  12. }
复制代码

代码分析:只要 bash 不退出,我们每次添加的环境变量都应该被生存起来,我们输入的环境变量是被当做指令生存在 command 内里,当下一次输入指令,上一次输入的内容就会被清空。putenv 添加环境变量,并不是把对应的字符串拷贝到体系的表当中,而是把该字符串的地址生存在体系的表中,因此我们要确保生存环境变量字符串的那个地址里的环境变量不会被修改,所以我们需要为用户输入的环境变量,也就是那一串字符串单独开辟一块空间进行存储,包管在内次重新输入指令的时候,不会影响到之前用户添加的环境变量。由于环境变量本质就是一个字符串,所以这里我们定义了一个字符二维数组来存储用户输入的环境变量,先把用户输入的环境变量存入我们定义的这个数组,然后再调用 putenv 函数将数组中的内容添加到当前的环境变量。如许就可以包管只要当前 bash 不退出,用户历史上添加的环境变量都在。这里涉及到二维数组传参的题目,再来回顾一下,数组名表示首元素地址,二维数组的首元素是一个一维数组,所以函数形参的类型是一个字符一维数组的地址,也就是 char(*)[USER_ENV_LONG]。
5.3 echo指令

  1. void echocommand(char **_argv, int _argc)
  2. {
  3.     if (_argv[1][0] == '$')
  4.     {
  5.         char *ptr = _argv[1] + 1;
  6.         printf("%s\n", getenv(ptr));
  7.     }
  8.     else
  9.     {
  10.         int i = 1;
  11.         while (i < _argc)
  12.         {
  13.             char *ret = strtok(_argv[i], """);
  14.             while (ret != NULL)
  15.             {
  16.                 printf("%s", ret);
  17.                 ret = strtok(NULL, """);
  18.             }
  19.             printf("%c", ' ');
  20.             i++;
  21.         }
  22.         printf("\n");
  23.     }
  24. }
复制代码

代码分析:echo 指令需要考虑将输入的 " 去掉,其次大概连续输入多个字符串,还要考虑 echo 和 $ 共同使用是去打印环境变量的值。
小结:当我们登陆的时候,体系就是要启动一个 shell 进程,我们 shell 本身的环境变量是在用户登录的时候,shell 会读取用户目录下的 .bash_profile 文件,内里生存了导入环境变量的方式。


六、结语

本日的分享到这里就竣事啦!如果觉得文章还不错的话,可以三连支持一下,春人的主页另有很多有趣的文章,欢迎小伙伴们前去点评,您的支持就是春人进步的动力!


免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

x
回复

使用道具 举报

0 个回复

倒序浏览

快速回复

您需要登录后才可以回帖 登录 or 立即注册

本版积分规则

tsx81429

金牌会员
这个人很懒什么都没写!

标签云

快速回复 返回顶部 返回列表