Write your own command line interpreter

Write your own command line interpreter

When I clicked on xshell to run the server, bash was loaded into the memory, and all the programs I executed on bash after that were as sub-processes of bash. Create a child process in the bash process and let the child process execute brand new code. Isn't this just program replacement?

So we let the child process perform program replacement and execute commands in our program, so don't we just write our own command line interpreter? This article will lead readers to consolidate part of the previous knowledge by implementing a simple command line interpreter.

1. Build a framework

When we open the server, there is a prompt on the far right, including the user name and server name and the current path; and it supports multiple inputs, so an infinite loop can be used here, and the process replacement is performed by the child process, so fork must be used function.

Through the study of environment variables, we know that the main function also has parameters, and there is an array of argv pointers. The content stored in this array is the command I input, and argv[0] stores the address of the command I want to execute. Later The contents are all options that come with my command. So I can also create an array to store the instructions I input (separated by spaces to separate the program I want to execute and the options it brings), and use the library function strtok to cut.

At this point, we can build such code according to our needs:

#include<stdio.h>
#incldue<string.h>
#include<unistd.h>
#include<assert.h>

#define NUM 1024
#define MAX 20

char LineCommand[NUM];//允许输入指令最大长度为1024
char*myargv[MAX];//执行程序+选项最多20条

int main()
{
    while(1)
    {
        printf("用户名@服务器 当前路径:");
        fflush(stdout);//上面没带\n,这里要强制刷新
        
        //将键盘中输入的字符全部存入数组中
        char*s=fgets(LineCommand,sizeof(LineCommand)-1,stdin);//留一个位置存放\0
        assert(s);//暴力检查,s不能为空
        
        
        //分割,使用库函数stork
        myargv[0]=strtok(LineCommand," ");
        int i=1;
        while(myargv[i++]=strtok(NULL," "));//循环切割,先将切割后的结果赋值给myargv,再将这个值作为判断,strtok在结束时会返回空
        
        //测试一下是否切割成功
		for(int i=0;myargv[i];i++)
        {
            printf("myargv[%d]:%s\n",i,myargv[i]);
        }
    }
    return 0;
}

It can be seen that the command is cut successfully, but it seems that there is an extra newline in the output. This is because the carriage return will definitely be entered when entering the command outside. So the last element in the array storing the command is \n, if you don’t want this, you can replace the last element with\0

When the cutting command is all right, you can start to use the subprocess to execute the process replacement to execute the system's instructions.

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

#define NUM 1024
#define MAX 20

char LineCommand[NUM];//允许输入指令最大长度为1024
char*myargv[MAX];//执行程序+选项最多20条

int main()
{
    
    
    while(1)
    {
    
    
        printf("用户名@服务器 当前路径:");
        fflush(stdout);//上面没带\n,这里要强制刷新
        
        //将键盘中输入的字符全部存入数组中
        char*s=fgets(LineCommand,sizeof(LineCommand)-1,stdin);//留一个位置存放\0
        assert(s);//暴力检查,s不能为空
        
        LineCommand[strlen(LineCommand)-1]=0;//处理最后一个元素为'\n'
        
        //分割,使用库函数stork
        myargv[0]=strtok(LineCommand," ");
        int i=1;
        while(myargv[i++]=strtok(NULL," "));//循环切割,先将切割后的结果赋值给myargv,再将这个值作为判断,strtok在结束时会返回空
        
        pid_t id=fork();
        assert(id!=-1);//fork失败返回-1
        if(id==0)
        {
    
    
            //子进程内部执行进程替换,我们有了数组,优先考虑使用带p的
            execvp(myargv[0],myargv);
            exit(-1);//如果程序替换失败就返回-1
    
        }
       
        //父进程要回收子进程资源
        int status=0;
        pid_t ret=waitpid(id,&status,0);//非阻塞式等待
        
    }
    return 0;
}

insert image description here

It can be seen that the command can be executed at this time, but there are still several problems here

1. There is no color difference when using the ls command: this is because there is one less "--color=auto" option, we can make appropriate enumerations for some commands to solve this problem

2. cd ..Unable to fall back to the superior path: This is related to the current path of the current process (the current path is the working path of this process), which can be chdirchanged by

