Linux实现简易版Shell的代码详解

2025-05-21 15:50

本文主要是介绍Linux实现简易版Shell的代码详解,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

《Linux实现简易版Shell的代码详解》本篇文章,我们将一起踏上一段有趣的旅程,仿照CentOS–Bash的工作流程,实现一个功能虽然简单,但足以让你深刻理解Shell工作原理的迷你Sh...

一、程序流程分析

我们日常使用Bash时,通过输入命令执行相应的操作,比如:

Linux实现简易版Shell的代码详解

那么,Bash是如何进行工作的呢?观察一下,就会发现,首先Bash会打印命令行提示符,包括当前用户、主机名以及路径。之后会等待我们输入相关命令,然后根据命令执行相应程序。程序执行结束后,就会再次打印命令行提示符,等待我们再次输入指令…很明显是一个死循环。

总结一下,Bash的大体工作流程是:

1.打印命令行提示符(包括当前用户名、主机名、路径)
2.获取用户输入的命令行
3.解析命令行
4.执行命令
5.继续打印命令行提示符…

注意:当Bash执行非内建命令时,会创建一个子进程,由子进程完成相应的工作,Bash自己等待子进程工作结束。而对于内建命令(如cd,echo),需要Bash自己执行任务。

Linux实现简易版Shell的代码详解

二、代码实现

接下来,我们开始按照上述工作流程,一步步实现我们的简易Shell。

1. 打印命令行提示符

Centos的命令行提示符主要包含三个内容:当前用户名、主机名和当前所在路径。 之前学习linux环境变量时,我们了解到环境变量(USER、HOSTNAME、PWD)中存储着这些内容。所以我们使用getenv函数获取环境变量相应的值。

代码实现:

#include <IOStream>
#include <cstdio>
#include <cstring>
#include <cstdlib>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>

//获取当前用户名
const char* GetUserName()
{
    const char* name = getenv("USER");
    return name == nullptr ? "none" : name;
}

//获取当前主机名
const char* GetHostName()
{
    const char* name = getenv("HOSTNAME");
    return name == nullptr ? "none" : name;
}

//获取当前工作路径
const char* GetPwd()
{
    const char* pwd = getenv("PWD");
    return pwd == nullptr ? "none" : pwd;
}

接下来,调用这些函数,形成一串命令行提示符并打印:

//创建并打印命令行提示符
void PrintCommandPrompt()
{
    char CommandPrompt[1024];
    //这里为了区分Bash,用大括号
    snprintf(CommandPrompt, sizeof CommandPrompt,"{%s@%s %s}@ " , GetUserName(), GetHostName(), GetPwd());
    std::cout << CommandPrompt << std::flush; // 打印并刷新缓冲区
}

注意这里打印结束后,由于没有换行,所以缓冲区可能不会刷新,导致命令行提示符没有出现在屏幕上。因此这里我们需要主动刷新缓冲区。

进行测试:

int main()
{
    PrintCommandPrompt();
    return 0;
}

运行结果:

Linux实现简易版Shell的代码详解

可以看到,用户名和主机名都正常打印,但在我们写的shell中,当前所在路径是绝对路径,太过冗长,所以可以对生成的路径进行一些处理:

//创建并打印命令行提示符
void PrintCommandPrompt()
{
    char CommandPrompt[1024];
    
    //处理当前工作路径
    std::string pwd = GetPwd();
    if(pwd != "/") // 如果是根目录,则直接输出
        pwd = pwd.substr(pwd.rfind('/') + 1); // 查找最后一个"/",并从下一个位置开始分割

    //这里为了区分Bash,用大括号
    snprintf(CommandPrompt, sizeof CommandPrompt,"{%s@%s %s}@ " , GetUserName(), GetHostName(), pwd.c_str());
    std::cout << CommandPrompt << std::flush; // 打印并刷新缓冲区
}

运行结果:

Linux实现简易版Shell的代码详解

2. 获取用户输入的命令行

