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

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

一、代码剖析

1. 头文件引入:

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

#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 

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

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

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

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

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

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

void find_Redirect(char* argv){
  char* start = NULL;
  for(int i = 0;i<(int)strlen(argv);i++){
    if(argv[i] == '>'||argv[i]=='<'){
      if(argv[i]=='<'){
        //cat <   log.txt
        argv[i] = '\0';
        //去除重定向后面的空格,只保留文件名部分,存放到全局变量redirFile中
        start = argv+i+1;
        trimSpace(start);
        redirFile = start;    
        redirType = INPUT_REDIR;
      }else{
        if(argv[i+1] == '>'){
          //cat xiaomi >> log.txt
          argv[i] = '\0';
          start = argv+i+2;
          trimSpace(start);
          redirFile = start;
          redirType = APPEND_REDIR;
        }else{
          argv[i] = '\0';
          start = argv+i+1;
          trimSpace(start);
          redirFile = start;
          redirType = OUTPUT_REDIR;
        }
      }
    }
  }
}

4. 主函数

  • 主函数包含一个无限循环,在每次循环中等待用户输入命令,并处理命令执行、重定向和内置命令等逻辑。
  • 在循环的开始,通过printf输出命令提示符,然后使用fgets读取用户输入的命令行,并处理去除结尾的换行符。
  • 接下来,调用find_Redirect函数来查找重定向符号,并解析重定向文件和类型。
  • 然后,使用strtok函数对输入命令进行切割,将切割后的命令参数存储到myargv数组中。同时,处理一些特殊的内置命令,如cdecho
  • 对于echo命令,根据重定向类型进行输出重定向。
  • 如果不是内置命令,则创建子进程,并在子进程中执行命令。根据重定向类型,进行输入输出重定向。
  • 最后,使用waitpid等待子进程执行完毕,并获取子进程的退出状态码和信号。
int main(){
  //一开始先初始化重定向文件和类型
  while(1){
  redirFile = NULL;
  redirType = NONE_REDIR;
    printf("[suhh@ziqiang address]$ ");
    fflush(stdout);
    //用户输入
    assert(fgets(lineComend,sizeof(lineComend)-1,stdin)!=NULL);
    //清除数组最后一个\n
    lineComend[strlen(lineComend)-1] = '\0';
   //ls -a -l 
   //切割字符串 靠' '
   find_Redirect(lineComend);
   myargv[0] = strtok(lineComend," ");
  // char * echo_str = NULL;
   if(strcmp(myargv[0],"echo")==0){
      myargv[1] = myargv[0] + strlen(myargv[0]) + 1;//取出剩余字串
      goto echo_start;
  }
   //strtok 会把剩余的放到静态变量中,下次调用只用穿null
   int i = 1;
   //myargv[end]=NULL
   if(myargv[0]!=NULL&&(strcmp(myargv[0],"ls")==0||strcmp(myargv[0],"ll")==0)){
     myargv[i++] = (char*)"--color=auto";
     if(strcmp(myargv[0],"ll")==0) {
      myargv[i++] = (char*)"-l";
      myargv[0] = (char*)"ls";
    }
     //让ls命令默认加上颜色
   }
   while(myargv[i++] = strtok(NULL," ")){}
   if(strcmp(myargv[0],"cd")==0){
     //如果是cd命令,不能用子进程执行,如果用子进程执行不会影响父进程,达不到效果
     //这时这就是内建命令
     if(myargv[1]!=NULL)chdir(myargv[1]);
     continue;
   }
echo_start:
   if(myargv[0]!=NULL && strcmp(myargv[0],"echo")==0){
     //这也是内建命令
     //判断重定向类型
     int fd = 0;
     if((redirType == OUTPUT_REDIR)||(redirType == APPEND_REDIR)){
        int flags = O_WRONLY | O_CREAT;
        if(redirType == APPEND_REDIR) flags = flags | O_APPEND;
        else flags = flags | O_TRUNC;
        fd = open(redirFile,flags,0666);
        //先把标准输出流备份一下
        dup2(1,5);
        //重定向
        dup2(fd,1);
     }
     if(myargv[1]!=NULL&&strcmp(myargv[1],"$?")==0){
       printf("%d,%d\n",lastCode,lastSig);
     }else{
       if(myargv[1]!=NULL){
         //如果用户输入 echo "hello"  去除“”
         if(myargv[1][0] =='"'){
           myargv[1] = (char*)&myargv[1][1];
         }
        int str_len =  strlen(myargv[1]);
        if((myargv[1][str_len-1] == '"')||(myargv[1][str_len -2] == '"')){
         if(myargv[1][str_len-1] == '"') str_len --;
          else str_len -= 2;
        }
         for(int i = 0 ;i=0);
   if(it == 0){
     //判断重定向
     //cat < log.txt
     if(redirType == INPUT_REDIR){
         int rfd = open(redirFile,O_RDONLY);
         if(rfd == -1){
          perror("错误:open");
          exit(errno);
         }
         dup2(rfd,0);
     }else if((redirType == OUTPUT_REDIR)||(redirType == APPEND_REDIR)){
      int flags = O_WRONLY | O_CREAT; 
      if(redirType == OUTPUT_REDIR) flags |= O_TRUNC;
      else flags |= O_APPEND;
      int wfd = open(redirFile,flags,0666);
      assert(wfd>=0);
      (void)wfd;
      dup2(wfd,1);
     }
     //子进程执行进程程序替换
     execvp(myargv[0],myargv);
     exit(1);
   }
   int status = 0;
   int wp = waitpid(it,&status,0);//阻塞等待
   assert(wp>=0);
   (void)wp;
   lastCode = (status>>8)&0xFF;
   lastSig = status&0x7F;
   printf("exit_code:%d,exit_signal:%d\n",lastCode,lastSig);
  }
  return 0;
}

 二、 涉及知识点

 1. fork()函数

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

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

Linux-Shell命令行解释器的模拟实现_第1张图片

Linux-Shell命令行解释器的模拟实现_第2张图片

2. 进程程序替换

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

 Linux-Shell命令行解释器的模拟实现_第3张图片

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

Linux-Shell命令行解释器的模拟实现_第4张图片

函数原型: 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

你可能感兴趣的:(linux,C,linux,运维,服务器)