当前位置: 首页 > article >正文

[Linux] Linux 模拟实现 Shell

标题:[Linux] Linux 模拟实现 Shell

个人主页@水墨不写bug(图片来源于网络)

目录

一、什么是shell

二、shell的理解

三、模拟实现shell

1)打印命令行提示

2)获取用户的输入字符串

3)分割命令字符串

4)检查是否是内建命令

1. 什么是内建命令

常见的内建命令

5)进程程序替换


 正文开始:

一、什么是shell

         Shell是用C语言编写的一个独立的可执行程序,它是用户与Linux或其他类Unix系统(如Mac OS)交流的桥梁,既是命令语言又是程序设计语言。

        定义:Shell是Linux内核的一个外层保护工具,并负责完成用户与内核之间的交互。

        功能:Shell是一个命令行解释器,它将用户输入的命令解析为操作系统所能理解的指令,实现用户与操作系统的交互。同时,Shell也提供了一种脚本编写功能,允许用户将一系列命令按照特定顺序排列,形成脚本文件,由Shell解释器逐行执行,以完成特定任务或实现一系列操作。

         Shell提供了丰富的内置命令和外部命令,如cd用于切换目录,ls用于列出目录内容,grep用于搜索文本等。这些命令可以组合使用,形成复杂的脚本,以实现各种功能。

 

二、shell的理解

         站在shell的角度,一个可执行程序,运行起来成为进程,它读取的一段指令其实一串字符串,操作系统无法理解这串字符串的具体意义,无法与用户交流,操作系统也就无法为用户服务。shell是外壳程序,就像一个包在操作系统外面的一层壳子,shell可以把用户输入的一串字符“翻译”

 给操作系统,把字符串转化为操作系统可以“听懂的”语言,这样操作系统就能听懂用户的需求,可以进一步为用户服务了。

        所以,根据这一理解,我们可以先设计一个框架,表明我们实现的shell的大致思路:

        1)打印命令行提示

        2)获取用户的输入字符串

        3)分割命令字符串,并存储到全局的字符串数组:myg_argv

        4)检查是否是内建命令,内建命令需要父进程自己执行

        5)进程程序替换

       

如果不知道什么是进程程序替换,可以浏览这一篇:《[Linux]进程程序替换》

        这也就表明了shell的运行机制,:

        假如shell读取到“ls”,shell从用户读入字符串"ls"。shell建立一个新的进程,然后在那个进程中运行ls程序并等待那个进程结束。

三、模拟实现shell

         由于我们当前实现的shell比较简单,目前没有分离编译最后链接,简单点说就是没有把声明和实现分离为源文件和头文件。而是简单的把一个一个的步骤封装为一个一个的函数。

1)打印命令行提示

         我们在与Linux的bash(shell的一种)命令行进行交互的时候,会发现每一次输入命令的前面,都会有一个命令行提示:

这个命令行提示包含的信息有:

用户名+主机名+当前目录 

        幸运的是,这些信息我们可以直接从环境变量中获取。我们可以通过<stdlib.h>中的getenv()接口获得环境变量,获取了这些环境变量之后就好办了,只需要用printf(标准格式化输出)即可实现打印命令行提示。具体实现如下:


const char *get_username()
{
    const char *name = getenv("USER");
    if (name == NULL)
    {
        return "NONE";
    }
    return name;
}

const char *get_hostname()
{
    const char *hostname = getenv("HOSTNAME");
    if(hostname == NULL)
    {
        return "NONE";
    }
    return hostname;
}

const char *get_cwd()
{
    const char *cwd = getenv("PWD");
    if (cwd == NULL)
    {
        return "NONE";
    }
    return cwd;
}
void output_sng_line()
{
    // 直接打印输出到stdout
    const char *cwd = get_cwd();

   
    if (strlen(cwd) != 1)//特殊处理根目录“/”的情况
    {
        printf("%s@%s:~%s$ ", get_username(), get_hostname(), cwd);
    }
    else
    {
        printf("%s@%s:%s$ ", get_username(), get_hostname(), cwd);
    }

    fflush(stdout); // 刷新缓冲区
}