在Bash输入命令时,往往会带上一些选项,并用空格隔开。因此,使用scanf或cin读入时会以空格作为分隔符,达不到想要的效果。这里我们选择用fgets进行读取。

另外,主函数当中,命令的全部处理应该放在一个死循环当中,这样才能完成用户多次派发的任务。

//获取用户输入的命令行
bool GetCommandLine(char* command, int size)
{
    //从键盘读取命令
    androidif(fgets(command, size, stdin) == nullptr)
    {
        return false;
    }

    //注意清理末尾的'\n'
    if(strlen(command) == 0) return false;
    command[strlen(command) - 1] = '\0';
    return true;
}

int main()
{
    while(true)
    {
        //打印命令行提示符
        PrintCommandPrompt();

        //读入命令
        char command[1024];
        if(!GetCommandLine(command, sizeof command)) // 若读取错误,就continue重新读取
        {
            std::cout << "读取错误" << std::endl;
            continue;
        }

        //打印输入的命令行
        std::cout << command << std::endl;
    }
    return 0;
}

注意:使用fgets从键盘读取字符串时会附带末尾’\n’,需要进行处理。

运行测试:

Linux实现简易版Shell的代码详解

可以看到,程序成功读入了我们的命令(包括空格),并且将命令回显出来。

3. 命令行解析

命令行解析的过程中,需要将用户读入的命令行进行分割,提取出要执行的程序名以及选项。这里我们创建两个全局变量g_argc和g_argv,分别存储解析到的命令行参数以及参数个数,方便后续指令的执行。

注:这里的命令行分割操作由strtok函数完成。

代码实现:

//全局变量存储命令行参数及其个数
int g_argc = 0;
char* g_argv[128];

//命令行解析
bool CommandParse(char* command)
{
    g_argc = 0;
    for(char* p = strtok(command, " "); p != nullptr; p = strtok(nullptr, " "))
    {
        g_argv[g_argc++] = p;
    }
    return g_argc == 0 ? false : true;
}

int main()
{
    while(true)
    {
        //打印命令行提示符
        PrintCommandPrompt();

        //读入命令
        char command[1024];
        if(!GetCommandLine(command, sizeof command)) // 若读取错误,就continue重新读取
        {
            std::cout << "输入错误" << std::endl;
            continue;
        }
        // //打印输入的命令行
        // std::cout << command << std::endl;

        //命令行解析
        if(!CommandParse(command))
        {
            std::cout << "命令行解析失败" << std::endl;
            continue;
        }
        //打印解析结果
        for(int i = 0; i < g_argc; i++)
        {
            std::cout << g_argv[i] << std::endl;
        }
    }
    return 0;
}

测试结果:

Linux实现简易版Shell的代码详解

程序成功地按照空格将我们输入的命令行参数提取了出来。接下来,根据提取到的参数,就可以执行相关指令了。

4. 执行命令

对于非内建命令,Bash会创建子进程,并让子进程执行;而对于内建命令,则是由Bash自己执行。因此,执行命令之前,需要先判断该命令是否是内建命令,然后进行相应的操作。

为什么会有内建命令?

  • 效率: 执行内建命令通常比执行外部命令更快,因为避免了创建新进程的开销。
  • Shell 功能: 许多内建命令直接操作 Shell 的内部状态,例如改变当前工作目录 (cd)、设置环境变量 (export)、控制 Shell 行为等,这些功能如果作为外部命令实现会更加复杂或不可能。
  • 基本操作: 一些非常基础和常用的操作需要作为内建命令提供,以确保 Shell 的基本功能可用

内建命令的处理

在Bash当中,可以使用type命令判断一个命令是否是内建命令,例如:

Linux实现简易版Shell的代码详解

当然,除了cd和echo命令,还有printf、help等内建命令。本次实现中,为了能够让大家深刻理解shell运行原理,同时降低实现难度,博主就只针对cd和echo这两个内建命令进行简易实现。

内建命令的检查和处理:

