LInux下C语言模拟实现 —— 极简版的命令行解释器

发布于:2024-04-23 ⋅ 阅读:(63) ⋅ 点赞:(0)

根据对进程的理解,我们知道然后去使用系统接口去调用程序和加载程序,因此我们可以利用接口去实现一个简易版的命令行解释器,核心思路就是获取用户输入的指令信息,然后利用指令信息去调用相关的接口,因此首先就是要如何获取用户输入的字符串

获取字符串 —— fgets

函数声明 : char * fgets (char *str,int size,FILE * stream);

头文件:#include<stdio.h>

说明:该函数用来从stream流中获取size大小的数据字符,并将其存到str中,若是读取文件流失败则会返回空指针,成功则返回str的指针。

利用该函数我们可以从缓存区中用户输入的字符串

如:

测试好没有问题后,我们开始需要将字符串进行一定的处理,去得到能让exec类接口认识的形式,因此我们需要去对字符串进行切割,根据方便,我们最终比较容易得到的是指令的名称和使用方式的数组形式,因此我们选择execvp

处理字符串 —— strtok

函数声明:char * strtok(char *str,const char *delim);

头文件:#include <string.h>

函数说明:该函数用来对字符串进行切割,第一个参数是被切割的字符串(注意,该函数会改变原字符串),第二个参数是以什么字符或字符串为切割点,返回切割后的字符串指针,若是想多次切割则下次第一个参数传NULL,多次调用该函数,函数会自动在上次切割的位置,继续往后找切割位置切,并且返回,直到找到最后一段找不到切割符时,则返回剩下的最后一段,再次调用返回NULL

我们封装一个函数,定义一个用来存放指令和指令参数的字符串数组(字符指针数组),还有需要被切割的字符串,将其交给这个函数,完成切割并将切割好的字符放到字符串数组中,若是成功则返回,失败了则返回0,失败的情况我们可以认为是输入了无效的字符指令,此时不做任何处理,直接continue即可

以上,对输入指令这一步就处理好了,接着就是创建子进程去让子进程调用系统接口,而父进程只需要等待进程结束然后回收即可

创建子进程 —— fork

函数声明:pid_t fork(void);

头文件:#include<unistd.h>

函数说明:该函数用于创建子进程,调用函数后对父进程返回子进程的pid,子进程返回0,创建失败则返回-1

进程等待 —— waitpid

函数声明:pid_t waitpid(pid_t pid, int *status, int options);

头文件:#include<sys/wait.h>

函数说明:第一个参数可以用来指定子进程的pid,第二个参数则是用来返回子进程的结束状态的,第三个参数用来选择是阻塞等待还是非阻塞轮询的方式去等待,具体可以参考上一篇博客《进程控制》,里面有较为详细的介绍到进程创建、进程结束、进程等待和进程替换。

进程替换 —— execvp

函数声明:int execvp(const char *file, char *const argv[ ])

头文件:#include<unistd.h>

函数说明:第一次参数输入指令的名称,第二个输入如何使用该指令的方法数组

以上关于进程创建、进程等待和进程替换具体可以看上一篇博客介绍,这里简单复述一下

接下来我们只需要创建子进程,然后让子进程去调用execvp即可

测试一下是否能够执行一些基本的指令,例如“ls -a -l”等等,基本功能通过测试后,再来对一些细节进行完善

ls的自动配色方案 "--color=auto"

首先是关于ls,我们发现我们自己写的命令行解释器中的ls指令没有配色,这是因为我们没有设置自动的配色方案,我们可以对ls指令单独进行一下处理,当检查到ls指令时,我们对切割好后的字符串数组后加上自动配色方案“--color=auto”

内置命令(内建命令)

基本的功能完成后,我会发现有部分指令不能成功的实现想要的效果,例如cd命令,和对环境变量进行各种操作的命令等等,这些命令不能通过子进程去执行,而是需要在父进程中直接调用系统接口去操作的

改变当前工作路径 —— chdir

函数声明:int chdir(const char *path);

头文件:#include<unistd.h>

函数说明:该函数用于改变当前工作路径,第一个参数传参为具体的路径

利用chdir函数,我们在父进程部分直接对cd命令进行处理,当检测到cd命令时,我们再检查其路径是否为空,若不为空,则将路径交给函数执行即可,不管是否成功,都continue

接下来还有关于对环境变量进行操作的指令,export

我们使用export的目的是对当前进程的环境变量进行操作,而不是对子进程,因此同样需要对这个函数进行操作,实际上,与环境变量相关的操作都不能让子进程去操作,而是直接在父进程进行处理

添加环境变量 —— putenv

函数声明:int putenv(char* string);

头文件:#include<stdlib.h>

函数说明:该函数用于添加环境变量,但需要注意,使用该函数传过去的指针位置,需要靠自己去进行维护,函数内部记录的事你传参过去的地址位置,该位置记录的环境变量若是后续被修改,则之前传入的环境变量也会被同样修改,所以需要自己去维护传入的变量

因此,我们可以先定义一个二维字符数组和对应下标去维护(c语言就是麻烦),然后再使用这个函数去实现添加环境变量即可

此时,我们直接调用env函数去测试是否成功时,我们看到的是这个进程通过子进程调用的env,此时看到的环境变量是通过这个进程继承下去的,我们同样可以看到结果是否正确,但是我们实际想要看到的是当前进程的环境变量,因此我们对env指令也进行处理,让它打印出父进程的环境变量表

