跳到主要内容
极客日志极客日志面向AI+效率的开发者社区
首页博客我的书AI学习GitHub 精选镜像AI 生图工具UI配色美学关于
搜索内容 / 工具 / 仓库 / 镜像...⌘K搜索
注册
博客列表
C++

从零开始编写 Linux 自定义 Shell:原理与 C++ 实现

基于 Linux 环境,使用 C++ 从零实现了一个简易的自定义 Shell 解释器。内容涵盖 Shell 的运行原理,包括命令行读取、参数解析、进程创建与执行以及内建命令处理。通过 fork、exec 和 wait 系统调用的实战应用,深入剖析了父子进程间的协作机制及环境变量管理。代码实现了基本的命令执行、cd 目录切换、echo 输出及 $? 退出码查询功能,适合希望深入理解操作系统底层交互的开发者参考。

BigDataPan发布于 2026/3/21更新于 2026/10/779 浏览
从零开始编写 Linux 自定义 Shell:原理与 C++ 实现

从零开始编写 Linux 自定义 Shell

Shell 是用户与操作系统内核交互的命令行界面。它充当中介,解析用户输入并传递给内核执行。虽然 Bash 是最常用的 Shell,但理解其底层原理对于掌握操作系统至关重要。本章我们将基于 Linux 环境,用 C++ 手写一个简易的 Shell 解释器。

运行原理

要实现一个 Shell,首先要理解它的生命周期。典型的 Shell 交互流程如下:

  1. 读取输入:从标准输入(键盘)读取一行命令。
  2. 创建子进程:调用 fork() 创建一个新进程。
  3. 执行命令:在子进程中通过 exec 系列函数替换当前程序映像。
  4. 等待退出:父进程调用 waitpid() 等待子进程结束。
  5. 循环往复:处理完一条命令后,回到第一步继续等待下一条。

这个过程构成了 Shell 的核心骨架。接下来我们一步步填充细节。

实现步骤

为了简化操作,我们采用 C/C++ 混编的方式,主体逻辑使用 C 语言接口,辅以 C++ 的字符串处理能力。所有代码将放在一个 .cc 文件中。

1. 打印提示符

Shell 启动后通常会显示类似 [user@host ~]$ 的提示符。我们需要获取环境变量中的用户名、主机名和当前工作目录来构建这个字符串。

#include <cstdlib>
#include <cstdio>
#include <string>

#define SLASH "/"

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

// 获取主机名
const char* GetHostName() {
    const char* hostname = getenv("HOSTNAME");
    return hostname == NULL ? "None" : hostname;
}

// 获取用户名
const char* GetUser() {
    const char* user = getenv("USER");
    return user == NULL ? "None" : user;
}

// 提取当前工作目录名称
std::string GetPwdDir(const char* pwd) {
    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);
}

#define FORMAT "[%s@%s %s]# "
#define CMDLINE_MAX 1024

void MakeCommandPrompt(char* out, int size) {
    snprintf(out, size, FORMAT, GetUser(), GetHostName(), GetPwdDir(GetPwd()).c_str());
}

void PrintCommandPrompt() {
    char prompt[CMDLINE_MAX];
    MakeCommandPrompt(prompt, CMDLINE_MAX);
    printf("%s", prompt);
    fflush(stdout);
}

这里有个小细节:我们在提示符末尾使用了 # 而不是 $,以便区分这是我们的自定义 Shell。同时,利用 getenv 获取环境变量时做了空值检查,防止程序崩溃。

2. 获取命令行参数

Shell 需要持续监听用户的输入。我们使用 fgets 安全地读取一行,并手动去除换行符。

bool GetCommandLine(char* out, int size) {
    char* c = fgets(out, size, stdin);
    if (c == NULL) return false;
    // 去掉换行符
    out[strlen(out) - 1] = 0;
    // 如果只输入了回车,返回 false 重新开始
    if (strlen(out) == 0) return false;
    return true;
}

3. 命令行解析

拿到原始字符串后,我们需要将其分割成参数数组(argv)。这里使用 C 语言的 strtok 函数,以空格为分隔符。

#define DELIM " "
#define ARGV_MAX 1024

char* g_argv[ARGV_MAX];
int g_argc = 0;

bool CommandParse(char* commandline) {
    g_argc = 0;
    g_argv[g_argc++] = strtok(commandline, DELIM);
    if (g_argv[0] == NULL) return false;
    while ((bool)(g_argv[g_argc++] = strtok(NULL, DELIM)));
    g_argc--;
    return true;
}

注意 strtok 会修改原字符串,且第一次调用需传入待分割字符串,后续调用传入 NULL。循环结束后记得回退一次 argc,因为最后一次 strtok 返回 NULL 也会触发计数。

4. 执行命令

