【Linux】模拟实现shell命令行解释器

发布时间:2023年12月18日

需要云服务器等云产品来学习Linux的同学可以移步/–>腾讯云<–/官网,轻量型云服务器低至112元/年,优惠多多。(联系我有折扣哦)

1. 主要思路

一个Shell是一直在运行着的,为了保证Shell本身的运行稳定,所以每次在接收到指令的时候,会派生子进程,把子进程替换成要执行的指令,执行完毕之后子进程被回收,然后再次回到等待指令的时候。

主要步骤:

  • 输出命令提示符
  • 从终端获取命令行输入
  • 解析命令行输入信息;
  • 创建子进程;
  • 进程程序替换;
  • 进程等待;

2. 流程图

通过上述的分析,我们可以画出以下的流程图:

image-20231208174726993

3. 实现过程

3.1 初步实现

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

#define NUM 1024//输入缓冲区大小
#define OPT_NUM 64//命令参数最大个数
char lineCommand[NUM];//输入缓冲区
char* argv[OPT_NUM];
int main()
{
    while(1)//死循环,因为Shell要一只运行着
    {
        //打印输出命令提示符
        printf("[用户名@主机名 当前路径]$ ");
        fflush(stdout);//由于打印命令提示符的时候没有换行,所以这里手动刷新缓冲区
        //获取输入
        char* str = fgets(lineCommand, sizeof(lineCommand) - 1, stdin);//最后一个位置用于在极端情况下保证字符串内有'\0'
        assert(str);//判断合法性
        (void)str;
        lineCommand[strlen(lineCommand) - 1] = '\0';//消除行命令中的换行

        //命令解析(字符串切割)
        argv[0] = strtok(lineCommand, " ");
        int i = 1;
        while(argv[i++] = strtok(NULL, " "));//使用字符串切割函数依次拿到每个参数

        //创建子进程
        pid_t id = fork();
        if(id == -1)
        {
            perror("fork");
            exit(errno);
        }
        else if(id == 0)
        {
            //child
            //进程程序替换
            execvp(argv[0], argv);
            //执行到此处的时候,证明进程替换错误
            perror("exec:");
            exit(errno);
        }
        else
        {
            //parent
            //进程等待
            int status = 0;//退出状态
            pid_t ret = waitpid(id, &status, 0);//阻塞等待
            if(ret == -1)
            {
                perror("wait fail");
                exit(errno);
            }
        }
    }
    return 0;
}

image-20231208234529296

可以看到,这里我们自己的Shell已经能够执行Linux的基本指令啦,但是我们的ls好像是没有颜色的,那么这里我们在Shell里面加上特判

if(argv[0] != NULL && strcmp(argv[0], "ls") == 0)  //ls颜色显示
{
    argv[i++] = (char*)"--color=auto";
}

image-20231208235302681

image-20231208235206197

3.2 当前路径

我们知道,在进程运行起来之后,在/proc目录下会有一个内存级的目录存放进程的相关信息,我们可以在这里找到我们运行的进程。

image-20231218003355065

可以看到,这里有两个路径。

  • exe:代表可执行文件在磁盘中的路径
  • cwd:current work directory,当前进程的工作路径,即我们常说的当前路径

当前路径可以更改吗?

可以,当前路径可以通过系统调用chdir更改

image-20231218004724666

path:想要更改的路径

return value:调用成功返回0,失败返回-1

image-20231218010027170

可以看到,cwd已经被更改。


在我们之前实现的Shell中,如果想要使用cd命令更改当前路径,似乎是做不到的

image-20231218010431478

这是因为,按照我们之前的思路,每次都是派生子进程来执行命令,那么子进程的工作目录会切换到我们指定的路径,但是子进程执行完毕之后就被回收了,我们在父进程看不见路径的切换。

所以有了上述chdir的前置知识,那么就很好解决这个问题了:在命令解析之后,如果发现遇到了cd命令,就不要派生子进程,直接使用父进程执行系统调用chdir

image-20231218011359336

image-20231218011520668

3.3 内建命令/外部命令

