自定义shell命令行解释器自制

🎬 胖咕噜的稞达鸭个人主页
🔥 个人专栏 : 《数据结构《C++初阶高阶》
《Linux系统学习》
《算法入门》

⛺️技术的杠杆,撬动整个世界!


shell的原理:
以环境变量继承为基础,通过「提示符显示→命令读取→解析→内建命令执行 / 外部命令 fork+exec 启动子进程」的无限闭环,模拟命令交互与执行能力

头文件和宏定义:

cpp 复制代码
#include <iostream>
#include <cstdio>
#include <cstdlib>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <cstring>
#include <unordered_map>//哈希表,用来存储别名映射:alias 名称-原始命令

#define COMMAND_SIZE 1024//命令行/提示符的最大长度
#define FORMAT "[%s@%s %s]输入命令 "
//命令提示符格式([用户名@主机名 当前目录]输入命令)

全局变量定义

shell命令行解释器有两张表:

  1. 一张是命令行参数表,存储解析之后的命令和参数;(我们宏定义命令行参数,一行命令最多有128个参数,而且每一个参数都要封装在一个参数数组中,初始化每一个参数的值都是0);
  2. 一张是环境变量表,用于shell自身维护的环境变量的集合(我们宏定义环境变量个数是100个,都被封装在一个环境变量数组中,初始化环境变量的个数为0)
  3. 映射表的存在意义是:alias_list是一个C++哈希表,存储shell的命令别名(映射关系),就是给常用命令起一个小名。ls -l 简化为ll
cpp 复制代码
// 下面是shell定义的全局数据
// 1. 命令行参数表
#define MAXARGC 128//最大参数个数(如ls -a -l 是3个参数)
char *g_argv[MAXARGC];//参数数组
int g_argc = 0; //实际参数个数(如ls -a -l 对应g_argc=3)

// 2. 环境变量表(shell自身维护的环境变量集合)
#define MAX_ENVS 100 //最大环境变量个数
char *g_env[MAX_ENVS];//环境变量数组
int g_envs = 0;//实际环境变量个数

// 3. 别名映射表
std::unordered_map<std::string, std::string> alias_list;

// for test
char cwd[1024];           
char cwdenv[1024];

// last exit code
int lastcode = 0;

获取用户名,主机名,家目录,当前工作目录等

cpp 复制代码
const char *GetUserName()
{
    const char *name = getenv("USER");
    return name == NULL ? "None" : name;
}    
                                                 
const char *GetHostName()
{
    const char *hostname = getenv("HOSTNAME");
    return hostname == NULL ? "None" : hostname;
}

const char *GetPwd()          
{
    //const char *pwd = getenv("PWD");
    const char *pwd = getcwd(cwd, sizeof(cwd));
    if(pwd != NULL)
    {
        snprintf(cwdenv, sizeof(cwdenv), "PWD=%s", cwd);
        putenv(cwdenv);
    }
    return pwd == NULL ? "None" : pwd;
}

const char *GetHome()
{
    const char *home = getenv("HOME");
    return home == NULL ? "" : home;
}
cpp 复制代码
// getcwd:系统调用,获取真实当前目录

getcwd:系统调用,获取真实当前目录

格式:PWD=%s,环境变量的标准格式是键=值,举例子:如果cwd/home/user,格式化cwdenv之后是:PWD = home/user;

接着更新系统环境变量PWDputenv(cwdenv)的作用是把格式化之后的"PWD=/home/user"注册到系统中,覆盖原来的PWD环境变量。

snprintf中的四个参数意义:

bash 复制代码
snprintf(cwdenv,sizeof(cwdenv),"PWD=%s",cwd);

第一个参数cwdenv目标缓冲区,

第二个参数缓冲区的最大容量,

第三个参数格式化输入的字符串,

第四个参数替换占位符的实际内容

为什么getcwd有两个参数?
getcwd,第一个参数是存放目录路径的缓冲区,第二个参数可以判断这个命令输入的内容是否超过了缓冲区大小,导致内存溢出。

为什么要判断pwd是否为空?

如果直接格式化PWD,如果当前目录的路径长度超过size()第二个参数,getcwd写不下完整的路径,就会返回NULL;或者是权限问题,普通用户状态下无法执行超级用户下的某些指令,getcwd无法获取路径,就会返回NULL

