
输出重定向、dup2 与 minishell 重定向实战
一、输出重定向:先看现象
1.1 关闭 1 号后打开文件
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdlib.h>
int main()
{
close(1); // ① 关闭标准输出
int fd = open("myfile", O_WRONLY|O_CREAT, 00644); // ② 打开文件
if(fd < 0){
perror("open");
return 1;
}
printf("fd: %d\\n", fd); // ③ 打印 fd
fflush(stdout); // ④ 冲刷 stdout 缓冲
close(fd);
exit(0);
}
运行发现:
- 屏幕上什么都没显示;
- 打开 myfile 一看——fd: 1 竟然写进了文件里!
现象:
本来应该输出到显示器上的内容,输出到了文件 myfile 当中(fd=1)。
这种现象叫做输出重定向。
shell 里常见的重定向有:>(覆盖)、>>(追加)、<(输入)。
1.2 为什么 printf 的内容跑进了文件?
回顾上一篇的 fd 分配规则:找到 fd_array 中最小未使用的下标。
初始: [0]=标准输入 [1]=标准输出 [2]=标准错误 [3]=空 …
close(1):[1]=空 !
open("myfile") → 最小未用下标 = 1 → fd = 1
关键一步发生了:
文件描述符表里 1 号下标原本指向"显示器文件"的 file 对象,现在被改写成了指向 myfile 的 file 对象。
task_struct files_struct file 对象
┌────────────────────┐ ┌──────────────────┐ ┌──────────────────┐
│ … │ │ file* fd_array[]│ │ │
│ struct files* │───────►│ ┌──────────────┐ │ │ 标准输入(键盘) │
│ files │ │ │ 0 │ file* │─┼───►│ 显示器(原标准输出)│
└────────────────────┘ │ │ 1 │ file* │─┼───►│ myfile(新打开) │
│ │ 2 │ file* │─┼───►│ 标准错误(显示器) │
│ │ 3 │ file* │─┼───►│ … │
│ └──────────────┘ │ └──────────────────┘
└──────────────────┘
▲
1 号下标从"显示器"被改指到"myfile"
1.3 重定向的本质(一句话)
凡是往 1 号文件描述符写的内容,都写到了 myfile 当中,而不再写到标准输出!
- 重定向不是"把数据从屏幕挪到文件"这种玄学操作;
- 本质是修改文件描述符表里某个下标的指向——把 1 号(或 0 号)下标从默认设备改指到目标文件;
- 之后所有对 fd=1 的写操作(printf、write(1,…) 等),自然就写进文件了。