3. Unable to echo $?query the exit code of the last command: To get the exit code of the last time, I first need to save the exit code of the last time, so I need to define two variables. In addition, I need to enumerate to make ? The exit code of the second time instead of printing to the screen? Change to output the last exit code instead of printing to the screen? Change to output the last exit code instead of printing it to the screen ?

2. Improve the code through simple enumeration

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

#define NUM 1024
#define MAX 20

char LineCommand[NUM];//允许输入指令最大长度为1024
char*myargv[MAX];//执行程序+选项最多20条
int lastcode=0;//上个程序的退出码
int lastsig=0;//上个程序的退出信号

int main()
{
    
    
    while(1)
    {
    
    
        printf("用户名@服务器 当前路径:");
        fflush(stdout);//上面没带\n,这里要强制刷新
        
        //将键盘中输入的字符全部存入数组中
        char*s=fgets(LineCommand,sizeof(LineCommand)-1,stdin);//留一个位置存放\0
        assert(s);//暴力检查,s不能为空
        
        LineCommand[strlen(LineCommand)-1]=0;//处理最后一个元素为'\n'
        
        //分割,使用库函数stork
        myargv[0]=strtok(LineCommand," ");
        int i=1;
        
        //让ls选项带颜色标识
        if(myargv[0]!=NULL&&strcmp(myargv[0],"ls")==0)
        {
    
    
            myargv[i++]="--color=auto";
        }
      
        while(myargv[i++]=strtok(NULL," "));//循环切割,先将切割后的结果赋值给myargv,再将这个值作为判断,strtok在结束时会返回空
        
       if(myargv[0]!=NULL&&strcmp(myargv[0],"cd")==0)
        {
    
    
            if(myargv[1]!=NULL)
            {
    
    
                chdir(myargv[1]);//通过chdir系统调用,将当前的工作目录改为myargv数组下标为1的元素
                continue;//后面的语句不用再执行了,直接下一次循环
            }
        }
        
       if(myargv[0]!=NULL&&strcmp(myargv[0],"echo")==0)
        {
    
    
           if(myargv[1]!=NULL&&strcmp(myargv[1],"$?")==0)
           {
    
    
				printf("%d %d\n",lastcode,lastsig);
                continue;
           }
        }
        
        pid_t id=fork();
        assert(id!=-1);//fork失败返回-1
        if(id==0)
        {
    
    
            //子进程内部执行进程替换,我们有了数组,优先考虑使用带p的
            execvp(myargv[0],myargv);
            exit(-1);//如果程序替换失败就返回-1
    
        }
       
        //父进程要回收子进程资源
        int status=0;
        pid_t ret=waitpid(id,&status,0);//非阻塞式等待
        
        lastcode=(status>>8)&0xff;
        lastsig=status&0x7f;
        
    }
    return 0;
}

insert image description here

3. Implement redirection

The command line interpreter supports redirection, but as far as the code we have written so far does not support redirection. The essence of redirection is that the fd used by the upper layer remains unchanged, and the point of fd corresponding to struct file* is changed in the kernel. If you don't know much, you can go to see the blogger's basic IO: Basic IO

That is to say, just use dup2the system call to change the pointing of struct file* in fd. When we perfect this function, a simple command line interpreter will be completed.

Append redirection is essentially another kind of output redirection, so these two can be written together. The specific implementation is as follows:

#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<unistd.h>
#include<sys/types.h>
#include<sys/wait.h>
#include<sys/stat.h>
#include<fcntl.h>
#include<ctype.h>
#include<assert.h>
#include<errno.h>

#define NUM 1024 //定义最大输入指令大小
#define MAX 20
#define Skipspace(start) do{
      
      \
                             while(isspace(*start)) start++;\
                          } while(0)

//定义文件重定向的类型,否则后面无法区分

#define NON 0
#define APPEND 1
#define OUTPUT 2
#define INPUT 3


char LineCommand[NUM];//定义输入字符数组
char*myargv[MAX];



//设置退出结果和退出信号
int lastcode=0;
int lastsig=0;

//4-15,增加重定向功能,>输出重定向,>>追加重定向,<输入重定向
//重定向首先要分割文件名和指令,所以在标识重定向的位置要放\0

char*readfile;
int redirType=NON;