这样getcwd一旦执行失败,cwd中的内容是随机的,会导致PWD环境变量被设置为无效值,甚至程序奔溃。

环境变量初始化

为什么要环境变量初始化?

环境变量初始化的本质是为自定义shell建立独立而且可控的环境变量体系,避免直接依赖父shell传递的environ(系统全局环境变量数组):

  • 父 Shell 的 environ 是只读或共享的,直接修改可能导致权限错误;
  • 无法灵活添加、删除、修改环境变量。
cpp 复制代码
void InitEnv()
{
    extern char **environ;//1.声明系统全局环境变量数组
    memset(g_env, 0, sizeof(g_env));//2.清空自定义环境变量数组
    g_envs = 0;//3.重置环境变量计数

    //本来要从配置文件来
//核心步骤:从父 Shell 传递的 environ 复制环境变量到自定义 g_env(深拷贝)
    //1. 获取环境变量
    for(int i = 0; environ[i]; i++)
    {
        // 1.1 申请空间
        g_env[i] = (char*)malloc(strlen(environ[i])+1);
        strcpy(g_env[i], environ[i]);
        g_envs++;
    }
    g_env[g_envs++] = (char*)"HAHA=for_test"; //for_test
    g_env[g_envs] = NULL;

    //2. 导成环境变量
    for(int i = 0; g_env[i]; i++)
    {
        putenv(g_env[i]);
     }
    environ = g_env;
}

内建命令实现

Cd命令实现:

cpp 复制代码
//command
bool Cd()
{
    // cd argc = 1
    if(g_argc == 1)//已经解析出有效的命令名(g_argv[0] == "cd"),g_argc == 0是不可能出现的场景//情况1:输入cd,没有参数
    {
        std::string home = GetHome();//获取家目录
        if(home.empty()) return true;//家目录获取失败直接返回
        chdir(home.c_str());//系统调用,切换工作目录(参数为c字符串)
    }
    else//输入:cd + 参数
    {
        std::string where = g_argv[1];//获取目标目录参数
        // cd - / cd ~
        if(where == "-")//切换到上一级目录
        {
            // Todu
        }
        else if(where == "~")//切换到家目录
        {
            // Todu
        }
        else//直接切换到目录路径
        {
            chdir(where.c_str());
        }
    }
    return true;
}

内建echo命令的实现函数,无返回值(内建命令执行结果通过输出体现)

g_argc== 2:仅仅处理一个参数的场景(命令名echo+1个参数,总个数为2)

适用于三种场景:

  1. echo $?上一个命令的退出码;
  2. echo $PATH输出环境变量xxx的值
  3. echo+普通字符串
cpp 复制代码
void Echo()
{
    if(g_argc == 2)
    {
        // echo "hello world"
        // echo $?
        // echo $PATH
        std::string opt = g_argv[1];
        if(opt == "$?")
        {
            std::cout << lastcode << std::endl;
            lastcode = 0;// 输出后重置退出码(避免重复使用,符合原生 Shell 行为)
        }
        else if(opt[0] == '$')
        {
            std::string env_name = opt.substr(1);//截取$后面的环境变量名($PATH->path)
            const char *env_value = getenv(env_name.c_str());//读取环境变量值
            if(env_value)//读取出来环境变量存在就输出
                std::cout << env_value << std::endl;
        }
        else
        {
            std::cout << opt << std::endl;
        }
    }
}

辅助函数:构造命令提示符

  1. 提取目录名
cpp 复制代码
// / /a/b/c
std::string DirName(const char *pwd)
{
#define SLASH "/"
// 路径分隔符(Linux 下为 /)
    std::string dir = pwd;
    if(dir == SLASH) return SLASH;//特殊情况:路径是根目录(/),直接返回 /
    auto pos = dir.rfind(SLASH);
// 从字符串末尾查找第一个 / 的位置(如 /home/user → 5)
    if(pos == std::string::npos) return "BUG?";
    return dir.substr(pos+1);
}
  1. 构造提示符字符串
    char cmd_prompt[]:让函数能把生成的提示符 "交出来" 的结果容器
    int size:防止容器装不下导致内存溢出的安全保障
cpp 复制代码
void MakeCommandLine(char cmd_prompt[], int size)
{
    snprintf(cmd_prompt, size, FORMAT, GetUserName(), GetHostName(), DirName(GetPwd()).c_str());
}
  1. 打印命令行提示符
