如何写一个命令行解释器(SHELL)

news2024/9/29 7:19:55

文章目录

  • 前言
  • 什么是命令行解释器 ——SHELL
  • SHELL的结构
    • void print_info(char ** env) //打印命令行信息函数
    • void read_comand(char **buffer) //读取指令函数
    • char **split_line(char *buffer, int *flag) //分割字符串函数
    • int excute_line(char **buffer, int flag) // 执行指令的函数
  • 总结

前言

本shell设计的框架参考的是Tutorial - Write a Shell in C,但是代码都是自己写的,还加了一些新的功能😆

什么是命令行解释器 ——SHELL

关于什么是shell前面的文章深入理解 shell/bash已经说过了,我这里就不多赘述了。

SHELL用到的知识点: 进程替换、字符串操作库函数、文件系统、进程等待、动态增容

SHELL的结构

关于一个shell我们首先他是一个循环,重复的接受我们输入的命令行,直到收到退出指令,每一个循环我们都要执行以下任务:

  1. 获取终端的信息并打印在屏幕上,包括主机名称、用户名称、当前目录
  2. 读取屏幕上用户输入的命令行
  3. 将读取的命令行,按照可执行指令参数的形式切割成若干个字符串
  4. 执行指令
  5. 接受指令执行成果,并判断循环是否继续

这个结构是贯穿整个shell执行周期的根本,所以我们先将主函数写出来,然后再实现每一步的具体功能:

int main(int argc, char **argv, char **envp)
{
    while (1)
    {
        char *buffer = NULL; // 存储 输入的命令行

        print_info(envp); // 输出命令行前面的主机信息

        read_comand(&buffer); // 读取 屏幕上输入的指令

        int flag = 0;

        char **p = split_line(buffer, &flag); // 将字符串裂项成 字符串数组

        int ret = excute_line(p, flag); // 执行 指令

        if (ret == EXIT_SHELL) // 接受返回值并判断是否要退出SHELL
            break;
    }
}

void print_info(char ** env) //打印命令行信息函数

这个函数主要是获取主机信息并打印在屏幕上,获取内容包括主机名称、用户名称、当前目录。
我的实现方法是从环境变量中获取,环境变量指针数组是通过main函数的参数列表传入(如果想看指针数组内容,命令行键入env即可查看),主体思想就是遍历指针数组找到我们想要的信息,以我的终端为例,我们要找的环境变量为:
在这里插入图片描述
所以我们要寻找的环境变量名称为:HOSTNAMEUSERPWD,找到后截取等号后面的内容存储下来,思路简单,但是对string库函数使用有一定熟练度要求,代码实现:

void print_info(char **envp)
{
    char **cur = envp;
    char HOST_NAME[100]; // 主机型号和名称
    char USER[100];      // 用户名
    char PWD[100];      //路径
    char CUR_dic[20];   //当前目录
    while ((*cur) != NULL)    //env最后一个元素为NULL
    {
        char tmp[30];   //用来存储截取每个用户变量名
        int j = 0;
        for (j = 0; j < strlen(*cur); j++)   // 等号前面的一定是变量名
        {
            if ((*cur)[j] == '=')
                break;
            else
                tmp[j] = (*cur)[j];
        }
        tmp[j] = '\0';

        if (strcmp(tmp, "HOSTNAME") == 0)
            strcpy(HOST_NAME, strstr(*cur, "=") + 1);

        if (strcmp(tmp, "USER") == 0)
            strcpy(USER, strstr(*cur, "=") + 1);

        if (strcmp(tmp, "PWD") == 0)   //pwd作为路径还需要特殊处理,才能得到当前目录
        {
            strcpy(PWD, strstr(*cur, "=") + 1);
            int i = 0;
            for (i = strlen(PWD); i >= 0; i--)
            {
                if (PWD[i] == '/')
                    break;
            }
            strcpy(CUR_dic, PWD + i + 1);
        }
        cur++;
    }
    if (strcmp(USER, "root") == 0)   //区分 超级用户 和 普通用户
        printf("[%s@%s %s]# ", USER, HOST_NAME, CUR_dic);
    else
        printf("[%s@%s %s]$ ", USER, HOST_NAME, CUR_dic);
}

最后还有一个小细节是:超级用户和普通用户最后一个字符有点区别,要加以区分

void read_comand(char **buffer) //读取指令函数

这个函数目的是读取用户输入的命令行,采取动态增容的模式,buffer为