//内建命令处理
bool CheckAndExecBuiltin()
{
    //取出命令行参数表的首元素,判断是否为内建命令,如果是,则直接执行
    std::string str = g_argv[0];
    if(str == "cd")
    {
        Cd();
        return true;
    }
    else if(str == "echo")
    {
        Echo();
        return true;
    }
    //else if(...)
    return false; // 不是内建命令,直接返回
}

int main()
{
    while(true)
    {
        //打印命令行提示符
        PrintCommandPrompt();

        //读入命令
        char command[1024];
        if(!GetCommandLine(command, sizeof command)) // 若读取错误,就continue重新读取
        {
            std::cout << "输入错误" << std::endl;
            continue;
        }
        // //打印输入的命令行
        // std::cout << command << std::endl;

        //命令行解析
        if(!CommandParse(command))
        {
            std::cout << "命令行解析失败" << std::endl;
            continue;
        }
        // //打印解析结果
        // for(int i = 0; i < g_argc; i++)
        // {
        //     std::cout << g_argv[i] << std::endl;
        // }

        //内建命令的处理
        if(CheckAndExecBuiltin())
        {
            continue; // 是内建命令,执行完毕就回去重新打印提示符
        }

        //不是内建命令,由子进程处理
    }
    return 0;
}

cd的简易实现

cd的功能是改变当前工作路径。如果创建子进程,其只能修改它自己的工作路径,而无法修改Bash的工作路径。因此cd操作需要Bash亲自完成。 我们获取到命令行参数后,可以通过调用chdir函数实现:

void Cd()
{
    std::string dst;
    if(g_argc == 1 || g_argv[1] == std::string("~")) // 处理进入家目录的情况
    {
        dst = GetHome();
        if(dst == "") return;
    }
    else
    {
        dst = g_argv[1];
    }
    chdir(dst.c_str());
}

测试结果:

Linux实现简易版Shell的代码详解

可以看到,使用cd后,当前路径貌似并没有发生改变。为什么呢?实际上chdir确实起到了效果,但是我们的命令行提示符中,当前工作路径是从环境变量中获取的,环境变量中的PWD并没有发生改变。 因此,修改当前工作路径之后,要顺带着修改环境变量PWD的值。其次,当前工作路径改变后,修改环境变量之前,要获取到当前工作路径,就需要使用getcwd函数。进行键值处理后,使用putenv修改环境变量。

另外要注意:许多putenv函数的实现(特别是 POSIX 标准的实现)不会复制我们传递给它的字符串,而会直接使用传入的指针来表示新的环境变量,因此要创建一个全局变量存储PWD键值对,确保其不会被销毁。

Linux实现简易版Shell的代码详解

代码实现:

//全局的当前工作路径
char cwd[256];

void Cd()
{
    std::string dst;
    if(g_argc == 1 || g_argv[1] == "~") // 处理进入家目录的情况
    {
        dst = GetHome();
        if(dst == "") return;
    }
    else
    {
        dst = g_argv[1];
    }
    chdir(dst.c_str());

    //同时修改环境变量中的当前工作路径
    char tmp[128];
    if(getcwd(tmp, sizeof tmp))
    {
        snprintf(cwd, sizeof cwd, "PWD=%s", tmp); // 键值处理
        putenv(cwd);
    }
}

测试结果:

Linux实现简易版Shell的代码详解

echo的简易实现

echo不仅可以向屏幕打印字符串,还可以打印上一个执行的程序的退出码、以及环境变量等信息。为了确保效率以及执行的可靠性,该命令也是一个内建命令,由Bash亲自执行。

//全局变量记录上一个程序的退出码
int exit_code;

void Echo()
{
    std::string op = g_argv[1];
    if(op == "$?") // 打印上一个程序的退出码
    {
        std::cout << exit_code China编程<< std::endl;
        exit_code = 0;
    }
    else if(op[0] == '$') // 打印环境变量的值
    {
        //从"$"后的第一个字符开始,获取环境变量名
        std::string name = op.substr(1);

        //用getenv获取环境变量值
        const char* value = getenv(name.c_str());
        if(value)
            std::cout << value << std::endl;
    }
    else // 打印字符串
    {
        std::cout << op << std::endl;
    }
}