cpp 复制代码
void PrintCommandPrompt()
{
    char prompt[COMMAND_SIZE];
 // 存储提示符的缓冲区
    MakeCommandLine(prompt, sizeof(prompt));
    printf("%s", prompt);
    fflush(stdout);
}

命令读取与解析

  1. 读取用户输入
cpp 复制代码
//读取用户输入的命令                                                                 
  bool GetCommandLine(char* out,int size)    
  {                                           
      char* c = fgets(out,size,stdin);//将fgets:从键盘stdin读取用户输入,存到out中    
      if(c ==NULL)return false;                
      out[strlen(out)-1]=0;//去掉fgets读取的换行符                 
      if(strlen(out) ==0)return false;//用户只按回车,返回false    
      return true;         
  }       
  1. 命令解析:
cpp 复制代码
// 3. 命令行分析 "ls -a -l" -> "ls" "-a" "-l"
bool CommandParse(char *commandline)
{
#define SEP " "  // 命令参数分隔符(空格)
    g_argc = 0;  // 重置参数计数器(每次解析前清空)
    // 第一次调用 strtok:以 SEP(空格)分割 commandline,获取第一个参数(命令名)
    g_argv[g_argc++] = strtok(commandline, SEP);
    // 循环调用 strtok:后续传入 NULL,继续分割上一次的字符串,获取后续参数
    while((bool)(g_argv[g_argc++] = strtok(nullptr, SEP)));
    // 上面的循环会多计数一次(最后一次 strtok 返回 NULL,g_argc 仍+1),这里修正
    g_argc--;
    return g_argc > 0 ? true : false;  // 若解析到有效参数(至少有命令名),返回 true
}

辅助函数:打印参数

cpp 复制代码
void PrintArgv()
{
    for(int i = 0; g_argv[i]; i++)
    {
        printf("argv[%d]->%s\n", i, g_argv[i]);
    }
    printf("argc: %d\n", g_argc);
}

内建命令检测

cpp 复制代码
bool CheckAndExecBuiltin()
{
    std::string cmd = g_argv[0];
    if(cmd == "cd")
    {
        Cd();                                   
        return true;
    }
    else if(cmd == "echo")
    {
        Echo();
        return true;
    }
    else if(cmd == "export")
    {
    }
    else if(cmd == "alias")
    {
       // std::string nickname = g_argv[1];
       // alias_list.insert(k, v);
    }

    return false;
}

外部命令执行

cpp 复制代码
int Execute()
{
    pid_t id = fork();
    if(id == 0)//判断是不是子进程,fork()子进程会返回0
    {
        //child
        execvp(g_argv[0], g_argv);//替换当前进程的代码段,数据段,堆栈,执行指定的外部程序
        exit(1);
    }
    int status = 0;//用于存储waitpid获取的子进程退出状态
    //父进程在这里阻塞,直到子进程执行完毕,无论是execvp成功执行后退出还是失败后exit(1),才会执行后续代码。
    // father
    pid_t rid = waitpid(id, &status, 0);
    if(rid > 0)
    {
        lastcode = WEXITSTATUS(status);
    }
    //只有子进程 "正常终止"(比如exit(n)、return n)时,WEXITSTATUS才有意义;如果子进程被信号终止(比如kill -9),WEXITSTATUS返回 0。
    return 0;
}

调用fork()之后,操作系统就会复制当前进程(父进程)的内存空间,上下文等,形成一个子进程。

返回值规则:

父进程中返回子进程的PID

子进程中返回0;

id变量在父子进程中存储不同的值,是区分父子进程的关键。
g_argv[0]:要执行的程序路径 / 名称(比如"ls""/bin/ps");
g_argv:字符串数组,是传递给新程序的命令行参数(格式要求:最后一个元素必须是NULL,比如{"ls", "-l", NULL})。
g_argv:全局变量(从变量名前缀g_可判断),存储命令行参数列表。
核心逻辑

子进程调用execvp后,自身的代码会被g_argv[0]指定的程序替换;