读取屏幕上的字符我们是一个一个的读直到读取到EOF或者换行符为止

void read_comand(char **buffer)
{
    // 动态开辟内存
    char *p = (char *)malloc(BUFFER_SIZE * sizeof(char));
    int buffer_size = BUFFER_SIZE;

    if (p == NULL) // 开辟内存失败
    {
        fprintf(stderr, "read_line error\n");
        exit(0);
    }
    int i = 0;
    while (1)
    {
        char c = getchar();

        if (c == EOF || c == '\n')
        {
            p[i] = '\0';
            break;
        }
        else
        {
            p[i] = c;
            i++;
        }
        if (i >= buffer_size) // 如果空间不够 , 追加初始一倍 的空间
        {
            buffer_size += BUFFER_SIZE;
            p = realloc(p, buffer_size);
        }
    }
    *buffer = p;
}

char **split_line(char *buffer, int *flag) //分割字符串函数

切割字符串分三种情况: p为我们要返回的指针数组

  1. 单指令
    切割字符串这个函数就有点学问了,例如我们在命令行输入ls -l -a,经过这个函数就要变成一个指针数组:
    在这里插入图片描述

这个数组元素的类型是指向字符串的指针,数组结束的标志是出现NULL指针。那么如何将buffer数组切成一小块一小块的命令行参数呢?
这就要用到strtok函数来帮我们来干一些脏活和累活,用strtok按照空格来切分buffer,再将切出来的指针存储到指针数组中,最后strtok没得切的时候还会贴心的返回NULL来作为指针数组的结尾(strtok不会用的参考字符库函数总结)

  1. 管道
    管道实际上是两个指令用字符|隔开,所以我们只要对buffer稍加处理就可以变成对两个单指令切割
    在这里插入图片描述
    定义一个指针找到管道标识符|,将该位置的字符改成\0然后移一位,此时buffer和p2分别指向指令1和指令2的起始位置,这时就转换成两个单字符切割。对buffer先进行切割存入p指针数组,然后用NULL作为两个字符串切割结果的分割,然后再对p2数组切割存入p指针数组
    例如:如果我们键入ls -al | grep a.out ,最后的p指针数组应该为;
    在这里插入图片描述

  2. 重定向
    和管道一个道理,只是重定向第二个是文件名而不是指令所以不用切割,例如:我们输入指令ls -al > log.txt
    理论上,最后的p指针数组应该为:
    char ** p={"ls","-al",NULL,"log.txt"};
    在这里插入图片描述

同时我们在函数列表中传入了一个输出型参数用来标定该指令是普通指令还是重定向指令或管道指令,这对于我们下面的函数至关重要,不同类型的指令对应不同的执行策略
代码:

char **split_line(char *buffer, int *flag) // 将buffer分割成若干个指令数组 1 重定向 -1 管道  0 啥都不是
{
{
    char **p = (char **)malloc(6 * sizeof(char **));
    int size = 6;

    if (strstr(buffer, ">")) // 重定向
    {
        // 将buffer按照重定向符切成两个字符串
        *flag = 1;
        char *p2 = strstr(buffer, ">"); // 重定向符 后面的字符串
        *p2 = '\0';
        p2 += 1;

        int i = 0;
        p[i++] = strtok(buffer, " ");
        while ((p[i] = strtok(NULL, " ")) != NULL)
        {
            i++;
            if (i >= size)
            {
                p = append_space(p, i);
            }
        }

        i++;           // 两个 切成块的 字符串 用 NULL来分割
        if (i >= size) // 检查是否要扩容
        {
            p = append_space(p, i);
        }

        p[i++] = strtok(p2, " ");
        while ((p[i] = strtok(NULL, " ")) != NULL)
        {
            i++;
            if (i >= size) // 检查是否要扩容
            {
                p = append_space(p, i);
            }
        }
        return p;
    }
    else if (strstr(buffer, "|"))
    {
        // 将buffer按照重定向符切成两个字符串
        *flag = -1;
        char *p2 = strstr(buffer, "|"); // 重定向符 后面的字符串
        *p2 = '\0';
        p2 += 1;

        int i = 0;
        p[i++] = strtok(buffer, " ");
        while ((p[i] = strtok(NULL, " ")) != NULL)
        {
            i++;
            if (i >= size) // 检查是否要扩容
            {
                p = append_space(p, i);
            }
        }

        i++;           // 两个 切成块的 字符串 用 NULL来分割
        if (i >= size) // 检查是否要扩容
        {
            p = append_space(p, i);
        }

        p[i++] = strtok(p2, " ");
        while ((p[i] = strtok(NULL, " ")) != NULL)
        {
            i++;
            if (i >= size) // 检查是否要扩容
            {
                p = append_space(p, i);
            }
        }
        return p;
    }
    else // 没有管道 也没有重定向
    {
        *flag = 0;
        int i = 0;
        p[i++] = strtok(buffer, " ");
        while ((p[i] = strtok(NULL, " ")) != NULL)
        {
            i++;
            if (i >= size) // 检查是否要扩容
            {
                p = append_space(p, i);
            }
        }
        return p;
    }
}

int excute_line(char **buffer, int flag) // 执行指令的函数

由于切割时分成了三类,所以执行的时候也要分成三类😭

  1. 普通指令
    普通指令执行流程图大概是:
    在这里插入图片描述
    一个进程替换就完事了
    但是我们容易忽视的是内置指令(builtin),内置指令不需要创建子进程使用进程替换,直接在main进程内执行,举一个简答的例子:cd .. ,我们如果用子进程去执行,执行完之后子进程所在目录改变了,但是父进程(main)所在的目录并不会改变。对于这些内置指令,功能用函数来定义,并用函数指针数组来表示整体,在执行普通指令之前判断

  2. 重定向:
    重定向的思路是:
    在这里插入图片描述
    比普通指令多一步文件描述符替换,也就是将原本要打印到屏幕上的信息打印到文件里面,如果文件不存在就建立一个,很简单

  3. 管道:
    管道的思路是:
    在这里插入图片描述
    管道就比较复杂了,首先要执行指令1,并进行文件描述符替换使其原本要打印到屏幕上的东西输入到管道中,然后再执行指令2,因为指令默认是从屏幕上读取信息,所以我们还要将指令2的标准输入和管道的读端文件描述符替换,最后检查替换完成任务,相对比较复杂

代码

char *builtin_str[] = {"cd", "help", "exit"}; // 内置指令的名称

int func_cd(const char *args)
{
    int ret = chdir(args);
    if (ret == -1)
    {
        printf("cd is error\n");
        return -1;
    }
    else
        return 0;
}

int func_help(const char *args)
{
    printf("这是 石海涛 的shell\n 我的shell支持重定向、管道、简单内置命令\n更多内容访问我的博客\n ");
    return 0;
}

int func_exit(const char *args)
{
    return EXIT_SHELL;
}

int (*func[])(const char *p) = {&func_cd, &func_help, &func_exit}; // 函数指针数组 数组中的顺序必须和builtin_str中一致

int excute_command(char **args) // 执行单个指令
{
    for (int i = 0; i < sizeof(builtin_str) / sizeof(char *); i++) // 检查是否是内置命令
    {
        if (strcmp(args[0], builtin_str[i]) == 0)
        {
            return func[i](args[1]);
        }
    }
    // 不是内置命令就要开始进程替换了
    __pid_t i = fork();
    if (i == 0)
    {
        execvp(args[0], args);
    }

    int status = 0;
    waitpid(i, &status, 0);

    return status >> 8;
}

int excute_line(char **buffer, int flag) // 执行指令的函数
{

    if (flag == 0)
    {
        int ret = excute_command(buffer);
        return ret;
    }
    else if (flag == 1) // 重定向
    {

        char **file_name = buffer;
        while (*file_name != NULL)
        {
            file_name++;
        }

        file_name++; // 找到文件名

        pid_t i = fork();
        if (i == 0)
        {
            int fd = open(*file_name, O_CREAT | O_RDWR, 0644);
            dup2(fd, 1);
            execvp(buffer[0], buffer);
        }
        int status;
        waitpid(i, &status, 0);
        return status >> 8;
    }
    else // 管道
    {

        char **command2 = buffer;
        while (*command2 != NULL)
        {
            command2++;
        }

        command2++; // 找到指令2的起始位置
        // printf("%s\n", *command2);

        pid_t i = fork();
        if (i == 0)
        {
            int fd[2];
            if (pipe(fd) == -1)
                fprintf(stderr, "create pipe fail\n");
            pid_t son = fork();
            if (son == 0)
            {
                close(fd[0]);
                dup2(fd[1], 1);
                // printf("1\n");
                execvp(buffer[0], buffer);
            }
            // 父进程

            waitpid(son, NULL, 0);
            close(fd[1]);
            dup2(fd[0], 0);
            // printf("2\n");
            execvp(command2[0], command2);
        }
        int status;
        waitpid(i, &status, 0);
        return status >> 8;
    }
}

总结

到此整个shell就差不多,语言层面对C语言字符串操作函数、读写文件函数要求比较高。完整代码放在gitee上了仓库

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/186382.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Redis 安全汇总小结

Redis redis 是一个C语言编写的 key-value 存储系统&#xff0c;可基于内存亦可持久化的日志型、Key-Value数据库&#xff0c;并提供多种语言的API。它通常被称为数据结构服务器&#xff0c;因为值&#xff08;value&#xff09;可以是 字符串(String), 哈希(Hash), 列表(list…

电子技术——基本MOS放大器配置

电子技术——基本MOS放大器配置 上一节我们探究了一种MOS管的放大器实现&#xff0c;其实MOS放大器还有许多变种配置&#xff0c;在本节我们学习最基本的三大MOS放大器配置&#xff0c;分别是共栅极&#xff08;CG&#xff09;、共漏极&#xff08;CD&#xff09;、共源极&…

【MSSQL】分析数据库日志文件无法收缩的问题

一、问题描述 在SQL Server 2008R2数据库中&#xff0c;无法对数据库日志进行收缩&#xff0c;导致日志不断膨胀。 二、问题分析 由于是日志文件不断增大且无法收缩&#xff0c;所以初步判断为存在未提交的事务。检查可能阻止日志阶段的活动事务&#xff0c;执行&#xff1a…

使用 JMX 连接远程服务进行监测

使用 JMX 连接远程服务进行监测1.JVM参数2.启动脚本3.演示使用相关JMX工具连接部署在服务器上的Java应用&#xff0c;可以对应用的内存使用量&#xff0c;CPU占用率和线程等信息进行监测。相关监测工具有jconsole&#xff0c;jprofiler&#xff0c;jvisualvm等。1.JVM参数 监测…

本地镜像发布到阿里云

1、找到阿里云控制台中的容器镜像服务&#xff0c;进入个人版 2、先创建命名空间&#xff0c;再创建镜像仓库 记住创建时设置的密码&#xff0c;选择创建本地的镜像仓库 建完之后&#xff0c;选择管理 进入后的界面如下 内容如下&#xff1a; 1. 登录阿里云Docker Registry $…

547、RocketMQ详细入门教程系列 -【消息队列之 RocketMQ(一)】 2023.01.30

目录一、RocketMQ 特点二、基本概念2.1 生产者2.2 消费者2.3 消息服务器2.4 名称服务器三、参考链接一、RocketMQ 特点 RocketMQ 是阿里巴巴在2012年开源的分布式消息中间件&#xff0c;目前已经捐赠给 Apache 软件基金会&#xff0c;并于2017年9月25日成为 Apache 的顶级项目…

【自然语言处理】【大模型】PaLM:基于Pathways的大语言模型

PaLM&#xff1a;基于Pathways的大语言模型《PaLM: Scaling Language Modeling with Pathways》论文地址&#xff1a;https://arxiv.org/pdf/2204.02311.pdf 相关博客 【自然语言处理】【大模型】PaLM&#xff1a;基于Pathways的大语言模型 【自然语言处理】【chatGPT系列】大语…

电脑重装系统后找不到硬盘怎么办

有网友的win10系统电脑出了系统故障进行了重装&#xff0c;但是又发现了重装系统后找不到硬盘的新问题&#xff0c;那么重装系统后找不到硬盘怎么办呢? 工具/原料&#xff1a; 系统版本&#xff1a;win10专业版 品牌型号&#xff1a;戴尔成就5880 方法/步骤&#xff1a; …

使用FFmpeg工具进行推流、拉流、截图、变速、转换,及常见问题处理

下载安装 FFmpeg下载官网&#xff1a;FFmpeg &#xff0c;这里提供了官网下载的windows环境 4.1.3版本&#xff1a;https://download.csdn.net/download/qq_43474959/12311422 下载后&#xff0c;配置环境变量&#xff0c;将bin文件地址加入到path中&#xff1a; 测试 在cmd…

数据结构 | 图结构 | 最小生成树 | Kruskal Prim算法讲解

文章目录前言Kruskal算法Prim算法前言 讲解之前&#xff0c;我们需要先明白连通图是指什么&#xff1f;连通图具有以一个顶点为起点可以到达该图中的任意一个顶点的特性&#xff0c;就算它们不直接相连&#xff0c;但是它们之间至少有一条可以递达的路径。并且连通图是针对无向…

Mysql 中的日期时间函数汇总

日期和时间函数MySQL中内置了大量的日期和时间函数&#xff0c;能够灵活、方便地处理日期和时间数据&#xff0c;本节就简单介绍一下MySQL中内置的日期和时间函数。1 CURDATE()函数CURDATE()函数用于返回当前日期&#xff0c;只包含年、月、日部分&#xff0c;格式为YYYY-MM-D…

【Unity3D小工具】Unity3D中实现仿真时钟、表盘、仿原神时钟

推荐阅读 CSDN主页GitHub开源地址Unity3D插件分享简书地址我的个人博客 大家好&#xff0c;我是佛系工程师☆恬静的小魔龙☆&#xff0c;不定时更新Unity开发技巧&#xff0c;觉得有用记得一键三连哦。 一、前言 今天实现一个时钟工具&#xff0c;其实在之前已经完成了一个简…

P3366 【模板】最小生成树

题目描述 如题&#xff0c;给出一个无向图&#xff0c;求出最小生成树&#xff0c;如果该图不连通&#xff0c;则输出 orz。 输入格式 第一行包含两个整数 &#xfffd;,&#xfffd;N,M&#xff0c;表示该图共有 &#xfffd;N 个结点和 &#xfffd;M 条无向边。 接下来 &…

【K8S系列】Pod重启策略及重启可能原因

目录 1 重启策略 1.1 Always 1.2 OnFailure 1.3 Nerver 1.4 yaml示例 2 Pod常见异常状态 2.1 Pending状态 2.2 Waiting/ContainerCreating状态 2.3 CrashLoopBackOff状态 2.4 ImagePullBackOff状态 2.5 Error状态 2.6 其他状态说明 tips: 3.自动重启的可能原…

【HBase——陌陌海量存储案例】5. Apache Phoenix快速入门

6.3 快速入门 6.3.1 需求 本次的小DEMO&#xff0c;我们沿用之前的订单数据集。我们将使用Phoenix来创建表&#xff0c;并进行数据增删改查操作。 列名说明id订单IDstatus订单状态money支付金额pay_way支付方式IDuser_id用户IDoperation_time操作时间category商品分类 6.3.…

Ruoyi-Cloud框架学习-【06 新增业务项目】

新建模块 结构查看 新增子模块&#xff0c;按照若依赖的约定&#xff0c;避免后续出现问题&#xff0c;当然一通百通&#xff0c;也可以按照自己的思路配置&#xff0c;修改对应的配置即可。 后端项目结构如下&#xff0c;通过观察&#xff0c;我们需要新增自己的模块在ruoy…

git reset

reset三种模式区别和使用场景区别&#xff1a;--hard&#xff1a;重置位置的同时&#xff0c;直接将 working Tree工作目录、 index 暂存区及 repository 都重置成目标Reset节点的內容,所以效果看起来等同于清空暂存区和工作区。--soft&#xff1a;重置位置的同时&#xff0c;保…

Java后端项目排错经验分享

导致错误的原因有很多&#xff0c;最常见的无非也就那么几种&#xff1a; 1、粗心导致的格式问题以及代码多写少写错写字母的问题 2、代码逻辑问题 3、框架版本不匹配问题 无论是哪种问题&#xff0c;排查错误的方式最好最便捷的方式有调试日志&#xff0c;那么如果是线上问…

【头歌】循环队列及链队列的基本操作

第1关&#xff1a;循环队列的基本操作任务描述本关任务是实现循环队列的基本操作函数&#xff0c;以实现判断队列是否为满、是否为空、求队列元素个数、进队和出队等功能。相关知识队列的基本概念队列&#xff08;简称队&#xff09;也是一种运算受限的线性表&#xff0c;在这种…

Kubernetes集群搭建 (未完待续)

Kubernetes集群搭建 目录 前言前期准备K8S集群安装 虚拟机设置安装K8S集群k8s部署Nginx 附录1 Docker安装附录2 yum k8s 问题附录3 k8s start问题附录4 k8s master init 前言 本文指定Docker与K8s版本&#xff0c;保证兼容性&#xff0c;可供参考 Docker‐ce‐3:19.03.9‐3…