测试结果:

Linux实现简易版Shell的代码详解

非内建命令的处理

针对非内建命令,需要创建子进程,然后由子进程找到系统的对应指令程序的位置,直接进行进程程序替换,完成相关任务。这样我们就无需一个个实现相关指令操作了。

为了让子进程自动查找对应程序的位置,减少我们的工作www.chinasem.cn量,且使程序替换的参数与我们创建的全局命令行参数表一一对应,这里我们直接选用execvp进行程序替换。

#include <unistd.h>
  
int execvp(const char *file, char *const argv[])

代码实现:

//执行非内建命令
void Execute()
{
    //创建子进程
    pid_t id = fork();
    if(id < 0)
    {
        //创建子进程失败,直接退出
        exit(1);
    }
    else if(id == 0)
    {
        //子进程 -- 通过程序替换执行指令
        execvp(g_argv[0], g_argv);
    }
    else
    {
        //父进程 -- 等待子进程,并获取子进程的退出信息
        int status = 0;
        pid_t rid = waitpid(id, &status, 0);
        if(rid > 0) // 等待成功
        {
            //将退出信息存入全局exit_code
            exit_code = WEXITSTATUS(status);
        }
    }
}

int main()
{
    while(true)
    {
        //打印命令行提示符
        PrintCommandPrompt();

        //读入命令
        char command[1024];
        if(!GetCommandLine(command, sizeof command)) // 若读取错误,就continue重新读取
        {
            std::cout << "输入错误" << std::endl;
            continue;
        }
        // //打印输入的命令行
        // std::cout << command << std::endl;

        //命令行解析
        if(!CommandParse(command))
        {
            std::cout << "命令行解析失败" << std::endl;
            continue;
        }
        // //打印解析结果
        // for(int i = 0; i < g_argc; i++)
        // {
        //     std::cout << g_argv[i] << std::endl;
        // }

        //内建命令的处理
        if(CheckAndExecBuiltin())
        {
            continue; // 是内建命令,执行完毕就回去重新打印提示符
        }

        //不是内建命令,由子进程处理
        Execute();
    }
    return 0;
}

三、测试

接下来,我们输入一些指令进行测试,看看我们实现的shell能否达到预期效果:

Linux实现简易版Shell的代码详解

perfect!

四、程序全部代码

自实现shell的全部代码如下:

#include <iostream>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>

//全局变量存储命令行参数及其个数
int g_argc = 0;
char* g_argv[128];

//全局的当前工作路径
char cwd[256];

//全局变量记录上一个程序的退出码
int exit_code;

//获取当前用户名
const char* GetUserName()
{
    const char* name = getenv("USER");
    return name == nullptr ? "none" : name;
}

//获取当前主机名
const char* GetHostName()
{
    const char* name = getenv("HOSTNAME");
    return name == nullptr ? "none" : name;
}

//获取当前工作路径
const char* GetPwd()
{
    const char* pwd = getenv("PWD");
    return pwd == nullptr ? "none" : pwd;
}

//获取用户的家目录
const char* GetHome()
{
    const char* home = getenv("HOME");
    return home == nullptr ? "" : home;
}

//创建并打印命令行提示符
void PrintCommandPrompt()
{
    char CommandPrompt[1024];
    
    //处理当前工作路径
    std::string pwd = GetPwd();
    if(pwd != "/") // 如果是根目录,则直接输出
        pwd = pwd.substr(pwd.rfind('/') + 1); // 查找最后一个"/",并从下一个位置开始分割

    //这里为了区分Bash,用大括号
    snpwww.chinasem.cnrintf(CommandPrompt, sizeof CommandPrompt,"{%s@%s %s}@ " , GetUserName(), GetHostName(), pwd.c_str());
    std::cout << CommandPrompt << std::flush; // 打印并刷新缓冲区
}