总结:

                通过库函数getenv获得环境变量,printf格式化输出即可;

                输出的格式可以对照你的本地的命令行提示符。

        上面的实例是按照我的阿里云服务器的命令行提示符来设计的,这是我的命令行提示符:

(包括普通目录和 特殊处理的根目录)

       

        注意:获取的环境变量PATH是一串完整的路径,与上图的路径不同,这就需要我们对路径进行截取,由于通过函数进行修改需要传递二级指针,比较麻烦,所以我们可以考虑使用宏替换函数实现:


// 这样写do{}while(0):形成代码块;while(0)后面可随便带分号
#define Re_back(p)          \
    do                      \
    {                       \
        p += strlen(p) - 1; \
        while (*p != '/')   \
            --p;            \
    } while (0)

修改后的函数具体实现:


void output_sng_line()
{
    // 直接打印输出到stdout
    const char *cwd = get_cwd();

    // 这个宏后面可以随便加分号,看起来更像一个函数
    Re_back(cwd);

    if (strlen(cwd) != 1)
    {
        printf("%s@%s:~%s$ ", get_username(), get_hostname(), cwd);
    }
    else
    {
        printf("%s@%s:%s$ ", get_username(), get_hostname(), cwd);
    }

    fflush(stdout); // 刷新缓冲区
}


 

2)获取用户的输入字符串

         我们需要再设计一个缓冲区,用来存储读取的命令行信息——usercommand,SIZE的大小可以自己设计,可以大一些,这样允许一次输入比较长的指令。

        返回值表示获取命令是否成功,如果失败返回-1——如果失败,就没有运行的意义了,所以退出即可。

main函数接口调用:

获取用户的输入字符串 函数具体实现:

     一个小细节,在读取的时候,由于我们最后输入指令的时候,会附带一个换行符,表示输入命令结束,这个换行符也会被读入,于是需要将缓冲区的 '\0'的位置提前一个位置。


int get_user_command(char line[], int n)
{
    char *s = fgets(line, n, stdin);
    if (s == NULL)
    {
        printf("err\n");
        return -1;
    }
    // 处理字符串结尾的\n
    line[strlen(line) - 1] = ZERO;
    return 0;
    // printf("%s\n",line);
}


 

3)分割命令字符串

main函数接口调用:

         分割命令字符串具体实现:(复用库函数strtok(),字符串分割函数)


void partation_command(char *command, int n)
{
    (void)n; // 暂时禁用n,防止编译老是提醒

    myg_argv[0] = strtok(command, SKP);

    int index = 1;
    // 有意的设计 “=” :先赋值再判断;当srtok无法分割时,返回NULL,正好让g_argc最后一个元素为NULL
    while ((myg_argv[index++] = strtok(NULL, SKP)))
    {
    }
}

 


4)检查是否是内建命令

1. 什么是内建命令

        在Linux操作系统中,内建命令(builtin commands)是指那些直接由shell(如Bash、Zsh等)自身实现和提供的命令,而不是作为独立的可执行文件存在于文件系统中的命令。内建命令通常比外部命令(external commands)执行得更快,因为它们不需要启动一个新的进程来执行。 

常见的内建命令

以下是一些常见的Bash内建命令:

  • cd:更改当前工作目录。
  • echo:在标准输出上显示一行文本。
  • exec:用指定的命令替换当前的shell进程。

         等等....

        其实,内建命令之所以必须是内建命令,一定有一定的原因:

        以cd为例子,执行cd,如果父进程创建一个子进程让他帮自己执行,那么子进程的目录确实是切换了,但是父进程的目录还是没有变化啊!所以cd之所以必须是内建命令,是因为必须让父进程自身执行cd命令。让父进程自身切换工作路径,需要系统调用接口:

chdir() 

