Linux:如何实现Shell解释器?

熟悉Linux的小伙伴们都知道,shell作是用户使用系统的桥梁,那么今天我们便来看看shell是如何实现的;
shell是一种命令解释器,也是用户操作接口,Linux用户通过我们通过输入一系列的指令,被shell解释后调用需要的系统接口,从而操纵系统内核,完成期望的动作
在这里插入图片描述
所以,任何一个shell都必须要具备以下几点:

  • 抓取分析信息:能够读入用户操作的命令,并将其解析为我们需要的参数
  • 创建进程为了保证shell稳定运行,需要创建一个子进程,让用户期望的动作在子进程上完成,这样即使子进程崩溃,shell依然可以稳定运行
  • 识别并实现特殊符号:诸如cd,>,>>这些符号,是需要shell或子进程自行实现的,所以我们需要对那些自行实现的符号进行识别
  • 程序替换:shell是一个命令解释器,但它创建的子进程,是需要完成预期功能的,所以我们要使用程序替换,改变子进程的内存指针;
  • 错误提示:当用户操作非法,或者进程异常时,我们要及时的反馈或记录这些错误信息,方便用户调试;
    在这里插入图片描述

解析&实现

抓取用户输入
  • Shell工作时,用户每次输入的是一个字符串,这串字符包含了指定程序名(命令名)—附加操作–目标文件或路径:
    在这里插入图片描述
  • 我使用了常用的scanf来抓取这些字符串,但scanf—%s一旦识别到空格变回停止抓取,所以需要用到正则表达式:
    • scanf("%[^\n]%*c",buf):"%[^\n]"表示在遇到\n之前,scanf不会停止抓取字符,%*c表示抓取一个字符但不储存(扔掉),这是针对\n的,若是不这样做,scanf会到\n停止,但\n依然存在,scanf下次便会一直在这里等待,导致阻塞

参考代码:

char buff[1024] = {0};//抓来的字符串就放这里;
int getInput(){
  memset(buff,0x00,1024);//每次调用都要清空一次
  printf("[minishell@coolsuperman]$");//为了和Linux的shell长得一样,所以每次都要先打印这个界面;
  fflush(stdout);
  if(scanf("%[^\n]%*c",buff)!=1){//如果只是\n scanf阻塞
    getchar();
    return -1;
  }
  return 0;
}
  • 在这里我们需要考虑一个特殊情况,当用户什么都没做只输入了回车(\n)时,scanf会一直无法通过%[^\n],也就无法执行%*c的操作,\n将一直存在,所以我们要在此种情况下,手动使用getchar()扔掉\n;同时再次为情况下,用户不进行任何操作,我们需要返回一个异常值,方便后面处理;
分析字符串
  • 在得到字符串后,我们需要将这些指定程序,路径,附加操作符都分别存储,(程序替换要用)在这之中,还隐藏了诸多如空格,制表符等等的"空白符号",这些符号是是我们所不需要但真实存在的,所以也得识别并剔除出去;
  • 指定程序,路径等参数也是通过”空白字符“来间隔的,我们通过这里下手,使用ctype.h里的isspace()来识别它们,将有效字符后的空白字符置为‘\0’这样便将它们分成了一个个更短的有结尾的字符串
  • 因为我已经将他们分成了很多个字符串,所以我使用字符串指针数组argv[]来存储他们,并记录这些字符串的个数argc(也是替换时的重要参数)

参考代码:

int argc = 0;
char *argv[32];
int  AnalysisData(){
  argc = 0;//每次清空初始化
  char* go = buff;
  while(*go!='\0'){
    if(!isspace(*go)){
      argv[argc] = go;
      argc++;
      while((*go!='\0')&&(!isspace(*go))){
        go++;
      }
      *go = '\0';
    }
      go++;
  }
  argv[argc]=NULL;//告诉程序替换函数参数已经完了,详见程序替换函数原型需求
  return 0;
}

创建进程执行操作
  • 当上述两个动作都正常完成时,我们已经完成了准备工作,下面就要开始创建子进程,并进行程序替换了;
  • 在创建之前,我们需要对一些需要shell自行调用接口的命令如"cd",进行判断,在这里,我只实现了cd,但基本形式便是如此,如果要实现更多,可以封装一个函数进行处理;
  • 创建子进程时,如果创建失败,我们需要返回一下错误让用户知道原因方便调试,如果创建正常,我们便进入子进程操作;
  • 在子进程上,也有一些特殊命令">,>>,…"是系统命令里没有的,我们需要自行实现,之后便可以使用之前储存好的参数进行程序替换了,(如果这里你不太清楚,可以看我的另一篇博文:Linux:程序替换)
  • 如果替换失败,别忘了友好的返回错误信息,这事也要关闭掉这个没用的子进程(exit(-1))当成功替换时,要运行的程序已经变化了,也就不会运行到这里啦!?
  • 子进程的事情忙完了,但shell作为父进程,一定要进行进程等待,否则子进程退出时shell在忙其他的事不太关注,就会产生僵尸进程
int main(){
  while(1){
    if(getInput()<0)
      continue;
    AnalysisData();
    if(!strcmp(argv[0],"cd")){
      chdir(argv[1]);
      continue;
    }
    int pid = fork();
    if(pid<0){
      perror("fork error");
      return -1;
    }else if(pid==0){//返回0代表子进程
      int i = 0;
      for(;i<argc;i++){
        if(!strcmp(argv[i],">")){
          int fd = open(argv[i+1],O_WRONLY|O_CREAT|O_TRUNC);//只写,没有创新,清空再写入
          dup2(fd,1);
          argv[i] = NULL;
        }else if(!strcmp(argv[i],">>")){
          int fd = open(argv[i+1],O_WRONLY|O_CREAT|O_APPEND);//只写,没有创新,在末尾续;
          dup2(fd,1);
          argv[i] = NULL;
        }
      }
      execvp(argv[0],argv);//在PATH下找命令(argv[0]);
      perror("execvp error");
      exit(-1);

    }
    wait(NULL); //阻塞式等待,防止僵尸进程;
    }
  return 0;
}
需要用到的库
  • 这里是我当前环境使用这些自带函数需要用到的库,但我发现有的情况下,不同系统,配置,版本,要用的库文件不尽相同,如果在Linux下你引用库文件出现了问题,不妨去问问那个男人吧!(man+你要问的函数名)
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<sys/wait.h>
#include<unistd.h>
#include<fcntl.h>
#include<errno.h>
#include<ctype.h>

效果展示:

在这里插入图片描述

  • 我的完整代码放在github上,有需要的小伙伴可以
    点击下载

yo~
yo~
如果觉得不错~
请别忘了点个赞~
Bro你的鼓励~
给我坚持的勇气~
Peace out~

  • 4
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值