
这篇文章会带你基于我们前面文章自建的已有 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:扩展命令行解析 —— 识别重定向符号)
- [四、实现步骤 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 |
cat从input.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 内建命令的特殊处理
内建命令(如cd、echo)由父进程直接执行(不创建子进程),如果要支持内建命令的重定向(如echo "test" > log.txt),需要:
- 执行重定向前,保存父进程的原始 fd(如用
dup(1)保存 stdout); - 执行内建命令(此时输出会被重定向);
- 执行完后,恢复原始 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.txt→ls -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_type和g_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;
}
关键逻辑说明:
- 内建命令的 fd 保存 :用
dup保存父进程的0/1/2(stdin/stdout/stderr),后续执行完内建命令后恢复; - open 的 flags 参数 :
- 输入重定向(<):
O_RDONLY(只读); - 输出重定向(>):
O_WRONLY | O_CREAT | O_TRUNC(只写 + 创建 + 覆盖); - 追加重定向(>>):
O_WRONLY | O_CREAT | O_APPEND(只写 + 创建 + 追加);
- 输入重定向(<):
- dup2 的调用 :将 "文件 fd" 复制到 "标准流 fd"(如
dup2(fd, 1)让 stdout 指向文件),复制后关闭临时 fd(避免 fd 泄漏); - 错误合并(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_builtin传false,无需保存原始 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 重定向到管道的读取端"。实现思路:
- 用
pipe创建管道(得到读端 fd 和写端 fd); fork两个子进程,分别执行前序命令和后序命令;- 前序命令:
dup2(pipe_write_fd, 1)(stdout→管道写端); - 后序命令:
dup2(pipe_read_fd, 0)(stdin→管道读端); - 父进程关闭管道 fd,等待两个子进程退出。
8.2 扩展 2:支持后台运行(&)
后台运行(如ls -l &)的本质是 "父进程不等待子进程退出,直接返回提示符"。实现思路:
- 解析命令时识别末尾的
&符号,标记为后台运行; - 执行外部命令时,若为后台运行,父进程不调用
waitpid,直接继续循环显示提示符; - 维护一个后台进程列表,定期回收僵尸进程(用
waitpid(-1, &status, WNOHANG))。
8.3 扩展 3:支持命令别名(alias)
命令别名(如alias ll='ls -l')的本质是 "维护一个别名→原命令的映射表"。实现思路:
- 新增内建命令
alias,将别名和原命令存入哈希表; - 在
ParseCommandLine中,若命令是别名,替换为原命令(如ll→ls -l)。
九、总结:重定向实现的核心收获
通过本次实战,你不仅为微型 Shell 添加了重定向功能,更重要的是打通了 "进程控制" 与 "基础 IO" 的知识链路:
- 重定向的本质 :修改 fd 对应的
struct file指针,通过dup2实现 fd 的 "指向替换"; - 子进程与父进程的 IO 隔离:外部命令的重定向在子进程中执行,避免污染父进程;内建命令的重定向需保存并恢复原始 fd;
- 命令解析的扩展思路:从后往前遍历识别特殊符号,确保解析的准确性和无歧义;
- 实战能力提升:将内核原理(fd、struct file)转化为实际代码,解决真实场景中的 IO 流向控制问题。
至此,Linux 基础 IO 系列文章已全部完成。从 "文件本质" 到 "系统 IO",从 "文件描述符" 到 "重定向",再到 "Shell 重定向实战",我们走过了 "理论→原理→实战" 的完整路径。希望你能亲手运行代码,修改扩展,真正将这些知识内化为自己的技能。