获取当前进程的环境变量表 —— extern char **environ;

这是个声明,想获得当前的环境变量表,需要上面的声明即可,environ这个字符串数组(二级字符指针)内存的就是当前环境变量表

我们只需要打印一条条打印出来即可

接着,我们再最后完善一个指令,echo指令,echo指令作为输出指令,大部分情况可以交给子进程去做,但是涉及到环境变量的话,我们需要echo指令打印的是当前进程的环境变量,因此需要特殊处理,还有"echo $?"得到的是最近一次进程结束的退出码,这个也需要我们特殊处理

获取指定环境变量 —— getenv

函数声明:char *getenv(const char *name);

头文件:#include<stdlib.h>

函数说明:该函数用于获取指定名称的环境变量,给函数传参环境变量的名称,会返回该环境变量的内容

通过这个函数,我们可以去判断echo $... 若是识别到$,则我们可以判断是否是要找环境变量,当然也可能是$?,所以分类判断,先解决环境变量的问题

然后就是解决最近一次进程结束的退出码问题了,这里最近一次的进程,就是上一次子进程调用指令后结束的退出码,我们可以通过一个值去存下来,至于如何获取这个退出码,进程等待waitpid中有个输出型参数status,其中配套使用的宏WEXITSTATUS可以解析出退出码

WIFEXITED(status) 和 WEXITSTATUS(status)

WIFEXITED(status):检查进程是否正常退出

WEXITSTATUS(status):获取子进程的退出码

注意:这里的status是waitpid函数中第二个参数获取的status值

综上,基本的一个简单版的命令行解释器模拟实现就到这,当前有很多不完善的地方,目前这个简单版的命令行解释器,核心目的是为了在实现的过程中,更加深刻的理解进程创建、进程结束、进程等待和进程替换这四个步骤,在这个过程中熟悉和掌握各种函数接口的使用和理解,最后附加上完成的源码以供参考。

源码参考

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

#define MAX 1024
#define ARGC 64

int split(char* com_str,char* argv[])
{
  assert(com_str);
  assert(argv);
  char* tmp = NULL;
  tmp = strtok(com_str," ");
  int i = 0;
  if(tmp == NULL) return -1;
  while(tmp != NULL)
  {
    argv[i++] = tmp;
    tmp =  strtok(NULL," ");
  }
  return 0;
}

void test_print(char* s[])
{
  char* tmp = s[0];
  int i = 1;
  while(tmp)
  {
    printf("%s\n",tmp);
    tmp = s[i++];
  }
}

int main()
{
  extern char** environ;
  char myenv[64][256];
  int env_i = 0;
  int exit_id = 0;//子进程的退出码
  while(1)
  {

    char com_str[MAX] = {'0'};
    char* argv[ARGC] = {NULL};
    printf("输入你的指令:");
    fflush(stdout);
    char* s = fgets(com_str,sizeof(com_str),stdin);
    assert(s);
    (void)s;
    com_str[strlen(com_str)-1] = '\0';//去掉末尾的换行
    int n = split(com_str, argv);
    if(n == -1) continue;
    //关于指令接受的工作做好以后,接下来就是创建子进程让子进程去调用程序,出错后返回等等
    
    //对ls的细节处理
    if(strcmp(argv[0],"ls") == 0)
    {
      int pos = 0;
      while(argv[pos]) pos++;
      argv[pos] = (char*)"--color=auto";
    }
    //内置命令的处理
    //cd /path
    if(strcmp(argv[0],"cd") == 0)
    {
      if(argv[1] != NULL) chdir(argv[1]);
      continue;
    }
    // export myint=5
    if(strcmp(argv[0],"export")==0)
    {
      if(argv[1] != NULL)
      {
        strcpy(myenv[env_i],argv[1]);
        putenv(myenv[env_i++]);
      }
      continue;
    }
    //env
    if(strcmp(argv[0],"env") == 0)
    {
      int i;
      for(i = 0; environ[i];i++)
      {
        printf("%d : %s \n",i+1,environ[i]);
      }
      continue;
    }
    //处理echo 环境变量或者获取退出码的问题
    if(strcmp(argv[0],"echo") == 0)
    {
      if(argv[1][0] == '$')
      {
        if(argv[1][1] == '?') // echo $?
        {
          printf("%d\n",exit_id);
        }
        else//echo $环境变量名称
        {
          if(getenv(argv[1]+1))
            printf("%s\n",getenv(argv[1]+1));
        }
        continue;
      }
    }

    pid_t id = fork();
    assert(id>=0);
    (void)id;

    if(id == 0)//子进程
    {
      execvp(argv[0],argv);
      exit(1);//若是替换成功,则不应该继续执行下去,因此如果失败,我们设置退出码为1
    }
    //父进程
    int status = 0;
    waitpid(id,&status,0);
    if(WIFEXITED(status))
    {
      exit_id = WEXITSTATUS(status);
    }
  }
    return 0;
}

总结

本篇博客循序渐进的实现了一个简单的简易版Linux命令行解释器,每一步都有知识点的介绍和简单的分析说明,并且最后附上源码提供参考,模拟实现的目的是为了更好的理解上一节课学习的进程相关的四个步骤,熟悉和掌握其中的接口,更加深刻的理解进程控制的概念


网站公告

今日签到

点亮在社区的每一天
去签到