如果execvp执行成功,子进程后续的代码(比如exit(1)永远不会执行

如果execvp执行失败(比如程序不存在、权限不足),才会执行后续的exit(1)

主函数

cpp 复制代码
int main()                                
{
    // shell 启动的时候,从系统中获取环境变量
    // 我们的环境变量信息应该从父shell统一来
    InitEnv();

    while(true)
    {
        // 1. 输出命令行提示符
        PrintCommandPrompt();

        // 2. 获取用户输入的命令
        char commandline[COMMAND_SIZE];
        if(!GetCommandLine(commandline, sizeof(commandline)))
            continue;

        // 3. 命令行分析 "ls -a -l" -> "ls" "-a" "-l"
        if(!CommandParse(commandline))
            continue;
        //PrintArgv();

        // 检测别名
        // 4. 检测并处理内键命令
        if(CheckAndExecBuiltin())
            continue;

        // 5. 执行命令
        Execute();
    }
    //cleanup();
    return 0;
}

所以要写一个shell,需要循环以下过程:

  1. 获取命令行
  2. 解析命令行
  3. 建立一个子进程(fork)
  4. 替换子进程(execvp)
  5. 父进程等待子进程退出(wait)
    根据这些思路,和我们前面的学的技术,就可以自己来实现一个shell了。

完整源码:

cpp 复制代码
#include <iostream>
#include <cstdio>
#include <cstdlib>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <cstring>
#include <unordered_map>

#define COMMAND_SIZE 1024
#define FORMAT "[%s@%s %s]输入命令 "

// 下面是shell定义的全局数据

// 1. 命令行参数表
#define MAXARGC 128
char *g_argv[MAXARGC];
int g_argc = 0; 

// 2. 环境变量表
#define MAX_ENVS 100
char *g_env[MAX_ENVS];
int g_envs = 0;

// 3. 别名映射表
std::unordered_map<std::string, std::string> alias_list;

// for test
char cwd[1024];
char cwdenv[1024];

// last exit code
int lastcode = 0;

const char *GetUserName()
{
    const char *name = getenv("USER");
    return name == NULL ? "None" : name;
}

const char *GetHostName()
{
    const char *hostname = getenv("HOSTNAME");
    return hostname == NULL ? "None" : hostname;
}

const char *GetPwd()
{
    //const char *pwd = getenv("PWD");
    const char *pwd = getcwd(cwd, sizeof(cwd));
    if(pwd != NULL)
    {
        snprintf(cwdenv, sizeof(cwdenv), "PWD=%s", cwd);
        putenv(cwdenv);
    }
    return pwd == NULL ? "None" : pwd;
}

const char *GetHome()
{
    const char *home = getenv("HOME");
    return home == NULL ? "" : home;
}

void InitEnv()
{
    extern char **environ;
    memset(g_env, 0, sizeof(g_env));
    g_envs = 0;

    //本来要从配置文件来
    //1. 获取环境变量
    for(int i = 0; environ[i]; i++)
    {
        // 1.1 申请空间
        g_env[i] = (char*)malloc(strlen(environ[i])+1);
        strcpy(g_env[i], environ[i]);
        g_envs++;
    }
    g_env[g_envs++] = (char*)"HAHA=for_test"; //for_test
    g_env[g_envs] = NULL;

    //2. 导成环境变量
    for(int i = 0; g_env[i]; i++)
    {
        putenv(g_env[i]);
    }
    environ = g_env;
}

//command
bool Cd()
{
    // cd argc = 1
    if(g_argc == 1)
    {
        std::string home = GetHome();
        if(home.empty()) return true;
        chdir(home.c_str());
    }
    else
    {
        std::string where = g_argv[1];
        // cd - / cd ~
        if(where == "-")
        {
            // Todu
        }
        else if(where == "~")
        {
            // Todu
        }
        else
        {
            chdir(where.c_str());
        }
    }
    return true;
}

void Echo()
{
    if(g_argc == 2)
    {
        // echo "hello world"
        // echo $?
        // echo $PATH
        std::string opt = g_argv[1];
        if(opt == "$?")
        {
            std::cout << lastcode << std::endl;
            lastcode = 0;
        }
        else if(opt[0] == '$')
        {
            std::string env_name = opt.substr(1);
            const char *env_value = getenv(env_name.c_str());
            if(env_value)
                std::cout << env_value << std::endl;
        }
        else
        {
            std::cout << opt << std::endl;
        }
    }
}

// / /a/b/c
std::string DirName(const char *pwd)
{
#define SLASH "/"
    std::string dir = pwd;
    if(dir == SLASH) return SLASH;
    auto pos = dir.rfind(SLASH);
    if(pos == std::string::npos) return "BUG?";
    return dir.substr(pos+1);
}

void MakeCommandLine(char cmd_prompt[], int size)
{
    snprintf(cmd_prompt, size, FORMAT, GetUserName(), GetHostName(), DirName(GetPwd()).c_str());
    //snprintf(cmd_prompt, size, FORMAT, GetUserName(), GetHostName(), GetPwd());
}

void PrintCommandPrompt()
{
    char prompt[COMMAND_SIZE];
    MakeCommandLine(prompt, sizeof(prompt));
    printf("%s", prompt);
    fflush(stdout);
}

bool GetCommandLine(char *out, int size)
{
    // ls -a -l => "ls -a -l\n" 字符串
    char *c = fgets(out, size, stdin);
    if(c == NULL) return false;
    out[strlen(out)-1] = 0; // 清理\n
    if(strlen(out) == 0) return false;
    return true;
}

// 3. 命令行分析 "ls -a -l" -> "ls" "-a" "-l"
bool CommandParse(char *commandline)
{
#define SEP " "
    g_argc = 0;
    // 命令行分析 "ls -a -l" -> "ls" "-a" "-l"
    g_argv[g_argc++] = strtok(commandline, SEP);
    while((bool)(g_argv[g_argc++] = strtok(nullptr, SEP)));
    g_argc--;
    return g_argc > 0 ? true:false;
}

void PrintArgv()
{
    for(int i = 0; g_argv[i]; i++)
    {
        printf("argv[%d]->%s\n", i, g_argv[i]);
    }
    printf("argc: %d\n", g_argc);
}

bool CheckAndExecBuiltin()
{
    std::string cmd = g_argv[0];
    if(cmd == "cd")
    {
        Cd();
        return true;
    }
    else if(cmd == "echo")
    {
        Echo();
        return true;
    }
    else if(cmd == "export")
    {
    }
    else if(cmd == "alias")
    {
       // std::string nickname = g_argv[1];
       // alias_list.insert(k, v);
    }

    return false;
}

int Execute()
{
    pid_t id = fork();
    if(id == 0)
    {
        //child
        execvp(g_argv[0], g_argv);
        exit(1);
    }
    int status = 0;
    // father
    pid_t rid = waitpid(id, &status, 0);
    if(rid > 0)
    {
        lastcode = WEXITSTATUS(status);
    }
    return 0;
}

int main()
{
    // shell 启动的时候,从系统中获取环境变量
    // 我们的环境变量信息应该从父shell统一来
    InitEnv();

    while(true)
    {
        // 1. 输出命令行提示符
        PrintCommandPrompt();

        // 2. 获取用户输入的命令
        char commandline[COMMAND_SIZE];
        if(!GetCommandLine(commandline, sizeof(commandline)))
            continue;

        // 3. 命令行分析 "ls -a -l" -> "ls" "-a" "-l"
        if(!CommandParse(commandline))
            continue;
        //PrintArgv();

        // 检测别名
        // 4. 检测并处理内键命令
        if(CheckAndExecBuiltin())
            continue;

        // 5. 执行命令
        Execute();
    }
    //cleanup();
    return 0;
}
相关推荐
草莓熊Lotso35 分钟前
Git 分支管理:从基础操作到协作流程(本地篇)
大数据·服务器·开发语言·c++·人工智能·git·sql
报错小能手38 分钟前
C++异常处理 终极及总结
开发语言·c++
q***33374 小时前
oracle 12c查看执行过的sql及当前正在执行的sql
java·sql·oracle
tobebetter95276 小时前
How to manage python versions on windows
开发语言·windows·python
Y***h1877 小时前
第二章 Spring中的Bean
java·后端·spring
9***P3347 小时前
PHP代码覆盖率
开发语言·php·代码覆盖率
8***29317 小时前
解决 Tomcat 跨域问题 - Tomcat 配置静态文件和 Java Web 服务(Spring MVC Springboot)同时允许跨域
java·前端·spring
CoderYanger7 小时前
优选算法-栈:67.基本计算器Ⅱ
java·开发语言·算法·leetcode·职场和发展·1024程序员节
jllllyuz7 小时前
Matlab实现基于Matrix Pencil算法实现声源信号角度和时间估计
开发语言·算法·matlab