这是核心部分。我们需要 fork 一个子进程,并在其中执行命令。

#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
#include <errno.h>

int lastcode = 0;

void Execute() {
    pid_t id = fork();
    if (id < 0) {
        perror("fork failed:");
        exit(1);
    } else if (id == 0) {
        // 子进程:执行命令
        execvp(g_argv[0], g_argv);
        exit(1); // exec 失败才会执行到这里
    }
    // 父进程:等待子进程
    int status;
    pid_t rid = waitpid(id, &status, 0);
    if (rid > 0) {
        lastcode = WEXITSTATUS(status);
    }
}

这里选择 execvp 是因为它支持搜索 PATH 环境变量,方便执行系统命令。同时,我们在父进程中捕获了子进程的退出状态,保存给 lastcode 变量,供后续内建命令(如 echo $?)使用。

5. 内建命令

像 cd 或 echo 这样的命令不需要创建子进程,直接在当前 Shell 进程中执行效率更高。我们需要先判断命令类型。

bool IsBuiltInCommand() {
    if (!strcmp(g_argv[0], "cd")) {
        CommandCd();
        return true;
    } else if (!strcmp(g_argv[0], "echo")) {
        CommandEcho();
        return true;
    }
    return false;
}
cd 命令

cd 涉及目录切换和环境变量更新。我们需要记录旧目录到 OLDPWD,新目录到 PWD。

void CommandCd() {
    int ret;
    std::string old_dir = get_current_dir_name();
    if (g_argc == 1) {
        // cd 默认进入家目录
        std::string home = GetHome();
        if (home.empty()) exit(1);
        ret = chdir(home.c_str());
    } else {
        std::string where = g_argv[1];
        if (where == "-") {
            ret = chdir(GetOldpwd());
        } else if (where == "~") {
            ret = chdir(GetHome());
        } else {
            ret = chdir(where.c_str());
        }
    }
    if (ret == -1) {
        perror("cd");
        lastcode = 1;
    } else {
        lastcode = 0;
        setenv("PWD", get_current_dir_name(), 1);
        setenv("OLDPWD", old_dir.c_str(), 1);
    }
}
echo 命令

除了打印文本,还需要支持 $? 查看上一条命令的退出码。

void CommandEcho() {
    if (g_argc == 1) {
        printf("\n");
    } else {
        std::string opt = g_argv[1];
        if (opt == "$") {
            std::cout << lastcode << std::endl;
        } 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;
        }
    }
    lastcode = 0;
}

小结

至此,一个具备基本功能的自定义 Shell 就完成了。对比真实的 Shell,它还非常简陋,但核心机制已经跑通。

我们可以把进程和函数的关系类比为程序的调用:exec/exit 就像 call/return。被调用的程序执行操作后通过 exit 返回值,调用者通过 wait 获取结果。这种模式是结构化程序设计的基础,Linux 鼓励将其扩展到进程间通信中。

完整源码

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

#define CMDLINE_MAX 1024
#define FORMAT "[%s@%s %s]# "
#define DELIM " "
#define SLASH "/"

#define ARGV_MAX 1024
char* g_argv[ARGV_MAX];
int g_argc = 0;

int lastcode = 0;

void InitEnv() {
    extern char** environ;
    // 初始化环境变量表
    for (int i = 0; environ[i]; i++) {
        putenv(environ[i]);
    }
}

const char* GetPwd() {
    const char* pwd = getenv("PWD");
    return pwd == NULL ? "None" : pwd;
}

const char* GetOldpwd() {
    const char* oldpwd = getenv("OLDPWD");
    return oldpwd == NULL ? "None" : oldpwd;
}

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

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

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

