demo xshell (程序替换 工作目录 内建命令)

1.程序替换

在学习完一些列的进程替换接口之后我们大概就能知道,我们的环境变量以及命令行参数是如何传递给子进程的,这些参数是我们在调用进程替换时就传给了子进程的数据。

那么如果我们自己要实现一个简单的命令行解释器,我们是不是首先就需要对命令行的参数进行解析? 命令行参数的解析我们首先需要获取一行字符串,然后以空格为间隔将字符串拆分为程序名和选项。

复制代码
     #include<stdio.h>
     #include<assert.h>
     #include<string.h>
     #include<stdlib.h>
     #include<unistd.h>
     #include<sys/types.h>
     #include<sys/wait.h>
     
     char stringArg[1024]; //获取命令行输入
    char* argv[32]; //拆解后的命令行参数数组
    
    int main()
    {
        stringArg[1023]=0;
        //printf("%s\n",getenv("HOST"));
    
        while(1)
        {
            memset(stringArg,0,1024); //全部初始化\0 ,便于计算长度
            memset(argv,0,32*sizeof(char*));//全部初始化为NULL,命令行数组的结尾必须是NULL
            //打印一行提示信息   用户名@主机名 当前路径$
            printf("[%s@ %s %s]$ ",getenv("USER"),getenv("HOSTNAME"),getenv("PWD")); //不换行
            fflush(stdout);   //将提示信息打印出来
            //获取字符串   要读空格,不能用 scanf ,使用fgets
            fgets(stringArg,1023,stdin);
            stringArg[strlen(stringArg)-1]=0; //将最后的 \n 换成 \0 
            if(strlen(stringArg)==0)
            {
                //什么也没输入
                continue;
            }
            //拆解字符串 strtok
            int i=0;
            argv[i++]=strtok(stringArg," ");
          while(argv[i++]=strtok(NULL," ")); //最后一次当字符串结束再使用strtok,会返回NULL,刚好作为循环结束条件
            
    #ifdef _DEBUG_ARGV//测试切割功能
            char**p=argv;
            while(*p)
                printf("%s ",*p++);
            printf("\n");
    #endif
        }
    
    
        return 0;
    }   

切割字符串的工作可以直接使用 strtok 函数来完成。

完成切割字符串的工作之后,我们就可以将该命令行数组用于程序替换的参数了,这是最基础的shell。

复制代码
            //程序替换
            pid_t id=fork();
            if(id<0)
            {
                perror("fork failed");
                exit(1);
            }
            if(id==0)
            {
                //程序替换
                execvp(argv[0],argv);
                //如果替换失败则会执行下面的代码
                perror("execvp failed");
                exit(1);
            }
    
            //父进程等待回收僵尸
            int status=0;
            waitpid(id,&status,0);
            if(WIFEXITED(status))//正常退出
            {
                if(WEXITSTATUS(status))//退出码不为0 
                {
                    printf("运行成功,退出码为 :%d\n",  WEXITSTATUS(status));
                }
            }
            else//异常终止
            {
                printf("%s\n",strerror(errno));
            }

2.进程工作目录

在上面的代码实现下还有很多的小问题,比如我们使用cd命令的时候并不能切换目录,这是为什么呢?

要理解这个问题,首先我们需要知道一个概念:进程的工作目录

当我们启动一个进程时,我们打开进程目录 /proc,找到我们的进程,然后进到我们进程的目录中,我们能够看到两个特殊的东西

exe就是我们的二进制可执行程序的位置,这个我们很好理解,而cwd是一串路径,这就是进程的工作目录,当我们启动一个进程时,一般是我们在哪个路径下启动的这个进程,工作路径就是当前所在路径。我们可以在其他路径下降这个程序跑起来观察一下。

当我们在上一级目录下将该程序跑起来,进程的工作目录就变成了执行运行命令时所在的路径,这就是进程的工作路径。

那么进程工作路径可以修改吗?

在命令行中,我们的cd命令就是修改进程工作路径的,也就是我们的shell的工作路径,所以我们经常能够通过cd命令进入不同的路径,我们的环境变量PWD其实严格上来说就是当前进程工作路径。 而在程序中,我们可以使用 chdir 来更改工作目录,

到了这里,我们就能知道为什么我们的shell的cd命令不起作用了 ,因为我们是通过创建子进程然后进行程序替换执行的cd命令,那么修改的就是子进程的工作目录,而子进程工作目录被修改时,发生写时拷贝,不会影响父进程。而我们pwd查的是父进程的工作目录,所以我们的cd命令其实不应该让子进程执行,而是由我们的父进程自己执行,因为cd命令要修改的是我们当前进程也就是父进程的工作目录。

像这种不需要创建子进程来执行的,而是让shell自己执行的命令,叫做内建命令或者内置命令

比如我们前面提到的 echo 命令也是一个内建命令,这就是他为什么能够显示本地变量的原因,因为他不是通过创建子进程来执行的,而是shell自己执行。