//获取用户输入的命令行
bool GetCommandLine(char* command, int size)
{
    //从键盘读取命令
    if(fgets(command, size, stdin) == nullptr)
    {
        return false;
    }

    //注意清理末尾的'\n'
    if(strlen(command) == 0) return false;
    command[strlen(command) - 1] = '\0';
    return true;
}

//命令行解析
bool CommandParse(char* command)
{
    g_argc = 0;
    for(char* p = strtok(command, " "); p != nullptr; p = strtok(nullptr, " "))
    {
        g_argv[g_argc++] = p;
    }
    return g_argc == 0 ? false : true;
}

void Cd()
{
    std::string dst;
    if(g_argc == 1 || g_argv[1] == std::string("~")) // 处理进入家目录的情况
    {
        dst = GetHome();
        if(dst == "") return;
    }
    else
    {
        dst = g_argv[1];
    }
    chdir(dst.c_str());

    //同时修改环境变量中的当前工作路径
    char tmp[128];
    if(getcwd(tmp, sizeof tmp))
    {
        snprintf(cwd, sizeof cwd, "PWD=%s", tmp);
        putenv(cwd);
    }
}

void Echo()
{
    std::string op = g_argv[1];
    if(op == "$?") // 打印上一个程序的退出码
    {
        std::cout << exit_code << std::endl;
        exit_code = 0;
    }
    else if(op[0] == '$') // 打印环境变量的值
    {
        //从"$"后的第一个字符开始,获取环境变量名
        std::string name = op.substr(1);

        //用getenv获取环境变量值
        const char* value = getenv(name.c_str());
        if(value)
            std::cout << value << std::endl;
    }
    else // 打印字符串
    {
        std::cout << op << std::endl;
    }
}

//内建命令处理
bool CheckAndExecBuiltin()
{
    //取出命令行参数表的首元素,判断是否为内建命令,如果是,则直接执行
    std::string str = g_argv[0];
    if(str == "cd")
    {
        Cd();
        return true;
    }
    else if(str == "echo")
    {
        Echo();
        return true;
    }
    //else if(...)
    return false; // 不是内建命令,直接返回
}

//执行非内建命令
void Execute()
{
    //创建子进程
    pid_t id = fork();
    if(id < 0)
    {
        //创建子进程失败,直接退出
        exit(1);
    }
    else if(id == 0)
    {
        //子进程 -- 通过程序替换执行指令
        execvp(g_argv[0], g_argv);
    }
    else
    {
        //父进程 -- 等待子进程,并获取子进程的退出信息
        int status = 0;
        pid_t rid = waitpid(id, &status, 0);
        if(rid > 0) // 等待成功
        {
            //将退出信息存入全局exit_code
            exit_code = WEXITSTATUS(status);
        }
    }
}

int main()
{
    while(true)
    {
        //打印命令行提示符
        PrintCommandPrompt();

        //读入命令
        char command[1024];
  python      if(!GetCommandLine(command, sizeof command)) // 若读取错误,就continue重新读取
        {
            std::cout << "输入错误" << std::endl;
            continue;
        }
        // //打印输入的命令行
        // std::cout << command << std::endl;

        //命令行解析
        if(!CommandParse(command))
        {
            std::cout << "命令行解析失败" << std::endl;
            continue;
        }
        // //打印解析结果
        // for(int i = 0; i < g_argc; i++)
        // {
        //     std::cout << g_argv[i] << std::endl;
        // }

        //内建命令的处理
        if(CheckAndExecBuiltin())
        {
            continue; // 是内建命令,执行完毕就回去重新打印提示符
        }

        //不是内建命令,由子进程处理
        Execute();
    }
    return 0;
}

总结

本篇文章,我们基于之前学习的Linux进程相关概念以及进程控制接口,实现了一个简易版的shell。本次实现让我们对Shell的运行原理有了更深刻的理解。

以上就是Linux实现简易版Shell的代码详解的详细内容,更多关于Linux简易版Shell的资料请关注编程China编程(www.chinasem.cn)其它相关文章!

这篇关于Linux实现简易版Shell的代码详解的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/1154707