二、dup2 系统调用
2.1 为什么还需要 dup2?
close(1) + open 虽然能完成重定向,但先关再开有窗口期(1 号临时空置,中间若出错整个 stdout 就没了),而且写法繁琐。
Linux 提供了一个"一步到位"的系统调用:dup2。
2.2 函数原型
#include <unistd.h>
int dup2(int oldfd, int newfd);
语义:把 oldfd 指向的 file 对象,拷贝/复制到 newfd 这个下标上。
dup2(fd, 1):
让 1 号下标也指向 fd 所指向的 file 对象
→ 之后写 1 号 = 写 fd 指向的文件
和 close+open 的区别:
| close(1) + open | 先关 1,再 open 抢占 1 | 1 号有短暂空窗期,步骤繁琐 |
| dup2(fd, 1) | 直接把 fd 的指向复制给 1 | 一步完成,安全可靠 |
类比:
close+open 是"先辞职再入职",中间岗位空着;
dup2 是"一个人干两份活,把岗位直接挂到同一个员工名下"。
2.3 完整示例:把键盘输入回显到日志文件
#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>
int main() {
int fd = open("./log", O_CREAT | O_RDWR);
if (fd < 0) {
perror("open");
return 1;
}
close(1); // ① 关闭标准输出
dup2(fd, 1); // ② 让 1 号指向 fd(log 文件)
for (;;) { // ③ 循环读键盘
char buf[1024] = {0};
ssize_t read_size = read(0, buf, sizeof(buf) – 1);
if (read_size < 0) {
perror("read");
break;
}
printf("%s", buf); // ④ printf 输出 → 进 log 文件!
fflush(stdout); // ⑤ 冲刷缓冲
}
return 0;
}
效果:你在终端敲的内容,不再显示在屏幕上,而是全部写进了 log 文件。
2.4 printf 为什么也跟着走了?——stdio 与 fd 的关系
printf 是 C 库的 IO 函数,一般往 stdout 中输出;
但 stdout 底层访问文件的时候,找的还是 fd:1;
而此时 fd:1 下标所指向的内容,已经变成了 log 文件的地址,不再是显示器文件的地址;
所以,输出的任何消息都会往文件中写入,进而完成输出重定向。
核心结论:
printf → stdout → fd:1 → fd_array[1] 指向谁,输出就进谁。
库函数再高级,最终都逃不过"往 fd 写"这一层——重定向能截住 printf 的输出,就是因为改的是 fd 表。
2.5 追加(>>)和输入(<)怎么完成?
- 输出重定向 >:open(file, O_CREAT|O_WRONLY|O_TRUNC) + dup2(fd, 1)(覆盖写);
- 追加重定向 >>:open(file, O_CREAT|O_WRONLY|O_APPEND) + dup2(fd, 1)(每次写都到末尾);
- 输入重定向 <:open(file, O_RDONLY) + dup2(fd, 0)(让 0 号指向文件,read(0,…) 就从文件读)。
三个符号的本质完全一致:
open 目标文件 → dup2 把目标文件的指向复制到 0 或 1 号下标。
区别只在 open 的 flags 和 dup2 的目标下标。
三、3-6-5 在 minishell 中添加重定向功能
目标:
让上一篇写的 mini_shell 支持 ls -a -l > file.txt、cat < file.txt、echo hello >> log。
核心新增两个函数:
ParseRedir(解析重定向符号)和 DoRedir(子进程里执行重定向)。
3.1 全局设计:重定向状态
// 全局变量与重定向有关
#define NoneRedir 0 // 没有重定向
#define InputRedir 1 // 输入重定向 <
#define OutputRedir 2 // 输出重定向 >
#define AppRedir 3 // 追加输出重定向 >>
int redir = NoneRedir; // 当前命令的重定向类型
char *filename = nullptr; // 重定向目标文件名
3.2 TrimSpace:去掉首尾空格的小工具(宏)
// 跳过字符串前导空白字符
#define TrimSpace(pos) do{ \\
while(isspace(*pos)){ \\
pos++; \\
} \\
}while(0)
3.3 ParseRedir:从命令末尾往前找重定向符号
设计思路:
重定向符号在命令行的末尾(ls -a -l > file.txt),所以从后往前扫描,找到第一个 <、>、>>,把它和它前面的命令用 \\0 隔开,并把后面的部分当作文件名。
void ParseRedir(char command_buffer[], int len)
{
int end = len – 1; // 从缓冲区末尾开始
while(end >= 0)
{
if(command_buffer[end] == '<') // ① 输入重定向 <
{
redir = InputRedir;
command_buffer[end] = 0; // 把 < 替换成 \\0,命令和文件分开
filename = &command_buffer[end] + 1; // 文件名 = < 后面
TrimSpace(filename); // 去掉文件名前导空格
break;
}
else if(command_buffer[end] == '>') // ② 输出重定向 > 或 >>
{
if(command_buffer[end-1] == '>') // 前一个还是 > → 追加
{
redir = AppRedir;
command_buffer[end] = 0; // 把 >> 两个位置都置 0
command_buffer[end-1] = 0;
filename = &command_buffer[end]+1; // 文件名 = >> 后面
TrimSpace(filename);
break;
}
else // 只有一个 > → 覆盖
{
redir = OutputRedir;
command_buffer[end] = 0;
filename = &command_buffer[end]+1;
TrimSpace(filename);
break;
}
}
else
{
end–; // 没找到就往前继续找
}
}
}
举例:输入 ls -a -l > file.txt
原缓冲区: "ls -a -l > file.txt"
从末尾扫描 → 找到 '>'(前面不是 '>',是空格)→ OutputRedir
置 0 后: "ls -a -l \\0 file.txt"
filename = "file.txt"(TrimSpace 去前导空格)
之后 ParseCommand 再用 strtok 分割 "ls -a -l \\0" 时,只会得到 ls、-a、-l 三个参数——重定向部分已经被干净地切掉了。
3.4 ParseCommand:按空格分割命令
void ParseCommand(char command_buffer[])
{
// "ls -a -l -n"
const char *sep = " ";
gargv[gargc++] = strtok(command_buffer, sep); // 第一个参数
// =是刻意写的(把赋值结果转 bool 再判断)
while((bool)(gargv[gargc++] = strtok(nullptr, sep)));
gargc–; // 最后一次 strtok 返回 nullptr 被多算了一次
}
3.5 ParseCommandLine:总入口
void ParseCommandLine(char command_buffer[], int len) // 3. 分析命令
{
ResetCommandline(); // 清空上一轮状态
ParseRedir(command_buffer, len); // 先解析重定向
ParseCommand(command_buffer); // 再解析命令
// "ls -a -l -n"
// "ls -a -l -n" > file.txt
// "ls -a -l -n" < file.txt
// "ls -a -l -n" >> file.txt
}
3.6 DoRedir:真正的重定向执行(核心)
两条铁律:
1. 重定向应该让子进程自己做!
2. 程序替换会不会影响重定向? 不会
为什么必须子进程做?
因为重定向会永久修改进程的 fd 表。
如果在 shell 主进程里 dup2,shell 自己的 stdout 就没了,再也无法打印提示符——所以必须在 fork 出来的子进程里做,父进程(shell)的 fd 表毫发无损。
为什么 exec 不影响?
fd 表属于进程资源,程序替换(exec)只换代码和数据,不换 fd 表——所以子进程先重定向、再 exec,新程序的输出自然进文件。
void DoRedir()
{
// 1. 重定向应该让子进程自己做!
// 2. 程序替换会不会影响重定向? 不会
// 0. 先判断 && 重定向
if(redir == InputRedir) // ① 输入重定向 <
{
if(filename)
{
int fd = open(filename, O_RDONLY);
if(fd < 0)
{
exit(2); // 打开输入文件失败
}
dup2(fd, 0); // 0 号指向文件 → read(0) 从文件读
}
else
{
exit(1);
}
}
else if(redir == OutputRedir) // ② 输出重定向 >
{
if(filename)
{
int fd = open(filename, O_CREAT | O_WRONLY | O_TRUNC, 0666);
if(fd < 0)
{
exit(4); // 创建输出文件失败
}
dup2(fd, 1); // 1 号指向文件 → printf/write(1) 写文件(覆盖)
}
else
{
exit(3);
}
}
else if(redir == AppRedir) // ③ 追加输出重定向 >>
{
if(filename)
{
int fd = open(filename, O_CREAT | O_WRONLY | O_APPEND, 0666);
if(fd < 0)
{
exit(6); // 打开追加文件失败
}
dup2(fd, 1); // 1 号指向文件,O_APPEND 保证每次写都在末尾
}
else
{
exit(5);
}
}
else
{
// 没有重定向, Do Nothing!
}
}
三种重定向对照表:
| < | InputRedir | O_RDONLY |
dup2 (fd, 0) |
键盘输入 → 文件输入 |
| > | OutputRedir | O_CREAT|O_WRONLY|O_TRUNC |
dup2 (fd, 1) |
屏幕输出 → 文件(覆盖) |
| >> | AppRedir | O_CREAT|O_WRONLY|O_APPEND |
dup2 (fd, 1) |
屏幕输出 → 文件(追加) |
3.7 与执行流程的衔接
// fork 后的子进程里,exec 之前调用 DoRedir:
void ExecuteCommand()
{
pid_t id = fork();
if(id == 0)
{
// 子进程
DoRedir(); // ① 先做重定向(改子进程自己的 fd 表)
execvp(gargv[0], gargv); // ② 再程序替换(fd 表不受影响)
exit(127); // ③ exec 失败
}
// 父进程:waitpid 回收子进程,继续打印提示符
}
shell 主循环:
打印提示符 → fgets 读命令行 → ParseCommandLine(解析出 gargv/redir/filename)
→ fork
├─ 子进程:DoRedir() → execvp() → 新程序输出自动进文件
└─ 父进程:waitpid → 继续下一轮循环
完整闭环:
ls -a -l > file.txt 敲下去 → ParseRedir 切出命令和文件名 → fork → 子进程 DoRedir(open file + dup2(fd,1))→ execvp("ls") → ls 的 stdout 全部写进 file.txt → 父进程回收,提示符照常打印。
四、本篇小结
#include <iostream>
#include <ctype.h>
#include <cstdio>
#include <cstring>
#include <cstdlib>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <cstring>
#include <unordered_map>
#include <sys/stat.h>
#include <fcntl.h>
#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;
// 4. 关于重定向,我们关心的内容
#define NONE_REDIR 0
#define INPUT_REDIR 1
#define OUTPUT_REDIR 2
#define APPEND_REDIR 3
————————————遇到空格跳过—————————————
//#define TrimSpace(CMD,index) do{
// while(isspace(*(CMD+index)))
// {
// index++;
// }
//}while(0)
int redir = NONE_REDIR;
std::string filename;
// for test
char cwd[1024];
char cwdenv[2048];
// 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()
{
//如果内键命令做重定向,更改shell的标准输入,输出,错误
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)
{
int fd = -1;
// 子进程检测重定向情况
if(redir == INPUT_REDIR)
{
fd = open(filename.c_str(), O_RDONLY);
if(fd < 0) exit(1);
dup2(fd,0);
close(fd);
}
else if(redir == OUTPUT_REDIR)
{
fd = open(filename.c_str(), O_CREAT | O_WRONLY | O_TRUNC, 0666);
if(fd < 0) exit(2);
dup2(fd, 1);
close(fd);
}
else if(redir == APPEND_REDIR)
{
fd = open(filename.c_str(), O_CREAT | O_WRONLY | O_APPEND, 0666);
if(fd < 0) exit(2);
dup2(fd, 1);
close(fd);
}
else
{}
// 进程替换,会影响重定向的结果吗?不影响
//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;
}
void TrimSpace(char cmd[], int &end)
{
while(isspace(cmd[end]))
{
end++;
}
}
void RedirCheck(char cmd[])
{
redir = NONE_REDIR;
filename.clear();
int start = 0;
int end = strlen(cmd)-1;
//"ls -a -l >> file.txt" > >> <
while(end > start)
{
if(cmd[end] == '<')
{
cmd[end++] = 0;
TrimSpace(cmd, end);
redir = INPUT_REDIR;
filename = cmd+end;
break;
}
else if(cmd[end] == '>')
{
if(cmd[end-1] == '>')
{
//>>
cmd[end-1] = 0;
redir = APPEND_REDIR;
}
else
{
//>
redir = OUTPUT_REDIR;
}
cmd[end++] = 0;
TrimSpace(cmd, end);
filename = cmd+end;
break;
}
else
{
end–;
}
}
}
int main()
{
// shell 启动的时候,从系统中获取环境变量
// 我们的环境变量信息应该从父shell统一来
InitEnv();
while(true)
{
// 1. 输出命令行提示符
PrintCommandPrompt();
// 2. 获取用户输入的命令
char commandline[COMMAND_SIZE];
if(!GetCommandLine(commandline, sizeof(commandline)))
continue;
// 3. 重定向分析 "ls -a -l > file.txt" -> "ls -a -l" "file.txt" -> 判定重定向方式
RedirCheck(commandline);
// printf("redir: %d, filename: %s\\n", redir, filename.c_str());
// 4. 命令行分析 "ls -a -l" -> "ls" "-a" "-l"
if(!CommandParse(commandline))
continue;
//PrintArgv();
// 检测别名
// 5. 检测并处理内键命令
if(CheckAndExecBuiltin())
continue;
// 6. 执行命令
Execute();
}
//cleanup();
return 0;
}
1. 输出重定向现象:close(1) 后 open → fd=1 → 本应上屏的内容写进文件
2. 重定向本质 = 修改 fd 表下标的指向(1 号从显示器改指到 myfile)
3. dup2(oldfd, newfd):把 oldfd 的指向复制给 newfd,一步完成重定向
4. printf → stdout → fd:1 → 指向谁就写谁(库函数逃不出 fd 层)
5. 三种符号统一套路:open + dup2
< O_RDONLY dup2(fd, 0)
> O_CREAT|O_WRONLY|O_TRUNC dup2(fd, 1)
>> O_CREAT|O_WRONLY|O_APPEND dup2(fd, 1)
6. minishell 实现:
ParseRedir 从末尾扫 < > >>,置 0 切出 filename
DoRedir 在 fork 后的子进程里 open + dup2(必须子进程做!)
exec 不破坏 fd 表 → 先重定向再程序替换
7. 至此 Linux 基础IO 全部收尾:文件概念 → C 接口 → 系统调用 → fd → 重定向/dup2/minishell
面试题
一、重定向原理
Q1. 什么是输出重定向?用 close+open 怎么实现?
A:
本来输出到显示器的内容输出到了文件。
实现:
close(1) 关闭标准输出,再 open("myfile", O_WRONLY|O_CREAT),根据 fd 分配规则(最小未用下标),新文件抢到下标 1,之后所有往 1 号写的内容(printf/write)都进文件。
Q2. 重定向的本质是什么?
A:
不是搬数据,而是修改文件描述符表里某个下标的指向——把 1 号(或 0 号)下标从默认设备改指到目标文件的 file 对象。
之后对该 fd 的读写自然落到新文件上。
Q3. 为什么 printf 的内容也会被重定向带走?
A:
printf 是 C 库函数,往 stdout 输出,但 stdout 底层访问时找的还是 fd:1。
当 fd:1 下标已指向目标文件而非显示器时,printf 输出的任何消息都会写进文件。
库函数最终都落到"往 fd 写"这一层,改 fd 表就能截住一切输出。
二、dup2
Q4. dup2 的函数原型和语义?
A:
int dup2(int oldfd, int newfd);(unistd.h)。
把 oldfd 所指向的 file 对象复制到 newfd 下标上,之后 newfd 和 oldfd 指向同一个打开文件。
Q5. close(1)+open 和 dup2 的区别?
A:
close+open 是先关 1 再 open 抢占下标,1 号有短暂空窗期、步骤繁琐;
dup2(fd,1) 直接把 fd 的指向复制给 1 号,一步完成、安全可靠,是重定向的首选方案。
Q6. 为什么重定向后 write(1,…) 和 printf 都进文件?
A:
两者最终都通过 fd:1 访问文件:
write 是直接系统调用写 fd 1;
printf 是库函数写 stdout,stdout 底层也找 fd:1。
fd 表里 1 号指向谁,两者就写谁。
Q7. 输出重定向 >、追加重定向 >>、输入重定向 < 的 open flags 和 dup2 目标分别是什么?
A:
> :O_CREAT|O_WRONLY|O_TRUNC,dup2(fd,1)(覆盖写);
>> :O_CREAT|O_WRONLY|O_APPEND,dup2(fd,1)(追加写);
< :O_RDONLY,dup2(fd,0)(文件当键盘)。
区别只在 flags 和 0/1 下标。
Q8. O_TRUNC 和 O_APPEND 在重定向里的作用?
A:
O_TRUNC 打开瞬间清空文件(> 覆盖);
O_APPEND 每次写都定位到末尾(>> 追加)。
这就是 > 和 >> 在文件内容上的本质差异。
三、minishell 重定向实现
Q9. ParseRedir 为什么从命令缓冲区末尾往前扫描?
A:
重定向符号位于命令行末尾(如 ls -a -l > file.txt),从末尾往前找第一个 <、>、>> 效率最高,找到后把符号位置置 0,与命令隔开,符号后面的部分就是文件名。
Q10. ParseRedir 怎么区分 > 和 >>?
A:
找到 > 后检查它前一个字符:command_buffer[end-1] == '>' 则是追加(AppRedir),两个位置都置 0;
否则是覆盖(OutputRedir),只置一个位置。
Q11. 为什么重定向必须让子进程做?
A:
重定向会永久修改进程的 fd 表。
若在 shell 主进程做 dup2,shell 自己的 stdout 就被改没了,无法再打印提示符;
在 fork 出的子进程里做,只影响子进程,父进程 fd 表毫发无损。
Q12. 为什么 exec 不影响已经做好的重定向?
A:
fd 表属于进程资源,程序替换(exec)只换代码和数据段,不换 fd 表。
所以子进程先 DoRedir 再 execvp,新程序的输出依然进目标文件。
Q13. DoRedir 里 exit 的不同错误码有什么用?
A:
区分失败原因:
输入文件打开失败 exit(2)、未给文件名 exit(1)/exit(3)/exit(5)、输出文件创建失败 exit(4)、追加文件打开失败 exit(6)——父进程 waitpid 后可通过 WEXITSTATUS 拿到具体错误。
Q14. ls -a -l > file.txt 在 minishell 中的完整执行链路?
A:
fgets 读命令行 → ParseRedir(置 0 切出 file.txt,redir=OutputRedir)→ ParseCommand(strtok 得 ls/-a/-l)→ fork → 子进程 DoRedir(open(file,O_CREAT|O_WRONLY|O_TRUNC) + dup2(fd,1))→ execvp("ls") → ls 输出全部进 file.txt → 父进程 waitpid 后继续打印提示符。
Q15. ParseCommand 里 while((bool)(gargv[gargc++] = strtok(nullptr, sep))); 后为什么要 gargc–?
A:
最后一次 strtok 返回 nullptr 时,gargc++ 已经执行(多算了一次),退出循环后需要减 1 让 gargc 等于真实参数个数。
Q16. TrimSpace 是干什么的?为什么文件名要 TrimSpace?
A:
跳过字符串前导空白字符(isspace)。
因为 ls -a -l > file.txt 里 > 后面有空格,直接取 &command_buffer[end]+1 会得到 " file.txt",不去空格 open 会失败。
四、综合
Q17. stdin/stdout/stderr 与 0/1/2 的关系?
A:
C 标准库的 stdin/stdout/stderr 是 FILE* 流,底层分别绑定文件描述符 0/1/2。
重定向改 fd 表后,stdio 流跟着变——因为它们访问的 fd 指向变了。
Q18. 重定向和管道有什么区别?(提前铺垫)
A:
重定向是把 fd 指向文件(>/< 目标是普通文件);
管道是把一个进程的 stdout 接到另一个进程的 stdin(目标是管道文件,pipe() 创建)。
本质都是改 fd 表指向,只是目标对象不同——一个是文件,一个是管道。
Q19. 为什么说"一切皆文件"让重定向变得简单?
A:
因为显示器、文件、管道都是"文件",fd 表里放的都是 file 对象指针。
重定向只需把下标指向从"显示器文件"换成"目标文件",无需任何特殊处理——统一抽象是重定向能如此优雅的前提。
Q20. 手撕:写出 DoRedir 中 > 的分支。
A:
else if(redir == OutputRedir)
{
if(filename)
{
int fd = open(filename, O_CREAT | O_WRONLY | O_TRUNC, 0666);
if(fd < 0) exit(4);
dup2(fd, 1);
}
else exit(3);
}





