Linux系统之----模拟实现shell

news2025/7/13 16:10:27

在前面一个阶段的学习中,我们已经学习了环境变量、进程控制等等一系列知识,也许有人会问,学这个东西有啥用?那么,今天我就和大家一起综合运用一下这些知识,模拟实现下shell!

首先我们来看一看我们的shell都有些什么,打开一个shell:

有一个命令行提示符, 由用户名,主机名,当前目录,提示符$构成,那我们一点一点破解!

1.获取用户名,主机名

首先,用户名和主机名都可以通过getenv来查看,那么我们是不是也可以通过getenv来获取呢?于是,我们便可以像如下一样写出代码:

static std::string GetUserName()
{
    std::string username = getenv("USER");
    return username.empty() ? "None" : username;
}
static std::string GetHostName()
{
    std::string hostname = getenv("HOSTNAME");
    return hostname.empty() ? "None" : hostname;
}

那为什么要加上那个static呢?就是为了增强鲁棒性和健壮性,保证代码只能在这个文件里面使用!

 具体代码内容比较简单,就不解释了!

2.路径的设置

之后是这个路径,要求我们要随着我们操作的变化而变化:

路径也可以通过env来查看,所以我们写出如下代码:

char pwd[1024]; // 全局变量空间,保存当前shell进程的工作路径
int lastcode = 0;
static std::string GetPwd()
{
    // 环境变量的变化,可能会依赖于进程,pwd需要shell自己更新环境变量的值
    //std::string pwd = getenv("PWD");
    //return pwd.empty() ? "None" : pwd;
    
    char temp[1024];
    getcwd(temp, sizeof(temp));
    // 顺便更新一下shell自己的环境变量pwd
    snprintf(pwd, sizeof(pwd), "PWD=%s", temp);
    putenv(pwd);

    // /
    // /home/whb/code/code
    std::string pwd_lable = temp;
    const std::string pathsep = "/";
    auto pos = pwd_lable.rfind(pathsep);
    if(pos == std::string::npos)
    {
        return "None";
    }

    pwd_lable = pwd_lable.substr(pos+pathsep.size());
    return pwd_lable.empty() ? "/" : pwd_lable;

代码逻辑:使用getcwd函数获取当前工作目录的绝对路径,并将其存储在字符数组temp中,再使用snprintf函数将temp中的路径格式化为PWD=路径的形式,并存储在pwd中。然后使用putenv函数将这个新的环境变量PWD设置到当前进程的环境变量中, 说人话就是:更新环境变量PWD,之后便是找“/”,如果没有找到/(即posstd::string::npos),则返回字符串"None"。寻找的具体操作就是先定义一个变量,并将temp赋值过去,之后找最后一个/,最后是使用substr函数提取pwd_label中最后一个/之后的部分。如果提取后的部分为空(即路径以/结尾),则返回"/";否则返回提取后的路径部分。

具体显示效果请参考centos的,我这里是Ubuntu22.04的,可能显示界面不是很一样,代码以centos为准!

 补充:这里我们用到了snprintf,其本质和printf是差不多的,Printf是根据给定的格式进行写入,而这个snprintf是像字符串中写入

3.获取家目录 

之后我们还要获取家目录,即当有人用我的shell的时候输入env的时候我们的家目录也要有显示: 

这里直接给出代码: 

static std::string GetHomePath()
{
    std::string home = getenv("HOME");
    return home.empty() ? "/" : home;
}

4.输出提示符

 但是,我们将这些都获取了就完事了吗?我们得输出啊!因此,我们还要写一个命令行输出的函数!用于输出提示符

void PrintCommandPrompt()
{
    std::string user = GetUserName();
    std::string hostname = GetHostName();
    std::string pwd = GetPwd();
    printf("[%s@%s %s]# ", user.c_str(), hostname.c_str(), pwd.c_str());
}

 上述代码没什么解释的内容,应该大家都会的~

5.输入指令

之后输出了命令行提示符就完事了吗?我们还要输入指令啊!就像下图一样

 代码如下图所示:

//获取用户的键盘输入
bool GetCommandString(char cmd_str_buff[], int len)
{
    if(cmd_str_buff == NULL || len <= 0)
        return false;
    char *res = fgets(cmd_str_buff, len, stdin);
    if(res == NULL)
        return false;
    // ls -a -l\n -> ls -a -l\0
    cmd_str_buff[strlen(cmd_str_buff) - 1] = 0;
    return strlen(cmd_str_buff) == 0 ? false : true;
}

这里我解释一下,用户输入的指令假设为ls -l -a -n,我们要是普通的scanf或者cin的话那肯定不行,我们要以回车为结尾,而不是以空格为结尾,为此我们使用fgets函数, 之后还要检查一下是否成功正确写入!但是,这样真的没问题吗?(假设没有注释下面的那句代码),试一下就知道了,不行的,会多打出来一个空行!原因就是我们表面上输入的是ls -a -l,但是我们还按了回车!!!实际上获取的确是ls -a -l\n!!!改起来也容易,我们直接把\n换为\0就Ok了~

 好了,现在我们的指令是获取完了,但是佢现在还是一坨啊,是一大串,我们的shell还是识别不了我们想干什么,所以下一步我们肯定就是将其进行分割!

6.分割指令

这里由于一条命令语句里面会有诸多条,如命令 ls -a -l 我们要将其分开,这时我们可以借助命令行参数表(实际上就是一个数组),那我们怎么分割呢?我们在之前学过strtok,可以用起来!

这里补充介绍一下strtok,它按照指定的分隔符(通常是空白字符或逗号等)将一个字符串分割成多个子字符串,用法如下:

char *strtok(char *str, const char *delim);

值得注意的是,strtok是有记忆性的,会自动记住本次切割到哪里了!所以之后的调用中,第一个参数传NULL即可 ,而且strtok 会修改原始字符串,因为它在每个标记后插入空字符 \0 来分割字符串。这意味着原始字符串在 strtok 处理后将不再保持原样。

// 命令行参数表,我故意定义成为全局
char *gargv[ARGS] = {NULL};
int gargc = 0;
bool ParseCommandString(char cmd[])
{
    if(cmd == NULL)
        return false;
#define SEP " "
    //3. "ls -a -l" -> "ls" "-a" "-l"
    gargv[gargc++] = strtok(cmd, SEP);
    // 整个数字,最后以NULL结尾
    while((bool)(gargv[gargc++] = strtok(NULL, SEP)));
    // 回退一次,命令行参数的格式
    gargc--;

//#define DEBUG
#ifdef DEBUG
        printf("gargc: %d\n", gargc);
        printf("----------------------\n");
        for(int i = 0; i < gargc; i++)
        {
            printf("gargv[%d]: %s\n",i, gargv[i]);
        }
        printf("----------------------\n");
        for(int i = 0; gargv[i]; i++)
        {
            printf("gargv[%d]: %s\n",i, gargv[i]);
        }
#endif

    return true;
}

7.初始化

 在基础变量都准备完成之后,我们可以尝试运行我们的shell了,但是在运行之前,我们要先进行初始化!

void InitGlobal()
{
    gargc = 0;
    memset(gargv, 0, sizeof(gargv));
}

8. 父子进程的创建

通过前面学习,我们知道进程在创建的时候会被fork()分为父子进程,所以我们也要模仿实现以下~

void ForkAndExec()
{
    pid_t id = fork();
    if(id < 0)
    {
        //for : XXXXX
        perror("fork"); // errno -> errstring
        return;
    }
    else if(id == 0)
    {
        //子进程
        execvp(gargv[0], gargv);
        exit(0);
    }
    else
    {
        //父进程
        int status = 0;
        pid_t rid = waitpid(id, &status, 0);
        if(rid > 0)
        {
            lastcode = WEXITSTATUS(status);
        }
    }
}

这里仅解释一下中间部分:如果 fork() 返回 0,表示当前是子进程。使用 execvp() 函数执行一个新的程序,该程序由全局变量 gargv 指定(gargv[0] 是程序名,gargv 是参数列表)。如果 execvp() 执行失败,将调用 exit(0) 终止子进程。

9.内建命令的构建

在原版shell中,我们输入cd或者echo,我们的shell会对其进行响应,这个就是内建命令,那我们实现下:

bool BuiltInCommandExec()
{
    //内建命令: 是shell自己执行的命令,如同shell执行一个自己的函数
    //gargv[0]
    std::string cmd = gargv[0];
    bool ret = false;
    if(cmd == "cd")
    {
        // build
        if(gargc == 2)
        {
            std::string target = gargv[1];
            if(target == "~")
            {
                ret = true;
                chdir(GetHomePath().c_str());
            }
            else{
                ret = true;
                chdir(gargv[1]);
            }
        }
        else if(gargc == 1)
        {
            ret = true;
            chdir(GetHomePath().c_str());
        }
        else
        {
            //BUG
        }
    }
    else if(cmd == "echo")
    {
        if(gargc == 2)
        {
            std::string args = gargv[1];
            if(args[0] == '$')
            {
                if(args[1] == '?')
                {
                    printf("lastcode: %d\n", lastcode);
                    lastcode = 0;
                    ret = true;
                }
                else{
                    const char *name = &args[1];
                    printf("%s\n", getenv(name));
                    lastcode = 0;
                    ret = true;
                }
            }
            else{
                printf("%s\n", args.c_str());
                ret = true;
            }
        }
    }
    return ret;
}

如此,代码主体设计完成,下面我们写一下main.cc文件以及myshell.h文件:

由于这两个文件就是函数头文件以及各个接口函数调用,这里直接以汇总的形式给出!

这里说明一下,我们的几乎所有软件都是一个死循环!!!我们之前写的程序其实就是一个代码片段!

这是main.cc文件

#include "myshell.h"

#define SIZE 1024

int main()
{
    char commandstr[SIZE];
    while(true)
    {
        // 0. 初始化操作
        InitGlobal();
        // 1. 输出命令行提示符
        PrintCommandPrompt();
        // 2. 获取用户输入的命令
        if(!GetCommandString(commandstr, SIZE))
            continue;
        // 3. "ls -a -l" -> "ls" "-a" "-l"
        // 对命令字符串,进行解析 -> 命令行参数表
        ParseCommandString(commandstr);

        // 4. 检测命令,内键命令,要让shell自己执行!
        if(BuiltInCommandExec())
        {
            continue;
        }

        // 5.执行命令, 让子进程来进行执行
        ForkAndExec();
    }
    return 0;
}

这是myshell.h文件

#ifndef __MYSHELL_H__
#define __MYSHELL_H__

#include <stdio.h>

#define ARGS 64

void Debug();
void InitGlobal();
void PrintCommandPrompt();
bool GetCommandString(char cmd_str_buff[], int len);
bool ParseCommandString(char cmd[]);
void ForkAndExec();
bool BuiltInCommandExec();
#endif

 这是myshell.cc文件

#include "myshell.h"
#include <iostream>
#include <cstdio>
#include <cstring>
#include <string.h>
#include <stdlib.h>
#include <string>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>

// 命令行参数表,我故意定义成为全局
char *gargv[ARGS] = {NULL};
int gargc = 0;
char pwd[1024]; // 全局变量空间,保存当前shell进程的工作路径
int lastcode = 0;

void Debug()
{
    printf("hello shell!\n");
}

//void GetUserName(char name[], int len)
//{
//
//}



static std::string GetUserName()
{
    std::string username = getenv("USER");
    return username.empty() ? "None" : username;
}
static std::string GetHostName()
{
    std::string hostname = getenv("HOSTNAME");
    return hostname.empty() ? "None" : hostname;
}
static std::string GetPwd()
{
    // 环境变量的变化,可能会依赖于进程,pwd需要shell自己更新环境变量的值
    //std::string pwd = getenv("PWD");
    //return pwd.empty() ? "None" : pwd;
    
    char temp[1024];
    getcwd(temp, sizeof(temp));
    // 顺便更新一下shell自己的环境变量pwd
    snprintf(pwd, sizeof(pwd), "PWD=%s", temp);
    putenv(pwd);

    // /
    // /home/whb/code/code
    std::string pwd_lable = temp;
    const std::string pathsep = "/";
    auto pos = pwd_lable.rfind(pathsep);
    if(pos == std::string::npos)
    {
        return "None";
    }

    pwd_lable = pwd_lable.substr(pos+pathsep.size());
    return pwd_lable.empty() ? "/" : pwd_lable;
}

static std::string GetHomePath()
{
    std::string home = getenv("HOME");
    return home.empty() ? "/" : home;
}

// 输出提示符
void PrintCommandPrompt()
{
    std::string user = GetUserName();
    std::string hostname = GetHostName();
    std::string pwd = GetPwd();
    printf("[%s@%s %s]# ", user.c_str(), hostname.c_str(), pwd.c_str());
}

//获取用户的键盘输入
bool GetCommandString(char cmd_str_buff[], int len)
{
    if(cmd_str_buff == NULL || len <= 0)
        return false;
    char *res = fgets(cmd_str_buff, len, stdin);
    if(res == NULL)
        return false;
    // ls -a -l\n -> ls -a -l\0
    cmd_str_buff[strlen(cmd_str_buff) - 1] = 0;
    return strlen(cmd_str_buff) == 0 ? false : true;
}

bool ParseCommandString(char cmd[])
{
    if(cmd == NULL)
        return false;
#define SEP " "
    //3. "ls -a -l" -> "ls" "-a" "-l"
    gargv[gargc++] = strtok(cmd, SEP);
    // 整个数字,最后以NULL结尾
    while((bool)(gargv[gargc++] = strtok(NULL, SEP)));
    // 回退一次,命令行参数的格式
    gargc--;

//#define DEBUG
#ifdef DEBUG
        printf("gargc: %d\n", gargc);
        printf("----------------------\n");
        for(int i = 0; i < gargc; i++)
        {
            printf("gargv[%d]: %s\n",i, gargv[i]);
        }
        printf("----------------------\n");
        for(int i = 0; gargv[i]; i++)
        {
            printf("gargv[%d]: %s\n",i, gargv[i]);
        }
#endif

    return true;
}


void InitGlobal()
{
    gargc = 0;
    memset(gargv, 0, sizeof(gargv));
}

void ForkAndExec()
{
    pid_t id = fork();
    if(id < 0)
    {
        //for : XXXXX
        perror("fork"); // errno -> errstring
        return;
    }
    else if(id == 0)
    {
        //子进程
        execvp(gargv[0], gargv);
        exit(0);
    }
    else
    {
        //父进程
        int status = 0;
        pid_t rid = waitpid(id, &status, 0);
        if(rid > 0)
        {
            lastcode = WEXITSTATUS(status);
        }
    }
}

bool BuiltInCommandExec()
{
    //内建命令: 是shell自己执行的命令,如同shell执行一个自己的函数
    //gargv[0]
    std::string cmd = gargv[0];
    bool ret = false;
    if(cmd == "cd")
    {
        // build
        if(gargc == 2)
        {
            std::string target = gargv[1];
            if(target == "~")
            {
                ret = true;
                chdir(GetHomePath().c_str());
            }
            else{
                ret = true;
                chdir(gargv[1]);
            }
        }
        else if(gargc == 1)
        {
            ret = true;
            chdir(GetHomePath().c_str());
        }
        else
        {
            //BUG
        }
    }
    else if(cmd == "echo")
    {
        if(gargc == 2)
        {
            std::string args = gargv[1];
            if(args[0] == '$')
            {
                if(args[1] == '?')
                {
                    printf("lastcode: %d\n", lastcode);
                    lastcode = 0;
                    ret = true;
                }
                else{
                    const char *name = &args[1];
                    printf("%s\n", getenv(name));
                    lastcode = 0;
                    ret = true;
                }
            }
            else{
                printf("%s\n", args.c_str());
                ret = true;
            }
        }
    }
    return ret;
}




好了,本篇文章到此结束~ 

 

 

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

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

相关文章

TCP黏包解决方法

1. 问题描述 TCP客户端每100ms发送一次数据,每次为16006字节的数据长度。由于TCP传输数据时,为了达到最佳传输效能,数据包的最大长度需要由MSS限定(MSS就是TCP数据包每次能够传输的最大数据分段),超过这个长度会进行自动拆包。也就是说虽然客户端一次发送16006字节数据,…

vue访问后端接口,实现用户注册

文章目录 一、后端接口文档二、前端代码请求响应工具调用后端API接口页面函数绑定单击事件&#xff0c;调用/api/user.js中的函数 三、参考视频 一、后端接口文档 二、前端代码 请求响应工具 /src/utils/request.js //定制请求的实例//导入axios npm install axios import …

Nginx性能调优与深度监控

目录 1更改进程数与连接数 &#xff08;1&#xff09;进程数 &#xff08;2&#xff09;连接数 2&#xff0c;静态缓存功能设置 &#xff08;1&#xff09;设置静态资源缓存 &#xff08;2&#xff09;验证静态缓存 3&#xff0c;设置连接超时 4&#xff0c;日志切割 …

如何在大型项目中解决 VsCode 语言服务器崩溃的问题

在大型C/C项目中&#xff0c;VS Code的语言服务器&#xff08;如C/C扩展&#xff09;可能因内存不足或配置不当频繁崩溃。本文结合系统资源分析与实战技巧&#xff0c;提供一套完整的解决方案。 一、问题根源诊断 1.1 内存瓶颈分析 通过top命令查看系统资源使用情况&#xff…

AutoDL实现端口映射与远程连接AutoDL与Pycharm上传文件到远程服务器(李沐老师的环境)

文章目录 以上配置的作用前提AutoDL实现端口映射远程连接AutoDLPycharm上传文件到远程服务器以上配置的作用 使用AutoDL的实例:因本地没有足够强的算力,所以需要使用AutoDL AutoDL端口映射:当在实例上安装深度学习的环境,但因为实例的linux系统问题,无法图形化显示d2l中的文件…

13.thinkphp的Session和cookie

一&#xff0e;Session 1. 在使用Session之前&#xff0c;需要开启初始化&#xff0c;在中间件文件middleware.php&#xff1b; // Session 初始化 \think\middleware\SessionInit::class 2. TP6.0不支持原生$_SESSION的获取方式&#xff0c;也不支持session_开头的函数&…

多线程获取VI模块的YUV数据

一.RV1126 VI模块采集摄像头YUV数据的流程 step1&#xff1a;VI模块初始化 step2&#xff1a;启动VI模块工作 step3&#xff1a;开启多线程采集VI数据并保存 1.1初始化VI模块&#xff1a; VI模块的初始化实际上就是对VI_CHN_ATTR_S的参数进行设置、然后调用RK_MPI_VI_SetC…

[ctfshow web入门] web68

信息收集 highlight_file被禁用了&#xff0c;使用cinclude("php://filter/convert.base64-encode/resourceindex.php");读取index.php&#xff0c;使用cinclude("php://filter/convert.iconv.utf8.utf16/resourceindex.php");可能有些乱码&#xff0c;不…

16前端项目----交易页

交易 交易页Trade修改默认地址商品清单reduce计算总数和总价应用 统一引入接口提交订单 交易页Trade 在computed中mapState映射出addressInfo和orderInfo&#xff0c;然后v-for渲染到组件当中 修改默认地址 <div class"address clearFix" v-for"address in …

2003-2020年高铁线路信息数据

2003-2020年高铁线路信息数据 1、时间&#xff1a;2003-2020年 2、来源&#xff1a;Chinese High-speed Rail and Airline Database&#xff0c;CRAD 3、指标&#xff1a;高铁线路名称、起点名、终点名、开通时间、线路长度(km)、设计速度(km/h&#xff09;、沿途主要车站 …

MySQL COUNT(*) 查询优化详解!

目录 前言1. COUNT(*) 为什么慢&#xff1f;—— InnoDB 的“计数烦恼” &#x1f914;2. MySQL 执行 COUNT(*) 的方式 (InnoDB)3. COUNT(*) 优化策略&#xff1a;快&#xff01;准&#xff01;狠&#xff01;策略一&#xff1a;利用索引优化带 WHERE 子句的 COUNT(*) (最常见且…

nginx配置协议

1. 7层协议 OSI&#xff08;Open System Interconnection&#xff09;是一个开放性的通行系统互连参考模型&#xff0c;他是一个定义的非常好的协议规范&#xff0c;共包含七层协议。直接上图&#xff0c;这样更直观些&#xff1a; 1.1 协议配置 1.1.1 7层配置 这里我们举例…

UE5 PCG学习笔记

https://www.bilibili.com/video/BV1onUdY2Ei3/?spm_id_from333.337.search-card.all.click&vd_source707ec8983cc32e6e065d5496a7f79ee6 一、安装PCG 插件里选择以下进行安装 移动目录后&#xff0c;可以使用 Update Redirector References&#xff0c;更新下&#xff0…

《用MATLAB玩转游戏开发》打砖块:向量反射与实时物理模拟MATLAB教程

《用MATLAB玩转游戏开发&#xff1a;从零开始打造你的数字乐园》基础篇&#xff08;2D图形交互&#xff09;-《打砖块&#xff1a;向量反射与实时物理模拟》MATLAB教程 &#x1f3ae; 文章目录 《用MATLAB玩转游戏开发&#xff1a;从零开始打造你的数字乐园》基础篇&#xff08…

vue配置代理解决前端跨域的问题

文章目录 一、概述二、报错现象三、通过配置代理来解决修改request.js中的baseURL为/api在vite.config.js中增加代理配置 四、参考资料 一、概述 跨域是指由于浏览器的同源策略限制&#xff0c;向不同源(不同协议、不同域名、不同端口)发送ajax请求会失败 二、报错现象 三、…

java+vert.x实现内网穿透jrp-nat

用java vert.x开发一个内网穿透工具 内网穿透概述技术原理常见内网穿透工具用java vert.x开发内网穿透工具 jrp-nat为什么用java开发内网穿透工具&#xff1f;jrp-nat功能实现图解jrp-nat内网穿透工具介绍jrp-nat内网穿透工具特点jrp-nat软件架构jrp-nat安装教程jrp-nat程序下载…

【程序员AI入门:应用开发】8.LangChain的核心抽象

一、 LangChain 的三大核心抽象 1. ChatModel&#xff08;聊天模型&#xff09; 核心作用&#xff1a;与大模型&#xff08;如 GPT-4、Claude&#xff09;交互的入口&#xff0c;负责处理输入并生成输出。关键功能&#xff1a; 支持同步调用&#xff08;model.invoke&#xf…

每天五分钟机器学习:KTT条件

本文重点 在前面的课程中,我们学习了拉格朗日乘数法求解等式约束下函数极值,如果约束不是等式而是不等式呢?此时就需要KTT条件出手了,KTT条件是拉格朗日乘数法的推广。KTT条件不仅统一了等式约束与不等式约束的优化问题求解范式,KTT条件给出了这类问题取得极值的一阶必要…

Facebook的元宇宙新次元:社交互动如何改变?

科技的浪潮正将我们推向一个全新的时代——元宇宙时代。Facebook&#xff0c;这个全球最大的社交网络平台&#xff0c;已经宣布将公司名称更改为 Meta&#xff0c;全面拥抱元宇宙概念。那么&#xff0c;元宇宙究竟是什么&#xff1f;它将如何改变我们的社交互动方式呢&#xff…

概统期末复习--速成

随机事件及其概率 加法公式 推三个的时候ABC&#xff0c;夹逼准则 减法准则 除法公式 相互独立定义 两种分析 两个解法 古典概型求概率&#xff08;排列组合&#xff09; 分步相乘、分类相加 全概率公式和贝叶斯公式 两阶段问题 第一个小概率*A在小概率的概率。。。累计 …