相关文章

Redis实现高效内存管理的示例代码

《Redis实现高效内存管理的示例代码》Redis内存管理是其核心功能之一,为了高效地利用内存,Redis采用了多种技术和策略,如优化的数据结构、内存分配策略、内存回收、数据压缩等,下面就来详细的介绍... 目录1. 内存分配策略jemalloc 的使用2. 数据压缩和编码ziplist示例代码3. 优化的

基于C#实现PDF转图片的详细教程

《基于C#实现PDF转图片的详细教程》在数字化办公场景中,PDF文件的可视化处理需求日益增长,本文将围绕Spire.PDFfor.NET这一工具,详解如何通过C#将PDF转换为JPG、PNG等主流图片... 目录引言一、组件部署二、快速入门:PDF 转图片的核心 C# 代码三、分辨率设置 - 清晰度的决定因

Java Kafka消费者实现过程

《JavaKafka消费者实现过程》Kafka消费者通过KafkaConsumer类实现,核心机制包括偏移量管理、消费者组协调、批量拉取消息及多线程处理,手动提交offset确保数据可靠性,自动提交... 目录基础KafkaConsumer类分析关键代码与核心算法2.1 订阅与分区分配2.2 拉取消息2.3

SpringBoot集成XXL-JOB实现任务管理全流程

《SpringBoot集成XXL-JOB实现任务管理全流程》XXL-JOB是一款轻量级分布式任务调度平台,功能丰富、界面简洁、易于扩展,本文介绍如何通过SpringBoot项目,使用RestTempl... 目录一、前言二、项目结构简述三、Maven 依赖四、Controller 代码详解五、Service

Python 基于http.server模块实现简单http服务的代码举例

《Python基于http.server模块实现简单http服务的代码举例》Pythonhttp.server模块通过继承BaseHTTPRequestHandler处理HTTP请求,使用Threa... 目录测试环境代码实现相关介绍模块简介类及相关函数简介参考链接测试环境win11专业版python

GO语言短变量声明的实现示例

《GO语言短变量声明的实现示例》在Go语言中,短变量声明是一种简洁的变量声明方式,使用:=运算符,可以自动推断变量类型,下面就来具体介绍一下如何使用,感兴趣的可以了解一下... 目录基本语法功能特点与var的区别适用场景注意事项基本语法variableName := value功能特点1、自动类型推

Python从Word文档中提取图片并生成PPT的操作代码

《Python从Word文档中提取图片并生成PPT的操作代码》在日常办公场景中,我们经常需要从Word文档中提取图片,并将这些图片整理到PowerPoint幻灯片中,手动完成这一任务既耗时又容易出错,... 目录引言背景与需求解决方案概述代码解析代码核心逻辑说明总结引言在日常办公场景中,我们经常需要从 W

基于Python实现自动化邮件发送系统的完整指南

《基于Python实现自动化邮件发送系统的完整指南》在现代软件开发和自动化流程中,邮件通知是一个常见且实用的功能,无论是用于发送报告、告警信息还是用户提醒,通过Python实现自动化的邮件发送功能都能... 目录一、前言:二、项目概述三、配置文件 `.env` 解析四、代码结构解析1. 导入模块2. 加载环

linux系统上安装JDK8全过程

《linux系统上安装JDK8全过程》文章介绍安装JDK的必要性及Linux下JDK8的安装步骤,包括卸载旧版本、下载解压、配置环境变量等,强调开发需JDK,运行可选JRE,现JDK已集成JRE... 目录为什么要安装jdk?1.查看linux系统是否有自带的jdk:2.下载jdk压缩包2.解压3.配置环境

使用shardingsphere实现mysql数据库分片方式

《使用shardingsphere实现mysql数据库分片方式》本文介绍如何使用ShardingSphere-JDBC在SpringBoot中实现MySQL水平分库,涵盖分片策略、路由算法及零侵入配置... 目录一、ShardingSphere 简介1.1 对比1.2 核心概念1.3 Sharding-Sp