如何模拟实现cd命令呢? 其实也很简单,在创建子进程之前判断一下 命令的第一个字串是否为 cd,如果是 cd ,我们就直接使用chdir来更换工作目录。同时我们还要判断一下是否切换成功。

复制代码
            //内建命令
            if(strcmp("cd",argv[0])==0)  //cd
            {
                const char*changepath=argv[1];
                int ret=chdir(changepath);
                if(ret==-1)
                {                                                                                                                                                                                               
                    printf("%s\n",strerror(errno));
                }
                continue; 
                                                                                                                                                                                     
            }    

而如果是echo目录,我们也是直接在当前目录下执行,而是直接在父进程打印。但是echo还有一个问题,就是空格我们输入的内容,也需要打印,所以我们需要在切割第一个字串串之后判断是否为echo,如果为echo,后续就先不切割了,然后再判断是否有 符号,也就是在stringArg\[5\]是否为 ,如果为$ ,则需要去找我们的变量列表中的变量来打印。但是我们这里只支持环境变量就够了,其他的实现起来太复杂。

3.重定向

重定向的符号无非就是 > >> < ,而且不会出现在第一个子串上,所以只需要在将第一个基础命令切割出来之后,判断一下是否有重定向的符。判断完之后将重定向符号变为空格,以便切割。

同时,重定向只能在子进程中去替换 fd ,也就是在程序替换之前替换,防止父进程也被替换了

复制代码
            //判断是否有重定向
            int j=0;
            for(j=0;j<strlen(stringArg);++j)
            {
                if(stringArg[j]=='<')
                {
                    //输入重定向
                    MODE=CHANGEIN;
                    stringArg[j]='\0';
                    filename=&stringArg[j+1];
                    break;
                }
                else if(stringArg[j]=='>')
                {
                    stringArg[j]='\0';
                    if(stringArg[j+1]=='>')
                    {
                        j++;
                        MODE=CHANGEAPPEND;
                        stringArg[j]='\0';
                        filename=&stringArg[j+1];
                        break;
                    }
                    else
                    {
                        MODE=CHANGEOUT;
                        filename=&stringArg[j+1];
                        break;
                    }
                }
            }

                  if(id==0)
                {
                   //先检查是否有重定向
                   if(MODE==CHANGEOUT)
                   {
                       int fd=open(filename,O_WRONLY|O_CREAT|O_TRUNC,0666);
                       dup2(fd,1);
                   }
                   if(MODE==CHANGEIN)
                   {
                       int fd=open(filename,O_RDONLY);
                       dup2(fd,0);
                   }
                   if(MODE==CHANGEAPPEND)
                   {
                       int fd = open(filename,O_WRONLY|O_CREAT|O_APPEND,0666);
                       dup2(fd,1);
                   }
                    //程序替换
                    execvp(argv[0],argv);
                    //如果替换失败则会执行下面的代码
                    perror("execvp failed");
                    exit(1);
                }

为什么要把重定向符号设置为 \0 呢?因为我们不想要在切割字串的时候还将后面的文件名也切割进去,我们默认这些符号后面就是目标的文件名了。

完整代码

