欢迎光临
我们一直在努力

【Linux指南】进程控制系列(五)实战 —— 微型 Shell 命令行解释器实现

287e0d18392d0dc74c83c324a229e5c3.jpg

前面四篇文章,我们已经掌握了进程控制的 “全链路技能”:用fork创建子进程、exec替换程序、waitpid回收资源、exit终止进程。今天,我们将这些知识 “组装” 成一个能实际运行的工具 ——微型 Shell 命令行解释器(简称 “迷你 Shell”)。

这个迷你 Shell 将支持:命令行提示符(如[user@host dir]#)、内建命令(cd/export/env/echo)、外部命令(ls/ps等)、环境变量管理(继承与导出),完全遵循 Linux Shell 的核心工作逻辑。通过亲手实现,你会彻底明白 “输入一条命令后,Shell 到底在做什么”。

一、先搞懂:Shell 的本质是 “命令管家”

在写代码前,我们先回归本质:Shell 是一个 “命令管家”—— 它的核心工作是 “接收用户命令→解析命令→调度资源执行命令→反馈结果”,具体流程可拆解为一个无限循环:

  • 展示提示符:打印[用户名@主机名 工作目录]$,告诉用户 “可以输入命令了”;
  • 获取命令:读取用户输入的一行命令(如ls -l或cd /home);
  • 解析命令:将命令拆分为 “命令名 + 参数”(如ls -l拆成["ls", "-l", NULL]);
  • 执行命令:
    • 若为内建命令(如cd):Shell 自己执行(需修改 Shell 进程自身状态,不能用子进程);
    • 若为外部命令(如ls):Shell 创建子进程,子进程用exec替换为目标程序,Shell 等待子进程退出;
  • 循环往复:回到第一步,等待用户输入下一条命令。
  • 举个通俗的例子:Shell 就像餐厅服务员 —— 提示符是 “请问需要点什么?”,用户输入是 “一份牛排”(命令),解析是 “牛排 + 七分熟”(命令 + 参数),执行是:

    • 内建命令:服务员自己给你倒杯水(不用叫后厨);
    • 外部命令:服务员叫后厨(子进程)做牛排,自己在旁边等(waitpid),做好后给你端过来(反馈结果)。

    二、迷你 Shell 的核心模块设计

    我们将迷你 Shell 拆解为 5 个核心模块,逐个实现并讲解,最后整合为完整代码。每个模块都对应 Shell 的一个关键功能,且能复用前面学的进程控制知识。

    2.1 模块 1:命令行提示符 —— 给用户 “交互的入口”

    命令行提示符(如[ubuntu@localhost myshell]$)的作用是 “提示用户输入命令”,它需要包含三个关键信息:用户名、主机名、当前工作目录。我们通过 Linux 提供的函数获取这些信息:

    • 用户名:getenv("USER")(从环境变量中获取当前登录用户);
    • 主机名:getenv("HOSTNAME")(从环境变量中获取主机名);
    • 当前工作目录:getcwd()(获取当前进程的工作目录,比getenv("PWD")更准确,因为cd后PWD可能未更新)。
    实现细节:
    • 工作目录简化:默认显示完整路径(如/home/ubuntu/myshell),我们可以简化为 “最后一级目录”(如myshell),更符合常用 Shell 的习惯;
    • 缓冲区刷新:printf默认是 “行缓冲”,若提示符不含\\n,需用fflush(stdout)强制刷新,否则提示符会 “卡着不显示”。
    代码实现(提示符模块):

    c

    #include <stdio.h>
    #include <stdlib.h>
    #include <string.h>
    #include <unistd.h>

    // 全局变量:存储当前工作目录(避免频繁分配内存)
    #define BUF_SIZE 1024
    char g_pwd[BUF_SIZE] = {0};
    char g_last_pwd[BUF_SIZE] = {0}; // 存储上次工作目录,用于cd –

    // 1. 获取用户名
    static char* get_username() {
    char* username = getenv("USER");
    return (username == NULL) ? "unknown" : username;
    }

    // 2. 获取主机名
    static char* get_hostname() {
    char* hostname = getenv("HOSTNAME");
    return (hostname == NULL) ? "unknown-host" : hostname;
    }

    // 3. 获取当前工作目录(并更新PWD环境变量)
    static char* get_current_dir() {
    if (getcwd(g_pwd, BUF_SIZE) == NULL) { // getcwd获取当前工作目录
    perror("getcwd failed");
    return "unknown-dir";
    }
    // 更新环境变量PWD(确保echo $PWD能显示正确路径)
    static char pwd_env[BUF_SIZE] = {0};
    snprintf(pwd_env, BUF_SIZE, "PWD=%s", g_pwd);
    putenv(pwd_env); // putenv修改当前进程的环境变量
    return g_pwd;
    }

    // 4. 简化工作目录(只显示最后一级)
    static char* simplify_dir(char* full_dir) {
    if (full_dir == NULL || strcmp(full_dir, "/") == 0) {
    return "/"; // 根目录直接返回/
    }
    // 逆序查找最后一个/(如/home/ubuntu/myshell → 找到最后一个/,返回myshell)
    char* last_slash = strrchr(full_dir, '/');
    return (last_slash == NULL) ? full_dir : (last_slash + 1);
    }

    // 5. 打印命令行提示符
    void print_prompt() {
    char* username = get_username();
    char* hostname = get_hostname();
    char* full_dir = get_current_dir();
    char* simple_dir = simplify_dir(full_dir);

    // 格式:[用户名@主机名 简化目录]$
    printf("[%s@%s %s]$ ", username, hostname, simple_dir);
    fflush(stdout); // 强制刷新缓冲区,确保提示符立即显示
    }

    效果演示:

    调用print_prompt()后,终端会显示类似:

    plaintext

    [ubuntu@localhost myshell]$

    2.2 模块 2:命令获取与解析 —— 把 “字符串” 变成 “可执行指令”

    用户输入的命令是 “字符串”(如ls -l /home),我们需要将其拆分为 “命令名 + 参数数组”(如["ls", "-l", "/home", NULL]),才能传给execvp执行。这一步分两个子任务:

    子任务 1:获取用户输入(用 fgets 而非 scanf)
    • scanf的问题:遇到空格就停止读取,无法获取带空格的命令(如echo "hello world");
    • fgets的优势:读取一整行输入,包含空格,完美适配命令输入场景;
    • 处理细节:fgets会把用户输入的 “回车符\\n” 也读入,需将其替换为字符串结束符\\0;若输入为空行(只按回车),直接跳过。
    子任务 2:解析命令(用 strtok 按空格拆分)

    strtok是 C 标准库的字符串拆分函数,能按指定分隔符(这里是空格)拆分字符串:

    • 第一次调用:strtok(command, " "),传入要拆分的命令字符串,返回第一个 “非空格” 的子串(命令名);
    • 后续调用:strtok(NULL, " "),传入NULL表示 “继续拆分上次的字符串”,直到返回NULL(拆分结束);
    • 注意:拆分后的参数数组最后必须加NULL,因为execvp要求参数数组以NULL结尾。
    代码实现(命令获取与解析模块):

    c

    #include <ctype.h> // 包含isspace函数

    // 全局变量:存储命令参数(命令名+参数,最后以NULL结尾)
    #define ARGV_MAX 64
    char* g_argv[ARGV_MAX] = {0};
    int g_argc = 0; // 命令参数个数(包含命令名)

    // 1. 去除字符串前后的空格(避免空参数,如" ls -l " → "ls -l")
    static void trim_space(char* str) {
    if (str == NULL) return;
    // 去除前面的空格
    char* start = str;
    while (isspace(*start)) start++;
    // 去除后面的空格
    char* end = str + strlen(str) 1;
    while (end >= start && isspace(*end)) end;
    // 给字符串加结束符
    *(end + 1) = '\\0';
    // 移动字符串(覆盖前面的空格)
    memmove(str, start, end start + 2); // +2:包含end+1的\\0
    }

    // 2. 获取用户输入的命令
    int get_command(char* command_buf, int buf_size) {
    // 读取一行命令(fgets会读入\\n)
    if (fgets(command_buf, buf_size, stdin) == NULL) {
    // 若用户按Ctrl+D(EOF),返回-1表示退出
    printf("\\n");
    return 1;
    }
    // 去除换行符(将\\n替换为\\0)
    command_buf[strcspn(command_buf, "\\n")] = '\\0';
    // 去除前后空格
    trim_space(command_buf);
    // 处理空行(用户只按了回车)
    if (strlen(command_buf) == 0) {
    return 0;
    }
    return 1; // 成功获取有效命令
    }

    // 3. 解析命令(拆分为g_argv数组)
    void parse_command(char* command_buf) {
    // 重置全局变量(避免上次命令的残留)
    memset(g_argv, 0, sizeof(g_argv));
    g_argc = 0;

    // 第一次拆分:获取命令名
    char* token = strtok(command_buf, " ");
    while (token != NULL && g_argc < ARGV_MAX 1) { // 留一个位置放NULL
    g_argv[g_argc++] = token;
    // 后续拆分:获取参数
    token = strtok(NULL, " ");
    }
    g_argv[g_argc] = NULL; // 参数数组必须以NULL结尾
    }

    // 调试用:打印解析后的参数(可选)
    void debug_print_argv() {
    printf("解析结果:argc=%d\\n", g_argc);
    for (int i = 0; i < g_argc; i++) {
    printf("argv[%d] = %s\\n", i, g_argv[i]);
    }
    }

    效果演示:

    用户输入 ls -l /home ,解析后:

    • g_argc = 3;
    • g_argv = ["ls", "-l", "/home", NULL]。

    2.3 模块 3:内建命令处理 ——Shell “自己动手” 的命令

    内建命令(Built-in Command)是必须由 Shell 进程自身执行的命令,因为它们需要修改 Shell 的 “自身状态”(如cd修改工作目录、export修改环境变量)—— 若用子进程执行,修改的是子进程的状态,父进程(Shell)的状态不会变(进程具有独立性)。

    迷你 Shell 将实现 4 个核心内建命令:cd、export、env、echo,我们逐个实现。

    3.3.1 内建命令 1:cd—— 切换工作目录

    cd的核心是调用chdir系统函数修改当前进程的工作目录,但需要处理特殊场景:

    • cd(无参数):切换到用户家目录(getenv("HOME"));
    • cd ~:同无参数,切换到家目录;
    • cd -:切换到 “上次的工作目录”(需用g_last_pwd存储上次目录);
    • cd 目录路径:切换到指定目录(如cd /home)。
    3.3.2 内建命令 2:export—— 导出环境变量

    export的作用是 “将变量添加到当前进程的环境变量表中”,供后续执行的命令继承(如export MY_VAR=123):

    • 实现方式:用putenv函数(C 标准库),将 “KEY=VALUE” 格式的字符串添加到环境变量表;
    • 注意:export无参数时,可打印所有已导出的环境变量(可选功能)。
    3.3.3 内建命令 3:env—— 打印所有环境变量

    env的作用是 “打印当前进程的所有环境变量”,实现方式是遍历全局环境变量数组environ(extern char **environ),逐个打印每个环境变量(格式为 “KEY=VALUE”)。

    3.3.4 内建命令 4:echo—— 打印内容或环境变量

    echo支持三种场景:

    • echo 文本:直接打印文本(如echo hello → 输出hello);
    • echo $环境变量:打印指定环境变量的值(如echo $PATH → 输出/bin:/usr/bin);
    • echo $?:打印 “上次命令的退出码”(需用全局变量g_last_code存储)。
    代码实现(内建命令模块):

    c

    #include <sys/wait.h>

    // 全局变量:存储上次命令的退出码(供echo $?使用)
    int g_last_code = 0;
    // 全局变量:存储环境变量表(从父进程继承)
    extern char **environ;

    // 1. 判断是否为内建命令(返回1=是,0=否)
    int is_builtin_command() {
    if (g_argc == 0 || g_argv[0] == NULL) return 0;
    // 支持的内建命令列表
    const char* builtin_list[] = {"cd", "export", "env", "echo", NULL};
    for (int i = 0; builtin_list[i] != NULL; i++) {
    if (strcmp(g_argv[0], builtin_list[i]) == 0) {
    return 1;
    }
    }
    return 0;
    }

    // 2. 执行内建命令cd
    static void exec_cd() {
    char* target_dir = NULL;
    // 保存当前目录(用于cd -)
    strncpy(g_last_pwd, g_pwd, BUF_SIZE);

    // 处理不同参数场景
    if (g_argc == 1) {
    // cd无参数 → 切换到家目录
    target_dir = getenv("HOME");
    } else if (strcmp(g_argv[1], "~") == 0) {
    // cd ~ → 切换到家目录
    target_dir = getenv("HOME");
    } else if (strcmp(g_argv[1], "-") == 0) {
    // cd – → 切换到上次目录
    target_dir = g_last_pwd;
    printf("%s\\n", target_dir); // 模仿bash,打印切换后的目录
    } else {
    // cd 目录路径 → 切换到指定目录
    target_dir = g_argv[1];
    }

    // 调用chdir切换目录
    if (chdir(target_dir) == 1) {
    perror("cd failed");
    g_last_code = 1; // 退出码设为1(表示失败)
    } else {
    g_last_code = 0; // 退出码设为0(表示成功)
    }
    }

    // 3. 执行内建命令export
    static void exec_export() {
    if (g_argc != 2) {
    // 参数错误(如export无参数或多参数)
    fprintf(stderr, "用法:export KEY=VALUE\\n");
    g_last_code = 2;
    return;
    }
    // 检查参数格式(必须包含=,如MY_VAR=123)
    if (strchr(g_argv[1], '=') == NULL) {
    fprintf(stderr, "错误:export参数必须包含'='\\n");
    g_last_code = 2;
    return;
    }
    // 调用putenv添加环境变量
    if (putenv(g_argv[1]) != 0) {
    perror("export failed");
    g_last_code = 1;
    } else {
    g_last_code = 0;
    }
    }

    // 4. 执行内建命令env
    static void exec_env() {
    // 遍历environ数组,打印所有环境变量
    for (int i = 0; environ[i] != NULL; i++) {
    printf("%s\\n", environ[i]);
    }
    g_last_code = 0;
    }

    // 5. 执行内建命令echo
    static void exec_echo() {
    if (g_argc < 2) {
    // echo无参数 → 打印空行
    printf("\\n");
    g_last_code = 0;
    return;
    }

    char* content = g_argv[1];
    if (content[0] == '$') {
    // 场景1:echo $变量(如$PATH、$?)
    if (strcmp(content, "$?") == 0) {
    // echo $? → 打印上次命令的退出码
    printf("%d\\n", g_last_code);
    } else {
    // echo $环境变量 → 打印环境变量的值
    char* var_name = content + 1; // 跳过$,取变量名(如$PATH → PATH)
    char* var_value = getenv(var_name);
    if (var_value != NULL) {
    printf("%s\\n", var_value);
    }
    // 变量不存在时,不打印(模仿bash)
    }
    } else {
    // 场景2:echo 文本 → 打印文本(支持多参数,如echo hello world)
    for (int i = 1; i < g_argc; i++) {
    printf("%s ", g_argv[i]);
    }
    printf("\\n");
    }
    g_last_code = 0;
    }

    // 6. 统一执行内建命令
    void exec_builtin_command() {
    if (strcmp(g_argv[0], "cd") == 0) {
    exec_cd();
    } else if (strcmp(g_argv[0], "export") == 0) {
    exec_export();
    } else if (strcmp(g_argv[0], "env") == 0) {
    exec_env();
    } else if (strcmp(g_argv[0], "echo") == 0) {
    exec_echo();
    }
    }

    2.4 模块 4:外部命令执行 ——Shell “找帮手” 的命令

    外部命令(如ls/ps/gcc)是 “独立的可执行程序”,需要 Shell 创建子进程执行(避免覆盖 Shell 自身代码),核心流程是:

  • fork创建子进程;
  • 子进程用execvp替换为目标程序(自动从PATH查找命令路径);
  • 父进程用waitpid等待子进程退出,获取退出码(更新g_last_code);
  • 若execvp失败(如命令不存在),子进程退出并设置错误退出码。
  • 代码实现(外部命令模块):

    c

    void exec_external_command() {
    pid_t pid = fork();
    if (pid == 1) {
    // fork失败(如系统进程过多)
    perror("fork failed");
    g_last_code = 1;
    return;
    }

    if (pid == 0) {
    // 子进程:执行外部命令
    execvp(g_argv[0], g_argv);
    // 只有execvp失败时,才会执行到这里(成功则子进程代码被覆盖)
    perror("command not found"); // 错误原因:命令不存在、权限不足等
    exit(127); // 退出码127:标准的“命令未找到”错误码
    } else {
    // 父进程:等待子进程退出,获取退出码
    int status;
    waitpid(pid, &status, 0); // 阻塞等待子进程

    // 解析子进程的退出状态,更新g_last_code
    if (WIFEXITED(status)) {
    // 正常退出:获取退出码
    g_last_code = WEXITSTATUS(status);
    } else if (WIFSIGNALED(status)) {
    // 被信号终止(如Ctrl+C → SIGINT,kill -9 → SIGKILL)
    g_last_code = 128 + WTERMSIG(status); // 符合Linux标准(如128+2=130)
    }
    }
    }

    效果演示:

    用户输入ls -l,执行流程:

  • Shell 判断ls是外部命令,fork子进程;
  • 子进程调用execvp("ls", ["ls", "-l", NULL]),从PATH找到/bin/ls,替换为ls程序;
  • ls执行完毕后退出,父进程waitpid获取退出码(0 表示成功);
  • g_last_code更新为 0,下次执行echo $?会输出 0。
  • 2.5 模块 5:环境变量初始化 —— 继承父进程的 “配置”

    Shell 启动时,需要继承父进程的环境变量(如PATH/HOME/USER),这些环境变量存储在全局数组environ中(无需手动定义,只需用extern声明)。我们可以在 Shell 启动时,打印关键环境变量(可选),确保继承正常。

    代码实现(环境变量初始化):

    c

    // 初始化环境变量(打印关键变量,验证继承)
    void init_env() {
    printf("=== 迷你Shell启动 ===");
    // 打印关键环境变量(可选,用于调试)
    char* path = getenv("PATH");
    char* home = getenv("HOME");
    char* user = getenv("USER");
    if (path != NULL) printf("\\nPATH: %s", path);
    if (home != NULL) printf("\\nHOME: %s", home);
    if (user != NULL) printf("\\nUSER: %s", user);
    printf("\\n====================\\n\\n");

    // 初始化上次工作目录(启动时的当前目录)
    get_current_dir();
    strncpy(g_last_pwd, g_pwd, BUF_SIZE);
    }

    三、迷你 Shell 完整源码与运行演示

    将上述模块整合为完整代码(mini_shell.c),添加main函数实现 “无限循环” 的核心逻辑,再编译运行验证功能。

    3.1 完整源码

    c

    #include <stdio.h>
    #include <stdlib.h>
    #include <string.h>
    #include <unistd.h>
    #include <ctype.h>
    #include <sys/types.h>
    #include <sys/wait.h>

    // 全局常量定义
    #define BUF_SIZE 1024 // 命令缓冲区大小
    #define ARGV_MAX 64 // 命令参数最大个数

    // 全局变量定义
    char g_pwd[BUF_SIZE] = {0}; // 当前工作目录
    char g_last_pwd[BUF_SIZE] = {0}; // 上次工作目录(用于cd -)
    char* g_argv[ARGV_MAX] = {0}; // 命令参数数组
    int g_argc = 0; // 命令参数个数
    int g_last_code = 0; // 上次命令的退出码
    extern char **environ; // 全局环境变量数组

    // —————————— 模块1:命令行提示符 ——————————
    static char* get_username() {
    char* username = getenv("USER");
    return (username == NULL) ? "unknown" : username;
    }

    static char* get_hostname() {
    char* hostname = getenv("HOSTNAME");
    return (hostname == NULL) ? "unknown-host" : hostname;
    }

    static char* get_current_dir() {
    if (getcwd(g_pwd, BUF_SIZE) == NULL) {
    perror("getcwd failed");
    return "unknown-dir";
    }
    static char pwd_env[BUF_SIZE] = {0};
    snprintf(pwd_env, BUF_SIZE, "PWD=%s", g_pwd);
    putenv(pwd_env);
    return g_pwd;
    }

    static char* simplify_dir(char* full_dir) {
    if (full_dir == NULL || strcmp(full_dir, "/") == 0) {
    return "/";
    }
    char* last_slash = strrchr(full_dir, '/');
    return (last_slash == NULL) ? full_dir : (last_slash + 1);
    }

    void print_prompt() {
    char* username = get_username();
    char* hostname = get_hostname();
    char* full_dir = get_current_dir();
    char* simple_dir = simplify_dir(full_dir);
    printf("[%s@%s %s]$ ", username, hostname, simple_dir);
    fflush(stdout);
    }

    // —————————— 模块2:命令获取与解析 ——————————
    static void trim_space(char* str) {
    if (str == NULL) return;
    char* start = str;
    while (isspace(*start)) start++;
    char* end = str + strlen(str) 1;
    while (end >= start && isspace(*end)) end;
    *(end + 1) = '\\0';
    memmove(str, start, end start + 2);
    }

    int get_command(char* command_buf, int buf_size) {
    if (fgets(command_buf, buf_size, stdin) == NULL) {
    printf("\\n");
    return 1;
    }
    command_buf[strcspn(command_buf, "\\n")] = '\\0';
    trim_space(command_buf);
    if (strlen(command_buf) == 0) {
    return 0;
    }
    return 1;
    }

    void parse_command(char* command_buf) {
    memset(g_argv, 0, sizeof(g_argv));
    g_argc = 0;
    char* token = strtok(command_buf, " ");
    while (token != NULL && g_argc < ARGV_MAX 1) {
    g_argv[g_argc++] = token;
    token = strtok(NULL, " ");
    }
    g_argv[g_argc] = NULL;
    }

    // —————————— 模块3:内建命令处理 ——————————
    int is_builtin_command() {
    if (g_argc == 0 || g_argv[0] == NULL) return 0;
    const char* builtin_list[] = {"cd", "export", "env", "echo", NULL};
    for (int i = 0; builtin_list[i] != NULL; i++) {
    if (strcmp(g_argv[0], builtin_list[i]) == 0) {
    return 1;
    }
    }
    return 0;
    }

    static void exec_cd() {
    char* target_dir = NULL;
    strncpy(g_last_pwd, g_pwd, BUF_SIZE);

    if (g_argc == 1) {
    target_dir = getenv("HOME");
    } else if (strcmp(g_argv[1], "~") == 0) {
    target_dir = getenv("HOME");
    } else if (strcmp(g_argv[1], "-") == 0) {
    target_dir = g_last_pwd;
    printf("%s\\n", target_dir);
    } else {
    target_dir = g_argv[1];
    }

    if (chdir(target_dir) == 1) {
    perror("cd failed");
    g_last_code = 1;
    } else {
    g_last_code = 0;
    }
    }

    static void exec_export() {
    if (g_argc != 2) {
    fprintf(stderr, "用法:export KEY=VALUE\\n");
    g_last_code = 2;
    return;
    }
    if (strchr(g_argv[1], '=') == NULL) {
    fprintf(stderr, "错误:export参数必须包含'='\\n");
    g_last_code = 2;
    return;
    }
    if (putenv(g_argv[1]) != 0) {
    perror("export failed");
    g_last_code = 1;
    } else {
    g_last_code = 0;
    }
    }

    static void exec_env() {
    for (int i = 0; environ[i] != NULL; i++) {
    printf("%s\\n", environ[i]);
    }
    g_last_code = 0;
    }

    static void exec_echo() {
    if (g_argc < 2) {
    printf("\\n");
    g_last_code = 0;
    return;
    }

    char* content = g_argv[1];
    if (content[0] == '$') {
    if (strcmp(content, "$?") == 0) {
    printf("%d\\n", g_last_code);
    } else {
    char* var_name = content + 1;
    char* var_value = getenv(var_name);
    if (var_value != NULL) {
    printf("%s\\n", var_value);
    }
    }
    } else {
    for (int i = 1; i < g_argc; i++) {
    printf("%s ", g_argv[i]);
    }
    printf("\\n");
    }
    g_last_code = 0;
    }

    void exec_builtin_command() {
    if (strcmp(g_argv[0], "cd") == 0) {
    exec_cd();
    } else if (strcmp(g_argv[0], "export") == 0) {
    exec_export();
    } else if (strcmp(g_argv[0], "env") == 0) {
    exec_env();
    } else if (strcmp(g_argv[0], "echo") == 0) {
    exec_echo();
    }
    }

    // —————————— 模块4:外部命令执行 ——————————
    void exec_external_command() {
    pid_t pid = fork();
    if (pid == 1) {
    perror("fork failed");
    g_last_code = 1;
    return;
    }

    if (pid == 0) {
    execvp(g_argv[0], g_argv);
    perror("command not found");
    exit(127);
    } else {
    int status;
    waitpid(pid, &status, 0);
    if (WIFEXITED(status)) {
    g_last_code = WEXITSTATUS(status);
    } else if (WIFSIGNALED(status)) {
    g_last_code = 128 + WTERMSIG(status);
    }
    }
    }

    // —————————— 模块5:环境变量初始化 ——————————
    void init_env() {
    printf("=== 迷你Shell启动 ===");
    char* path = getenv("PATH");
    char* home = getenv("HOME");
    char* user = getenv("USER");
    if (path != NULL) printf("\\nPATH: %s", path);
    if (home != NULL) printf("\\nHOME: %s", home);
    if (user != NULL) printf("\\nUSER: %s", user);
    printf("\\n====================\\n\\n");

    get_current_dir();
    strncpy(g_last_pwd, g_pwd, BUF_SIZE);
    }

    // —————————— 主函数(核心循环) ——————————
    int main() {
    char command_buf[BUF_SIZE] = {0};
    init_env(); // 初始化环境变量

    // Shell核心循环:获取命令→解析→执行→循环
    while (1) {
    print_prompt(); // 1. 打印提示符
    int ret = get_command(command_buf, BUF_SIZE); // 2. 获取命令
    if (ret == 1) {
    // 用户按Ctrl+D,退出Shell
    printf("=== 迷你Shell退出 ===\\n");
    break;
    } else if (ret == 0) {
    // 空行,跳过
    continue;
    }

    parse_command(command_buf); // 3. 解析命令
    // debug_print_argv(); // 调试用:打印解析结果

    if (is_builtin_command()) { // 4. 执行内建命令
    exec_builtin_command();
    } else { // 5. 执行外部命令
    exec_external_command();
    }
    }

    return 0;
    }

    3.2 编译与运行演示

    步骤 1:编译代码

    在 Linux 终端中,执行编译命令:

    bash

    gcc mini_shell.c -o mini_shell -Wall

    • -o mini_shell:指定输出文件名为mini_shell;
    • -Wall:显示所有警告(避免潜在错误)。
    步骤 2:运行迷你 Shell

    bash

    ./mini_shell

    启动后会显示初始化信息和提示符:

    plaintext

    === 迷你Shell启动 ===
    PATH: /usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
    HOME: /home/ubuntu
    USER: ubuntu
    ====================

    [ubuntu@localhost myshell]$

    步骤 3:测试核心功能
  • 测试内建命令cd:

    bash

    [ubuntu@localhost myshell]$ cd /home
    [ubuntu@localhost home]$ cd ubuntu
    [ubuntu@localhost ubuntu]$ cd# 切换到上次目录(/home)
    /home
    [ubuntu@localhost home]$ cd ~ # 切换到家目录(/home/ubuntu)
    [ubuntu@localhost ubuntu]$

  • 测试内建命令export与echo:

    bash

    [ubuntu@localhost ubuntu]$ export MY_VAR=hello_mini_shell
    [ubuntu@localhost ubuntu]$ echo $MY_VAR # 打印环境变量
    hello_mini_shell
    [ubuntu@localhost ubuntu]$ echo $PATH # 打印系统环境变量
    /usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
    [ubuntu@localhost ubuntu]$ echo hello world # 打印文本
    hello world

  • 测试内建命令env:

    bash

    [ubuntu@localhost ubuntu]$ env | grep MY_VAR # 查看导出的环境变量
    MY_VAR=hello_mini_shell

  • 测试外部命令ls与ps:

    bash

    [ubuntu@localhost ubuntu]$ ls -l # 执行外部命令ls
    total 4
    drwxrwxr-x 2 ubuntu ubuntu 4096 Oct 1 16:00 myshell
    [ubuntu@localhost ubuntu]$ ps # 执行外部命令ps
    PID TTY TIME CMD
    1234 pts/0 00:00:00 bash
    5678 pts/0 00:00:00 mini_shell
    5679 pts/0 00:00:00 ps

  • 测试echo $?(查看退出码):

    bash

    [ubuntu@localhost ubuntu]$ ls -l # 成功执行,退出码0
    [ubuntu@localhost ubuntu]$ echo $?
    0
    [ubuntu@localhost ubuntu]$ lss # 命令不存在,退出码127
    command not found: lss
    [ubuntu@localhost ubuntu]$ echo $?
    127

  • 退出迷你 Shell:按Ctrl+D或输入exit(可扩展exit内建命令),Shell 会退出:

    bash

    [ubuntu@localhost ubuntu]$
    === 迷你Shell退出 ===

  • 四、进阶扩展:让迷你 Shell 更实用

    当前的迷你 Shell 已实现核心功能,但还可以扩展以下进阶功能,使其更接近真实 Shell(如 bash):

    4.1 扩展 1:支持命令别名(如ll=ls -l)

    • 实现思路:用哈希表(如struct alias_map)存储 “别名→原命令” 的映射;
    • 新增内建命令alias:alias ll='ls -l',将别名添加到哈希表;
    • 命令解析时:若命令是别名,替换为原命令(如ll→ls -l)。

    4.2 扩展 2:支持命令补全(按 Tab 补全命令 / 路径)

    • 实现思路:监听Tab键(需关闭终端的 “行缓冲”,用tcsetattr修改终端属性);
    • 补全逻辑:若输入的是命令前缀(如l),遍历PATH目录,找出以l开头的命令(如ls/less);若输入的是路径前缀(如/hom),调用readdir遍历目录,补全为/home。

    4.3 扩展 3:支持重定向(如ls > file.txt)

    • 实现思路:解析命令时识别>/<等重定向符号,拆分 “命令” 与 “重定向目标”;
    • 子进程执行前:调用open打开目标文件,用dup2重定向标准输出(stdout)或标准输入(stdin),再执行execvp。

    4.4 扩展 4:支持管道(如ls | grep txt)

    • 实现思路:用pipe创建管道,fork两个子进程:
      • 子进程 1:执行ls,将标准输出重定向到管道写入端;
      • 子进程 2:执行grep txt,将标准输入重定向到管道读取端;
      • 父进程:等待两个子进程退出,关闭管道两端。

    五、总结:迷你 Shell 背后的 “进程控制逻辑”

    这个迷你 Shell 虽然简单,但完全基于前面四篇文章的核心知识,是进程控制的 “集大成者”。我们可以用一张图总结其核心逻辑:

    plaintext

    用户输入 → 提示符(print_prompt)

    获取命令(get_command)→ 空行/EOF处理

    解析命令(parse_command)→ 生成g_argv数组

    判断命令类型:
    ├─ 内建命令(is_builtin_command)→ Shell自身执行(如cd修改工作目录)
    └─ 外部命令 → fork子进程 → 子进程execvp替换 → 父进程waitpid回收

    更新退出码(g_last_code)→ 回到提示符,循环

    通过这个实战,你应该能深刻理解:

    • 内建命令与外部命令的本质区别:是否需要修改 Shell 自身状态;
    • 进程控制的 “全链路” 应用:fork(创建子进程)→ exec(替换程序)→ waitpid(回收资源)→ exit(终止进程);
    • Linux Shell 的工作原理:不是 “自己执行命令”,而是 “管理子进程执行命令” 的 “管家”。

    至此,Linux 进程控制系统系列文章已全部完成。从进程的创建、终止、等待、替换,到最终实现迷你 Shell,我们走过了 “理论→实践” 的完整路径。希望你能亲手运行代码,修改扩展,真正将这些知识内化为自己的技能。

    赞(0)
    未经允许不得转载:171主机测评 » 【Linux指南】进程控制系列(五)实战 —— 微型 Shell 命令行解释器实现
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址