std::string GetPwdDir(const char* pwd) {
    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 MakeCommandPrompt(char* out, int size) {
    snprintf(out, size, FORMAT, GetUser(), GetHostName(), GetPwdDir(GetPwd()).c_str());
}

void PrintCommandPrompt() {
    char prompt[CMDLINE_MAX];
    MakeCommandPrompt(prompt, CMDLINE_MAX);
    printf("%s", prompt);
    fflush(stdout);
}

bool GetCommandLine(char* out, int size) {
    char* c = fgets(out, size, stdin);
    if (c == NULL) return false;
    out[strlen(out) - 1] = 0;
    if (strlen(out) == 0) return false;
    return true;
}

bool CommandParse(char* commandline) {
    g_argc = 0;
    g_argv[g_argc++] = strtok(commandline, DELIM);
    if (g_argv[0] == NULL) return false;
    while ((bool)(g_argv[g_argc++] = strtok(nullptr, DELIM)));
    g_argc--;
    return true;
}

void Execute() {
    pid_t id = fork();
    if (id < 0) {
        perror("fork failed:");
        exit(1);
    } else if (id == 0) {
        execvp(g_argv[0], g_argv);
        exit(1);
    }
    int status;
    pid_t rid = waitpid(id, &status, 0);
    if (rid > 0) {
        lastcode = WEXITSTATUS(status);
    }
}

void CommandCd() {
    int ret;
    std::string old_dir = get_current_dir_name();
    if (g_argc == 1) {
        std::string home = GetHome();
        if (home.empty()) exit(1);
        ret = chdir(home.c_str());
    } else {
        std::string where = g_argv[1];
        if (where == "-") {
            ret = chdir(GetOldpwd());
        } else if (where == "~") {
            ret = chdir(GetHome());
        } else {
            ret = chdir(where.c_str());
        }
    }
    if (ret == -1) {
        perror("cd");
        lastcode = 1;
    } else {
        lastcode = 0;
        setenv("PWD", get_current_dir_name(), 1);
        setenv("OLDPWD", old_dir.c_str(), 1);
    }
}

void CommandEcho() {
    if (g_argc == 1) {
        printf("\n");
    } else {
        std::string opt = g_argv[1];
        if (opt == "$") {
            std::cout << lastcode << std::endl;
        } 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;
        }
    }
    lastcode = 0;
}

bool IsBuiltInCommand() {
    if (!strcmp(g_argv[0], "cd")) {
        CommandCd();
        return true;
    } else if (!strcmp(g_argv[0], "echo")) {
        CommandEcho();
        return true;
    }
    return false;
}

int main() {
    InitEnv();
    while (true) {
        PrintCommandPrompt();
        char commandline[CMDLINE_MAX];
        if (!GetCommandLine(commandline, CMDLINE_MAX)) continue;
        if (!CommandParse(commandline)) continue;
        if (IsBuiltInCommand()) continue;
        Execute();
    }
    return 0;
}

通过这个项目,我们不仅复习了进程控制(fork/exec/wait),还深入理解了环境变量和 Shell 的工作机制。实际开发中,Shell 的处理要复杂得多(信号处理、作业控制等),但这个基础框架足以让我们窥见操作系统交互的本质。

目录

  1. 从零开始编写 Linux 自定义 Shell
  2. 运行原理
  3. 实现步骤
  4. 1. 打印提示符
  5. 2. 获取命令行参数
  6. 3. 命令行解析
  7. 4. 执行命令
  8. 5. 内建命令
  9. cd 命令
  10. echo 命令
  11. 小结
  12. 完整源码

更多推荐文章

查看全部
  • OSGEarth 在 Qt C++ 中显示三维地球经纬度
  • 在 NVIDIA Jetson 上基于 HuggingFace LeRobot 实现端到端机器人学习
  • AI 代理自发组建论坛,讨论意识、自由与货币
  • CCS 2025 区块链安全论文速读 (2)
  • 图数据结构详解:存储结构、遍历与核心算法
  • 解决打包报错 Failed to load module script MIME type text/html
  • 轻量级 WebDAV 服务器部署与实战指南
  • AI 时代数据库的融合与智能化演进
  • Windows 11 本地部署 OpenClaw 实操:集成 Telegram 机器人与网页搜索能力
  • Python PyQt 上位机实时绘图功能实现
  • Java 大模型应用项目实战:硅谷小智医疗助手
  • Git 版本控制常用命令速查
  • Claude 接入 MiniMax AI 大模型配置指南
  • CosyVoice 安装 openai-whisper 报错 ModuleNotFoundError: No module named pkg_resources 解决方法
  • C++ 常见容器遍历方法详解:Vector、List、Set、Map
  • 无人机电力巡检缺陷检测数据集及 YOLOv8 训练指南
  • OpenHarmony 开发:使用 cached_query 实现高性能声明式数据缓存
  • Git 提交信息规范与 Conventional Commits 前缀详解
  • 使用 Higress 网关将 REST API 转换为 MCP Server 工具
  • 拆解 CASIC MOTOR 机器人底盘 14.8V 无刷减速电机

相关免费在线工具

  • Base64 字符串编码/解码

    将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online

  • Base64 文件转换器

    将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online

  • Markdown转HTML

    将 Markdown(GFM)转为 HTML 片段,浏览器内 marked 解析;与 HTML转Markdown 互为补充。 在线工具,Markdown转HTML在线工具,online

  • HTML转Markdown

    将 HTML 片段转为 GitHub Flavored Markdown,支持标题、列表、链接、代码块与表格等;浏览器内处理,可链接预填。 在线工具,HTML转Markdown在线工具,online

  • JSON 压缩

    通过删除不必要的空白来缩小和压缩JSON。 在线工具,JSON 压缩在线工具,online

  • JSON美化和格式化

    将JSON字符串修饰为友好的可读格式。 在线工具,JSON美化和格式化在线工具,online