void redirect(char *commands)
{
    
    
  //从左到右开始扫描
 char * start=commands;
 char*end=commands+strlen(commands);
  while(start<end)
  {
    
    
      
    if(*start=='>')
    {
    
    
      //找到描述符,把这里换成\0
      *start='\0';
      start++;
      if(*start=='>')//说明是追加重定向,start还要向后挪动一个位置
      {
    
    
         start++;//后面可能有空格,要跳过空格
         redirType=APPEND;
      }     
      else{
    
    
        redirType=OUTPUT;
      }
      Skipspace(start);
      readfile=start;//前面定义的是以指针的方式不是没有道理的
      break;
    }
    else if(*start=='<')
    {
    
    
      //开头都是同样的处理
      *start='\0';
      ++start;

      redirType=INPUT;
      Skipspace(start);

      readfile=start;
      break;
    }
    else{
    
    
      start++;
    }
  }

}

int main()
{
    
    
  while(1)
  {
    
    

    redirType=NON;
    readfile=NULL;
    //写一个自己的shell,首先我的有提示符
      printf("用户名@服务器 当前路径:");
     fflush(stdout);

   //将用户输入的指令作为字符串存入数组中,用fgets函数获取输入的指令 我要将其切割出来
     char *s=fgets(LineCommand,sizeof(LineCommand)-1,stdin);//将stdin中输入的字符放到LineCommand中
     assert(s!=NULL);
    //清除最后一个\n
    LineCommand[strlen(LineCommand)-1]=0;

      redirect(LineCommand);
  //切割,argv存放的第一个字符串是程序
      myargv[0]=strtok(LineCommand," ");
     int i=1;
    if(myargv[0]!=NULL&&strcmp(myargv[0],"ls")==0)
    {
    
    
       myargv[i++]="--color=auto";
    }
    //
    // 在切割之前要把文件名和指令分开
    
     while(myargv[i++]=strtok(NULL," "));//循环切割

     if(myargv[0]!=NULL&&strcmp(myargv[0],"cd")==0)
     {
    
    
         //如果是cd命令,并且有输入cd到哪个路径,就将当前的工作路径改为myargv[1]
         if(myargv[1]!=NULL)
         {
    
    
           chdir(myargv[1]);

           continue;///后面的语句不用再执行了
        }
     }

#ifdef DEBUG 
     for(i=0;myargv[i];i++)
     {
    
    
       printf("myargv[%d]:%s\n",i,myargv[i]);
     }

#endif
     //创建子进程,让子进程替换
     pid_t id=fork();
     assert(id!=-1);

     if(id==0)
     {
    
    
          
       //因为指令由子进程进程替换来执行,所有重定向肯定也是交由子进程
       switch(redirType)
       {
    
    
         case NON:
           break;//不重定向
         case INPUT:
           {
    
    
             int flag=O_RDONLY;
             int fd=open(readfile,flag);
             if(fd<0)
             {
    
    
               perror("open");
               exit(errno);
             }
             dup2(fd,0);
           }
              break;
         case OUTPUT:
         case APPEND:
              {
    
    
                int flags=O_WRONLY|O_CREAT;
                if(redirType==APPEND) flags|=O_APPEND;
                else flags|=O_TRUNC;
                //先打开文件
                int fd=open(readfile,flags,0666);
                if(fd<0)
                {
    
    
                  perror("open");
                  exit(errno);
                }
               //重定向,更改标准输出
               dup2(fd,1);
              }
              break;
         default:
              //可能有错误
              printf("bug?\n");
              break;

       }


       //替换,选用带vp的来换
       execvp(myargv[0],myargv);
       exit(1);
     }

     int status=0;
     pid_t ret =  waitpid(id,&status,0);
     //父进程回收子进程,并获取退出码
    assert(ret>0);
    lastcode=(status>>8)&0xff;
    lastsig=(status)&0x7f;
     
    if(myargv[0]!=NULL&&strcmp(myargv[0],"echo")==0)
    {
    
    
      if(myargv[1]!=NULL&&strcmp(myargv[1],"$?")==0)
      {
    
    
          printf("%d %d\n",lastcode,lastsig);
          continue;
      }
    }    
      
  }
 
return 0;
}

Guess you like

Origin blog.csdn.net/m0_62633482/article/details/130310542