• Linux-Shell命令行解释器的模拟实现


    引言:本篇文章主要是简单实现一个shell命令行解释器,可以支持基础常见的linux的命令,支持内建命名echo、cd,同时支持重定向的操作!

    一、代码剖析

    1. 头文件引入:

     因代码是在linux下实现,引入的大多头文件是Linux的系统调用,建议在linux环境下使用。

    1. #include
    2. #include
    3. #include
    4. #include
    5. #include
    6. #include
    7. #include
    8. #include
    9. #include
    10. #include

     这些头文件包含了一些需要使用的库函数和系统调用。

     2. 定义一些常量和全局变量:

    这些常量和变量用于存储命令行输入、命令参数、重定向类型和重定向文件等信息。 

    1. #define OPTION 64
    2. #define NUM 1024
    3. //定义重定向类型
    4. #define NONE_REDIR 0
    5. #define INPUT_REDIR 1
    6. #define OUTPUT_REDIR 2
    7. #define APPEND_REDIR 3
    8. //定义个宏函数,用于将字符串指针移动到无空格的第一个字符上
    9. #define trimSpace(start) do{\
    10. while(*start == ' '){\
    11. start++;}\
    12. }while(0)
    13. //保存键盘上输入的命令
    14. char lineComend[NUM];
    15. char* myargv[OPTION];//定义一个char*的指针数组,用于存放参数
    16. int lastCode = 0;//父进程记录上次子进程执行结果
    17. int lastSig = 0;
    18. //记录是否是重定向和类型
    19. int redirType = NONE_REDIR;
    20. char *redirFile = NULL;//记录重定向文件名

     3.  定义一个辅助函数find_Redirect:

    这个函数用于查找命令行参数中的重定向符号(>和<)并解析相关的重定向文件和类型。

    1. void find_Redirect(char* argv){
    2. char* start = NULL;
    3. for(int i = 0;i<(int)strlen(argv);i++){
    4. if(argv[i] == '>'||argv[i]=='<'){
    5. if(argv[i]=='<'){
    6. //cat < log.txt
    7. argv[i] = '\0';
    8. //去除重定向后面的空格,只保留文件名部分,存放到全局变量redirFile中
    9. start = argv+i+1;
    10. trimSpace(start);
    11. redirFile = start;
    12. redirType = INPUT_REDIR;
    13. }else{
    14. if(argv[i+1] == '>'){
    15. //cat xiaomi >> log.txt
    16. argv[i] = '\0';
    17. start = argv+i+2;
    18. trimSpace(start);
    19. redirFile = start;
    20. redirType = APPEND_REDIR;
    21. }else{
    22. argv[i] = '\0';
    23. start = argv+i+1;
    24. trimSpace(start);
    25. redirFile = start;
    26. redirType = OUTPUT_REDIR;
    27. }
    28. }
    29. }
    30. }
    31. }

    4. 主函数

    • 主函数包含一个无限循环,在每次循环中等待用户输入命令,并处理命令执行、重定向和内置命令等逻辑。
    • 在循环的开始,通过printf输出命令提示符,然后使用fgets读取用户输入的命令行,并处理去除结尾的换行符。
    • 接下来,调用find_Redirect函数来查找重定向符号,并解析重定向文件和类型。
    • 然后,使用strtok函数对输入命令进行切割,将切割后的命令参数存储到myargv数组中。同时,处理一些特殊的内置命令,如cdecho
    • 对于echo命令,根据重定向类型进行输出重定向。
    • 如果不是内置命令,则创建子进程,并在子进程中执行命令。根据重定向类型,进行输入输出重定向。
    • 最后,使用waitpid等待子进程执行完毕,并获取子进程的退出状态码和信号。
    1. int main(){
    2. //一开始先初始化重定向文件和类型
    3. while(1){
    4. redirFile = NULL;
    5. redirType = NONE_REDIR;
    6. printf("[suhh@ziqiang address]$ ");
    7. fflush(stdout);
    8. //用户输入
    9. assert(fgets(lineComend,sizeof(lineComend)-1,stdin)!=NULL);
    10. //清除数组最后一个\n
    11. lineComend[strlen(lineComend)-1] = '\0';
    12. //ls -a -l
    13. //切割字符串 靠' '
    14. find_Redirect(lineComend);
    15. myargv[0] = strtok(lineComend," ");
    16. // char * echo_str = NULL;
    17. if(strcmp(myargv[0],"echo")==0){
    18. myargv[1] = myargv[0] + strlen(myargv[0]) + 1;//取出剩余字串
    19. goto echo_start;
    20. }
    21. //strtok 会把剩余的放到静态变量中,下次调用只用穿null
    22. int i = 1;
    23. //myargv[end]=NULL
    24. if(myargv[0]!=NULL&&(strcmp(myargv[0],"ls")==0||strcmp(myargv[0],"ll")==0)){
    25. myargv[i++] = (char*)"--color=auto";
    26. if(strcmp(myargv[0],"ll")==0) {
    27. myargv[i++] = (char*)"-l";
    28. myargv[0] = (char*)"ls";
    29. }
    30. //让ls命令默认加上颜色
    31. }
    32. while(myargv[i++] = strtok(NULL," ")){}
    33. if(strcmp(myargv[0],"cd")==0){
    34. //如果是cd命令,不能用子进程执行,如果用子进程执行不会影响父进程,达不到效果
    35. //这时这就是内建命令
    36. if(myargv[1]!=NULL)chdir(myargv[1]);
    37. continue;
    38. }
    39. echo_start:
    40. if(myargv[0]!=NULL && strcmp(myargv[0],"echo")==0){
    41. //这也是内建命令
    42. //判断重定向类型
    43. int fd = 0;
    44. if((redirType == OUTPUT_REDIR)||(redirType == APPEND_REDIR)){
    45. int flags = O_WRONLY | O_CREAT;
    46. if(redirType == APPEND_REDIR) flags = flags | O_APPEND;
    47. else flags = flags | O_TRUNC;
    48. fd = open(redirFile,flags,0666);
    49. //先把标准输出流备份一下
    50. dup2(1,5);
    51. //重定向
    52. dup2(fd,1);
    53. }
    54. if(myargv[1]!=NULL&&strcmp(myargv[1],"$?")==0){
    55. printf("%d,%d\n",lastCode,lastSig);
    56. }else{
    57. if(myargv[1]!=NULL){
    58. //如果用户输入 echo "hello" 去除“”
    59. if(myargv[1][0] =='"'){
    60. myargv[1] = (char*)&myargv[1][1];
    61. }
    62. int str_len = strlen(myargv[1]);
    63. if((myargv[1][str_len-1] == '"')||(myargv[1][str_len -2] == '"')){
    64. if(myargv[1][str_len-1] == '"') str_len --;
    65. else str_len -= 2;
    66. }
    67. for(int i = 0 ;i
    68. printf("%c",myargv[1][i]);
    69. }
    70. printf("\n");
    71. }
    72. }
    73. if((redirType == OUTPUT_REDIR)||(redirType == APPEND_REDIR)){
    74. //恢复标准输出流
    75. close(fd);
    76. dup2(5,1);
    77. }
    78. continue;
    79. }
    80. pid_t it = fork();
    81. assert(it>=0);
    82. if(it == 0){
    83. //判断重定向
    84. //cat < log.txt
    85. if(redirType == INPUT_REDIR){
    86. int rfd = open(redirFile,O_RDONLY);
    87. if(rfd == -1){
    88. perror("错误:open");
    89. exit(errno);
    90. }
    91. dup2(rfd,0);
    92. }else if((redirType == OUTPUT_REDIR)||(redirType == APPEND_REDIR)){
    93. int flags = O_WRONLY | O_CREAT;
    94. if(redirType == OUTPUT_REDIR) flags |= O_TRUNC;
    95. else flags |= O_APPEND;
    96. int wfd = open(redirFile,flags,0666);
    97. assert(wfd>=0);
    98. (void)wfd;
    99. dup2(wfd,1);
    100. }
    101. //子进程执行进程程序替换
    102. execvp(myargv[0],myargv);
    103. exit(1);
    104. }
    105. int status = 0;
    106. int wp = waitpid(it,&status,0);//阻塞等待
    107. assert(wp>=0);
    108. (void)wp;
    109. lastCode = (status>>8)&0xFF;
    110. lastSig = status&0x7F;
    111. printf("exit_code:%d,exit_signal:%d\n",lastCode,lastSig);
    112. }
    113. return 0;
    114. }

     二、 涉及知识点

     1. fork()函数

    fork函数属于系统调用,用于创建子进程,返回子进程pid给父进程,返回0给子进程.

    • fork之后会有两个进程分别执行后续的代码(其实在fork函数体内,父子进程已经产生,所以有两个返回值)
    • 子进程拥有自己的PCB和虚拟地址,这些都是和父进程一样的,也就是说,父进程的虚拟地址的内容拷贝了一份给子进程。
    • 因为子进程拥有和父进程一样的虚拟地址,体现在代码上,就是子进程也可以”共有“父进程定义的变量等,但如果子进程要改变它的值,就会发生写时拷贝,操作系统会在物理内存中重新开辟一段空间,再通过页表,重新映射到虚拟地址处。换句话说,子进程以为访问的虚拟地址是父进程一起共用的,实际上是另一个物理地址。

    2. 进程程序替换

    将指定进程加载到内存中,替换原本进程的代码段和数据段,让这个进程以为在执行自己的代码,其实是在执行别人的代码。

     

     在C语言中提供了丰富的函数用于程序替换

    函数原型: int execl(const char *path, const char *arg, ...); 

    举例:execl("/user/bin/ls","ls",NULL);

    解释:第一个参数是填要替换的程序在哪,路径

               第二个参数是要填这个程序要怎么执行

               第三个参数是要填这个程序要带什么参数,要以NULL结尾

    错误返回-1

     函数原型:int execlp(const char *file, const char *arg, ...);

    举例:execlp("ls","ls",NULL);

    解释:在上一个函数原型的基础上了一个p,代表path,此时无需传入程序地址,只需告诉程序叫什么,会自动在环境变量中进行可执行程序的查找。

               第一个参数填程序名

               第二个参数是要填这个程序要怎么执行

               第三个参数是要填这个程序要带什么参数,要以NULL结尾

    错误返回-1

     函数原型:int execv(const char *path, char *const argv[])

    举例:char* argv[] ={"ls","-a","-l","--color = auto",NULL};

               execv("/user/bin/ls",argv);

    解释:这次加了一个v,可以将所有可执行参数放到一个字符串指针数组中,统一传递。

               第一个参数是填要替换的程序在哪,路径

               第二个参数是要填字符串指针数组

    错误返回-1

     函数原型:int execvp(const char *file, char *const argv[]);

    举例:execvp("ls",argv);

    解释:这个加了v和p,拥有了前两个的特性。本文中的代码就是用的这个

               第一个参数填程序名

               第二个参数是要填字符串指针数组

    错误返回-1

  • 相关阅读:
    排序算法复习 | 插入排序(直接插入排序、希尔排序)与选择排序(直接选择排序、堆排序)
    【尚硅谷React】——React全家桶笔记
    Ubuntu docker安装mysql
    深度探讨丨关于工作量证明的常见误解
    el-dialog关闭后表单数据缓存没清空【已解决】
    【CMU15-445 Part-4】DatabaseStorage ii
    python的中秋之美
    孙卫琴的《精通Vue.js》读书笔记-CSS中DOM元素的过渡模式
    分布式事务之Seata AT
    【最详细】最新最全Redis面试大全(42道)
  • 原文地址:https://blog.csdn.net/weixin_56821642/article/details/134247743