复制代码
      #include<stdio.h>
    2 #include<assert.h>
    3 #include<string.h>
    4 #include<stdlib.h>
    5 #include<unistd.h>
    6 #include<sys/types.h>
    7 #include<sys/wait.h>
    8 #include<errno.h>
    9 #include<sys/stat.h>
   10 #include<fcntl.h>
   11 
   12 
   13 char stringArg[1024]; //获取命令行输入
   14 char* argv[32]; //拆解后的命令行参数数组
   15 #define CHANGEOUT 1  //输出重定向
   16 #define CHANGEIN 2  //输入重定向
   17 #define CHANGEAPPEND 4  //追加重定向
   18 char*filename; //重定向的目标文件
   19 int MODE;  //记录是否重定向
   20 
   21 
   22 int main()
   23 {
   24     stringArg[1023]=0;
   25     //printf("%s\n",getenv("HOST"));
   26 
   27     while(1)
   28     {  
   29         //重置errno
   30         errno=0;
   31         MODE =0 ;
   32 
   33         //获取字符串提取命令行
   34 
   35         memset(stringArg,0,1024); //全部初始化\0 ,便于计算长度
   36         memset(argv,0,32*sizeof(char*));
   37         //打印一行提示信息   用户名@主机名 当前路径$
   38         printf("[%s@ %s %s]$ ",getenv("USER"),getenv("HOSTNAME"),getenv("PWD"));   //不换行
   39         fflush(stdout);   //将提示信息打印出来
   40         //获取字符串   要读空格,不能用 scanf ,使用fgets
   41         fgets(stringArg,1023,stdin);
   42         stringArg[strlen(stringArg)-1]=0; //将最后的 \n 换成 \0 
   43         if(strlen(stringArg)==0)
   44         {
   45             //什么也没输入
   46             continue;
   47         }
   48         
   49         //判断是否有重定向
   50         int j=0; 
              for(j=0;j<strlen(stringArg);++j)                                                                                                                                                                    
   52         {
   53             if(stringArg[j]=='<')
   54             {
   55                 //输入重定向
   56                 MODE=CHANGEIN;
   57                 stringArg[j]='\0';
   58                 filename=&stringArg[j+1];
   59                 break;
   60             }
   61             else if(stringArg[j]=='>')
   62             {
   63                 stringArg[j]='\0';
   64                 if(stringArg[j+1]=='>')
   65                 {
   66                     j++;
   67                     MODE=CHANGEAPPEND;
   68                     stringArg[j]='\0';
   69                     filename=&stringArg[j+1];
   70                     break;
   71                 }
   72                 else
   73                 {
   74                     MODE=CHANGEOUT;
   75                     filename=&stringArg[j+1];
   76                     break;
   77                 }
   78             }
   79         }
   80 
   81         //拆解字符串 strtok
   82         int i=0;
   83         argv[i++]=strtok(stringArg," ");
   84         if(strcmp("echo",argv[0])==0&&MODE==0) //echo
   85         {
   86             //检查$
   87             if(stringArg[5]=='$')
   88             {
   89                 //只考虑环境变量
   90                 printf("%s\n",getenv(&stringArg[6]));
   91             }
   92             else
   93                 printf("%s\n",&stringArg[5]);
   94             continue;
   95         }
W> 96         while(argv[i++]=strtok(NULL," ")); //最后一次当字符串结束再使用strtok,会返回NULL,刚好作为结束条件以及命令行参数数组的结尾NULL
   97        
   98 
   99         //内建命令
  100         if(strcmp("cd",argv[0])==0)  //cd
              {
  102             const char*changepath=argv[1];                                                                                                                                                                  
  103             int ret=chdir(changepath);
  104             if(ret==-1)
  105             {
  106                 printf("%s\n",strerror(errno));
  107             }
  108             continue; 
  109             
  110         }
  111 
  112         //程序替换
  113         else
  114         {
  115             pid_t id=fork();
  116          
  117              if(id<0)
  118              {
  119                  perror("fork failed");
  120                  exit(1);
  121              }
  122              if(id==0)
  123              {
  124             //先检查是否有重定向
  125             if(MODE==CHANGEOUT)
  126             {
  127                 int fd=open(filename,O_WRONLY|O_CREAT|O_TRUNC,0666);
  128                 dup2(fd,1);
  129             }
  130             if(MODE==CHANGEIN)
  131             {
  132                 int fd=open(filename,O_RDONLY);
  133                 dup2(fd,0);
  134             }
  135             if(MODE==CHANGEAPPEND)
  136             {
  137                 int fd = open(filename,O_WRONLY|O_CREAT|O_APPEND,0666);
  138                 dup2(fd,1);
  139             }
  140                  //程序替换
  141                  execvp(argv[0],argv);
  142                  //如果替换失败则会执行下面的代码
  143                  perror("execvp failed");
  144                  exit(1);
  145              }
  146 
  147              //父进程等待回收僵尸
  148              int status=0;
  149              waitpid(id,&status,0);
  150              if(WIFEXITED(status))//正常退出
                  {
  152                  if(WEXITSTATUS(status))//退出码不为0 
  153                  {
  154                      printf("运行成功,退出码为 :%d\n",  WEXITSTATUS(status));
  155                  }
  156              }
  157              else//异常终止
  158              {
  159                  printf("%s\n",strerror(errno));
  160              }
  161         }
  162 
  163 
  164 #ifdef _DEBUG_ARGV//测试切割功能
  165         char**p=argv;
  166         while(*p)
  167             printf("%s ",*p++);
  168         printf("\n");
  169 #endif
  170     }
  171 
  172 
  173     return 0;
  174 }
相关推荐
FoldWinCard19 小时前
D5 Linux 网络及端口命令
linux·运维·服务器
零涂毕业设计20 小时前
毕业设计模块开发-OLED显示屏(IIC协议0.96寸)STM32 ESP32 FPGA Linux驱动免费分享
linux·stm32·单片机·嵌入式·esp32·fpga·oled
tedcloud12320 小时前
OmniRoute怎么部署?开源AI模型路由平台Linux部署教程
linux·服务器·人工智能·开源·音视频
霸道流氓气质20 小时前
KMS 密钥管理服务(Key Management Service)原理与实践
linux·服务器·数据库
SakitamaX20 小时前
LVS(Linux Virtual Server)概念详解
linux·运维·lvs
小张同学a.21 小时前
Linux系统管理
linux·运维
阿龙的工作记录21 小时前
深入理解Linux设备驱动模型
linux·嵌入式硬件·工作记录
代码村新手21 小时前
Linux的基本指令
linux·运维·服务器
hkj88081 天前
Ubuntu 切换java版本
java·linux·ubuntu
撩得Android一次心动1 天前
Linux编程笔记3【个人用】
linux·笔记