Linux下的命令分为两种:内建命令外部命令

  • 内建命令是 shell 程序的一部分,其功能实现在 bash 源代码中,不需要派生子进程来执行,也不需要借助外部程序文件来运行,而是由 shell 进程本身内部的逻辑来完成;
  • 外部命令则是通过创建子进程,然后进行进程程序替换,运行外部程序文件等方式来完成。

我们可以使用type命令来区分内建命令还是外部命令

image-20231218011808977

注:博主这里使用的是汉化过的云服务器,所以结果是中文,翻译上有一点差别

我们上面对cd命令的处理就是内建命令的一种,myshell 遇到 cd 命令时,由自己直接来改变进程工作目录,处理完毕直接 continue,而不会创建子进程。

同时,我们发现 echo 命令也是一个内置命令,这其实也很好的解释了 为什么 “echo $变量” 可以查看本地变量以及为什么 “echo $?” 可以获取最近一个进程的退出码 了:

虽然本地变量只在当前进程有效,但是使用 echo 查看本地变量时,shell 并不会创建子进程,而是直接在当前进程中查找,自然可以找到本地变量;

shell 可以通过进程等待的方式获取上一个子进程的退出状态,然后将其保存在 ? 变量中,当命令行输入 “echo $?” 时,直接输出 ? 变量中的内容,然后将 ? 置为0 (echo 正常退出的退出码),也不需要创建子进程。

那么,我们可以为我们的Shell添加echo命令了。

image-20231218012623823

image-20231218012526544

3.4 Shell 的最终实现

最后,经过一系列的优化,我们最终得到了一个简易的Shell的demo,这里附上源码:

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

#define NUM 1024//输入缓冲区大小
#define OPT_NUM 64//命令参数最大个数
char lineCommand[NUM];//输入缓冲区
char* argv[OPT_NUM];
int EXIT_CODE;
int main()
{
    while(1)//死循环,因为Shell要一只运行着
    {
        //打印输出命令提示符
        printf("[用户名@主机名 当前路径]$ ");
        fflush(stdout);//由于打印命令提示符的时候没有换行,所以这里手动刷新缓冲区
        //获取输入
        char* str = fgets(lineCommand, sizeof(lineCommand) - 1, stdin);//最后一个位置用于在极端情况下保证字符串内有'\0'
        assert(str);//判断合法性
        (void)str;
        lineCommand[strlen(lineCommand) - 1] = '\0';//消除行命令中的换行

        //命令解析(字符串切割)
        argv[0] = strtok(lineCommand, " ");
        int i = 1;

        if(argv[0] != NULL && strcmp(argv[0], "ls") == 0)//识别ls,自动加上颜色选项
        {
            argv[i++] = (char*)"--color=auto";
        }

        while(argv[i++] = strtok(NULL, " "));//使用字符串切割函数依次拿到每个参数

        if(argv[0] != NULL && strcmp(argv[0], "cd") == 0)
        {
            if(argv[1] != NULL)
            {
                chdir(argv[1]);
            }
            else
            {
                printf("no such file or directory\n");
            }
            continue;
        }
        if(argv[0] != NULL && strcmp(argv[0], "echo") == 0)
        {
            if(strcmp(argv[1], "$?") == 0)
            {
                printf("%d\n", EXIT_CODE);
                EXIT_CODE = 0;
            }
            else
            {
                printf("%s\n", argv[1]);
            }
            continue;
        }

        //创建子进程
        pid_t id = fork();
        if(id == -1)
        {
            perror("fork");
            exit(errno);
        }
        else if(id == 0)
        {
            //child
            //进程程序替换
            execvp(argv[0], argv);
            //执行到此处的时候,证明进程替换错误
            perror("exec:");
            exit(errno);
        }
        else
        {
            //parent
            //进程等待
            int status = 0;//退出状态
            pid_t ret = waitpid(id, &status, 0);//阻塞等待
            EXIT_CODE = (status >> 8) & 0xFF;
            if(ret == -1)
            {
                perror("wait fail");
                exit(errno);
            }
        }
    }
    return 0;
}

本节完

文章来源:https://blog.csdn.net/weixin_63249832/article/details/135053342
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。