main函数接口调用:

        返回值表示:

        1——是内建命令,父进程需要自己完成命令,则不需要执行第五步操作;

        其他——不是内建命令,父进程可以通过创建子进程来让子进程替自己完成命令。 

 检查进程是否是内建命令其实是很简单朴素的,就是一个一个的条件判断,具体函数实现如下:

(这里也给出了CD内建命令的实现方式)


void _CD()
{
    char *newpath = myg_argv[1];
    // 说明只有一个cd,默认切换到家目录
    if (newpath == NULL)
    {
        newpath = (char *)get_home();
    }

    chdir(newpath);

    // 这个时候,环境变量没有被改变,使用cd的时候,虽然可以切换路径,但是getpath打印出来的不变
    // path环境变量需要shell自己维护
    char tem[SIZE];
    getcwd(tem, sizeof(tem));
    snprintf(cwd, sizeof(cwd), "PWD=%s", tem);
    putenv(cwd);
}

int check_inbuildcommand_execute()
// 内建命令需要父进程执行,如果子进程执行cd,则子进程目录切换,父进程没切换
{
    int yes = 0;
    // myg_argv[0]一定是命令
    char *inb_cmd = myg_argv[0];

    // 检查是否是cd命令,
    if (strcmp(inb_cmd, "cd") == 0)
    {
        yes = 1;
        _CD();
    }
    else if (strcmp(inb_cmd, "echo") == 0 && strcmp(myg_argv[1], "$?") == 0)
    {
        yes = 1;
        printf("%d\n", lastcode);
    }
    return yes;
}

 


 

5)进程程序替换

 main函数接口调用:

         父进程创建一个子进程帮自己干活,父进程只需要坐在一旁等着就行了:


void execute_command()
{
    pid_t id = fork();
    if (id == 0) // 子进程给父进程做事
    {
        int ret = execvp(myg_argv[0], myg_argv);
        if (ret == -1)
        {
            exit(errno);
        }
    }
    else // 父进程
    {
        int status = 0;
        pid_t rid = waitpid(id, &status, 0); // 等待子进程退出

        if (rid > 0) // 等待子进程成功
        {
            if (WIFEXITED(status))
            {
                lastcode = WEXITSTATUS(status);
            }
            else
            {
                printf("lastcode 不重要了,子进程被信号杀掉\n");
            }
        }
        else
        {
            printf("waitpid fail");
            exit(-1); // 父进程shell退出
        }
    }
}

        最后不要忘了回收子进程的退出信息。 


完~

未经作者同意禁止转载


http://www.kler.cn/news/353518.html

相关文章:

  • 案例分析:Modbus设备如何通过MQTT网关连接阿里云IoT
  • 《数据结构》学习系列——树(中)
  • MySQL 的数据类型
  • [权威出版|稳定检索]2024年信息控制、电气与智慧交通国际会议(ICEIT 2024)
  • 【深入学习Redis丨第八篇】详解Redis数据持久化机制
  • 2025推荐选题|微信小程序实现经济新闻资讯
  • 双目标定的原理
  • bash之变量和参数
  • BWA 的基本介绍和主要算法
  • 【工欲善其事】巧用 PowerShell 自动清除复制 PDF 文本时夹杂的换行符号
  • 强推!首个全面涵盖LiDAR里程计算法的综述(一):LiDAR-Only与LIO算法综述
  • 一力破万法,高并发系统优化通解思路
  • Unity MMORPG 背包系统如何设计
  • 【Matlab算法】MATLAB下的简单四旋翼飞行器姿态控制仿真(附MATLAB完整代码)
  • Java项目:154 基于ssm旅游信息网站(含论文+ppt)
  • DNS:互联网域名系统的核心
  • 二维dp,LeetCode 887. 鸡蛋掉落
  • OJ-1014田忌赛马
  • Jenkins构建Springboot项目显示Lombok依赖不起作用
  • 什么是DDoS攻击?怎么防御DDoS攻击?