Linux中添加重定向(Redirection)功能到minishell
前言:在谈论添加minishell之前,我再重谈一下重定向的具体实现等大概思想!!!方便自己回顾!!!
目录
一、重定向(Redirection)原理详解
1、文件描述符基础
2、重定向的底层实现
2.1 系统调用层面
2.2 Shell 实现原理
3、不同类型重定向的机制
3.1 输出重定向 (>)
3.2 追加重定向 (>>)
3.3 输入重定向 (<)
3.4 错误重定向 (2>)
4、高级重定向原理
4.1 文件描述符复制 (n>&m)
4.2 管道重定向 (|)(先了解即可)
5、内核处理流程(重要!!!)
6、特殊文件与设备
7、性能考虑
二、在myshell中添加重定向功能
1、实现代码如下
2、Shell 实现代码详细解析
1. 头文件包含部分
2. 宏定义和全局变量
3. 系统信息获取函数
3.1 获取用户名
3.2 获取主机名
3.3 获取当前工作目录
3.4 获取家目录
4. 环境变量初始化
5. 内建命令实现
5.1 cd命令
5.2 echo命令
6. 命令行处理函数
6.1 生成提示符
6.2 获取用户输入
6.3 命令解析
6.4 重定向检查
1. TrimSpace 函数
功能:
参数:
工作原理:
作用:
2. RedirCheck 函数
功能:
参数:
全局变量影响:
详细工作流程:
示例分析:
7. 命令执行
7.1 内建命令检查
7.2 执行外部命令
1. 函数总体结构
2. 进程创建 (fork)
3. 子进程处理部分
3.1 重定向处理
输入重定向 (<)
重要结论
输出重定向 (>)
追加重定向 (>>)
3.2 命令执行
4. 父进程处理部分
5. 错误处理
6. 关键系统调用说明
7. 完整执行流程示例
8. 主函数
三、总结
一、重定向(Redirection)原理详解
在 Linux 系统中,重定向是一种强大的 I/O 控制机制,它允许用户改变命令的输入来源和输出目标。理解其底层原理对于高效使用 Linux 系统至关重要。
1、文件描述符基础
核心概念:
-
每个 Linux 进程启动时都会自动打开三个文件描述符(File Descriptor):
-
0 (STDIN):标准输入
-
1 (STDOUT):标准输出
-
2 (STDERR):标准错误输出
-
-
文件描述符是内核为每个进程维护的非负整数索引表,指向系统级的打开文件表条目
内核数据结构关系:
2、重定向的底层实现
2.1 系统调用层面
重定向主要通过以下系统调用实现:
-
open():打开文件获取文件描述符
-
dup()/dup2():复制文件描述符
-
close():关闭文件描述符
-
fork() 和 exec():创建新进程并执行程序
2.2 Shell 实现原理
当 Shell 解析到重定向符号时:
-
解析阶段:
-
Shell 识别重定向符号(>, <, >>等)
-
确定需要重定向的文件描述符(默认为0或1)
-
-
准备阶段:
-
对目标文件执行 open() 系统调用
-
使用 dup2() 复制文件描述符
-
-
执行阶段:
-
使用 fork() 创建子进程
-
在子进程中执行 dup2() 完成重定向
-
调用 exec() 执行目标命令
-
3、不同类型重定向的机制
3.1 输出重定向 (>)
实现流程:
-
打开或创建目标文件(O_WRONLY|O_CREAT|O_TRUNC)
-
使用 dup2(fd, STDOUT_FILENO) 将标准输出重定向到文件
-
关闭原始的标准输出文件描述符
示例代码等价:
int fd = open("file", O_WRONLY|O_CREAT|O_TRUNC, 0644);
dup2(fd, STDOUT_FILENO);
close(fd);
execvp(cmd, args);
3.2 追加重定向 (>>)
与>类似,但使用不同的打开标志:
int fd = open("file", O_WRONLY|O_CREAT|O_APPEND, 0644);
3.3 输入重定向 (<)
实现流程:
-
打开源文件(O_RDONLY)
-
使用 dup2(fd, STDIN_FILENO)
-
关闭原始的标准输入文件描述符
3.4 错误重定向 (2>)
使用 dup2() 将文件描述符2重定向:
dup2(fd, STDERR_FILENO);
4、高级重定向原理
4.1 文件描述符复制 (n>&m)
dup2() 工作原理:(先了解即可)
-
关闭文件描述符n(如果已打开)
-
使n成为m的副本,指向同一个打开文件表项
-
两个描述符共享文件偏移量和状态标志
4.2 管道重定向 (|)(先了解即可)
实现机制:
-
使用 pipe() 系统调用创建匿名管道(返回两个文件描述符)
-
pipefd[0]:读取端
-
pipefd[1]:写入端
-
-
第一个进程将 STDOUT 重定向到 pipefd[1]
-
第二个进程将 STDIN 重定向到 pipefd[0]
5、内核处理流程(重要!!!)
-
进程创建时:
-
继承父进程的文件描述符表
-
默认打开0,1,2指向终端设备
-
-
重定向发生时:
-
修改进程的文件描述符表
-
不改变系统级的打开文件表
-
-
I/O操作时:
-
通过文件描述符索引到打开文件表
-
最终访问实际文件或设备
-
6、特殊文件与设备
-
/dev/null:黑洞设备,丢弃所有写入的数据
-
/dev/zero:提供无限的零字节流
-
/dev/stdin、/dev/stdout、/dev/stderr:指向当前进程的标准流
7、性能考虑
-
缓冲机制:
-
全缓冲:文件重定向通常使用全缓冲
-
行缓冲:终端输出通常使用行缓冲
-
无缓冲:错误输出通常无缓冲
-
-
原子操作:
-
追加模式 (>>) 使用 O_APPEND 保证原子性
-
普通重定向 (>) 可能被截断
-
二、在myshell中添加重定向功能
1、实现代码如下
#include <iostream> // 标准输入输出流库
#include <ctype.h> // 字符处理函数库
#include <cstdio> // C标准输入输出库
#include <cstring> // C字符串处理库
#include <cstdlib> // C标准库,包含内存分配、随机数等
#include <unistd.h> // Unix标准库,提供系统调用接口
#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 // 追加重定向int redir = NONE_REDIR; // 当前重定向状态
std::string filename; // 重定向文件名// 测试用变量
char cwd[1024]; // 当前工作目录缓冲区
char cwdenv[1024]; // 环境变量缓冲区// 最后退出码
int lastcode = 0; // 记录上一条命令的退出状态码// 获取当前用户名
const char *GetUserName()
{const char *name = getenv("USER"); // 从环境变量获取用户名return name == NULL ? "None" : name; // 如果不存在返回"None"
}// 获取主机名
const char *GetHostName()
{const char *hostname = getenv("HOSTNAME"); // 从环境变量获取主机名return hostname == NULL ? "None" : hostname; // 如果不存在返回"None"
}// 获取当前工作目录
const char *GetPwd()
{const char *pwd = getcwd(cwd, sizeof(cwd)); // 获取当前工作目录if(pwd != NULL){// 将当前目录设置到环境变量中snprintf(cwdenv, sizeof(cwdenv), "PWD=%s", cwd);putenv(cwdenv);}return pwd == NULL ? "None" : pwd; // 如果获取失败返回"None"
}// 获取家目录
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; // 重置环境变量计数器// 从父进程继承环境变量for(int i = 0; environ[i]; i++){// 为每个环境变量分配内存并复制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"; // 添加测试环境变量g_env[g_envs] = NULL; // 环境变量数组以NULL结尾// 将环境变量设置到系统中for(int i = 0; g_env[i]; i++){putenv(g_env[i]);}environ = g_env; // 更新全局环境变量指针
}// cd命令处理函数
bool Cd()
{// cd argc = 1 表示只有cd命令没有参数if(g_argc == 1){std::string home = GetHome(); // 获取家目录if(home.empty()) return true; // 如果家目录为空则直接返回chdir(home.c_str()); // 切换到用户家目录}else{std::string where = g_argv[1]; // 获取目标目录// 处理特殊目录符号if(where == "-"){// Todu (待实现)}else if(where == "~"){// Todu (待实现)}else{chdir(where.c_str()); // 切换到指定目录}}return true;
}// echo命令处理函数
void Echo()
{if(g_argc == 2) // 只有一个参数的情况{std::string opt = g_argv[1]; // 获取echo的参数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; // 直接输出参数}}
}// 获取目录名
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());
}// 打印命令行提示符
void PrintCommandPrompt()
{char prompt[COMMAND_SIZE]; // 提示符缓冲区MakeCommandLine(prompt, sizeof(prompt)); // 生成提示符printf("%s", prompt); // 打印提示符fflush(stdout); // 刷新输出缓冲区
}// 获取用户输入的命令行
bool GetCommandLine(char *out, int size)
{// 从标准输入读取一行命令char *c = fgets(out, size, stdin);if(c == NULL) return false; // 读取失败返回falseout[strlen(out)-1] = 0; // 去掉末尾的换行符if(strlen(out) == 0) return false; // 空命令返回falsereturn true; // 成功获取命令
}// 命令行解析函数
bool CommandParse(char *commandline)
{
#define SEP " " // 定义命令分隔符(空格)g_argc = 0; // 重置参数计数器// 使用strtok分割命令行字符串g_argv[g_argc++] = strtok(commandline, SEP); // 第一个参数while((bool)(g_argv[g_argc++] = strtok(nullptr, SEP))); // 继续分割剩余部分g_argc--; // 修正计数器(因为循环结束后会多计数一次)return g_argc > 0 ? true:false; // 如果有参数返回true
}// 打印参数数组(调试用)
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()
{std::string cmd = g_argv[0]; // 获取命令名if(cmd == "cd") // cd命令{Cd();return true;}else if(cmd == "echo") // echo命令{Echo();return true;}else if(cmd == "export") // export命令(待实现){}else if(cmd == "alias") // alias命令(待实现){// std::string nickname = g_argv[1];// alias_list.insert(k, v);}return false; // 不是内建命令返回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{}// 执行命令execvp(g_argv[0], g_argv); // 执行程序exit(1); // 如果execvp失败则退出}int status = 0;// 父进程等待子进程结束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; // 从命令末尾开始检查// 检查重定向符号: > >> <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()
{InitEnv(); // 初始化环境变量while(true) // 主循环{PrintCommandPrompt(); // 打印提示符char commandline[COMMAND_SIZE]; // 命令缓冲区if(!GetCommandLine(commandline, sizeof(commandline))) // 获取用户输入continue; // 获取失败则继续循环RedirCheck(commandline); // 检查重定向if(!CommandParse(commandline)) // 解析命令continue;if(CheckAndExecBuiltin()) // 检查并执行内建命令continue;Execute(); // 执行外部命令}return 0;
}
2、Shell 实现代码详细解析
下面我将按照功能模块对这段代码进行详细讲解,之前已经实现的功能就不详细讲了,主要讲解重定向功能的实现:
1. 头文件包含部分
#include <iostream> // 标准输入输出流库
#include <ctype.h> // 字符处理函数库
#include <cstdio> // C标准输入输出库
#include <cstring> // C字符串处理库
#include <cstdlib> // C标准库,包含内存分配、随机数等
#include <unistd.h> // Unix标准库,提供系统调用接口
#include <sys/types.h> // 系统类型定义
#include <sys/wait.h> // 进程等待相关函数
#include <unordered_map> // 无序哈希表容器
#include <sys/stat.h> // 文件状态信息
#include <fcntl.h> // 文件控制选项
这部分包含了实现shell所需的各种库:
-
I/O处理库(iostream, cstdio)
-
字符串处理库(cstring)
-
系统调用相关库(unistd.h, sys/types.h, sys/wait.h)
-
文件操作库(sys/stat.h, fcntl.h)
-
数据结构(unordered_map用于实现别名功能)
2. 宏定义和全局变量
#define COMMAND_SIZE 1024 // 定义命令缓冲区大小
#define FORMAT "[%s@%s %s]# " // 定义命令行提示符格式// 命令行参数表
#define MAXARGC 128 // 最大参数数量
char *g_argv[MAXARGC]; // 全局参数数组
int g_argc = 0; // 参数计数器// 环境变量表
#define MAX_ENVS 100 // 最大环境变量数量
char *g_env[MAX_ENVS]; // 全局环境变量数组
int g_envs = 0; // 环境变量计数器// 别名映射表
std::unordered_map<std::string, std::string> alias_list;// 重定向相关定义
#define NONE_REDIR 0 // 无重定向
#define INPUT_REDIR 1 // 输入重定向
#define OUTPUT_REDIR 2 // 输出重定向
#define APPEND_REDIR 3 // 追加重定向int redir = NONE_REDIR; // 当前重定向状态
std::string filename; // 重定向文件名// 测试用变量
char cwd[1024]; // 当前工作目录缓冲区
char cwdenv[1024]; // 环境变量缓冲区// 最后退出码
int lastcode = 0; // 记录上一条命令的退出状态码
这部分定义了:
-
命令缓冲区大小和提示符格式
-
命令行参数存储结构
-
环境变量存储结构
-
别名映射表(使用unordered_map实现)
-
重定向相关状态和文件名
-
工作目录和环境变量缓冲区
-
上一条命令的退出状态码
3. 系统信息获取函数
3.1 获取用户名
const char *GetUserName()
{const char *name = getenv("USER"); // 从环境变量获取用户名return name == NULL ? "None" : name; // 如果不存在返回"None"
}
-
使用
getenv("USER")
从环境变量获取当前用户名 -
如果获取失败返回"None"
3.2 获取主机名
const char *GetHostName()
{const char *hostname = getenv("HOSTNAME"); // 从环境变量获取主机名return hostname == NULL ? "None" : hostname; // 如果不存在返回"None"
}
-
使用
getenv("HOSTNAME")
从环境变量获取主机名 -
如果获取失败返回"None"
3.3 获取当前工作目录
const char *GetPwd()
{const char *pwd = getcwd(cwd, sizeof(cwd)); // 获取当前工作目录if(pwd != NULL){// 将当前目录设置到环境变量中snprintf(cwdenv, sizeof(cwdenv), "PWD=%s", cwd);putenv(cwdenv);}return pwd == NULL ? "None" : pwd; // 如果获取失败返回"None"
}
-
使用
getcwd()
获取当前工作目录 -
将当前目录更新到PWD环境变量中
-
如果获取失败返回"None"
3.4 获取家目录
const char *GetHome()
{const char *home = getenv("HOME"); // 从环境变量获取家目录return home == NULL ? "" : home; // 如果不存在返回空字符串
}
-
使用
getenv("HOME")
获取用户家目录 -
如果获取失败返回空字符串
4. 环境变量初始化
void InitEnv()
{extern char **environ; // 外部环境变量指针memset(g_env, 0, sizeof(g_env)); // 清空环境变量数组g_envs = 0; // 重置环境变量计数器// 从父进程继承环境变量for(int i = 0; environ[i]; i++){// 为每个环境变量分配内存并复制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"; // 添加测试环境变量g_env[g_envs] = NULL; // 环境变量数组以NULL结尾// 将环境变量设置到系统中for(int i = 0; g_env[i]; i++){putenv(g_env[i]);}environ = g_env; // 更新全局环境变量指针
}
-
从父进程继承所有环境变量
-
为每个环境变量分配内存并复制
-
添加一个测试环境变量"HAHA=for_test"
-
使用
putenv()
设置环境变量 -
更新全局
environ
指针指向自定义环境变量表
5. 内建命令实现
5.1 cd命令
bool Cd()
{if(g_argc == 1) // 只有cd命令没有参数{std::string home = GetHome(); // 获取家目录if(home.empty()) return true; chdir(home.c_str()); // 切换到用户家目录}else{std::string where = g_argv[1]; // 获取目标目录// 处理特殊目录符号if(where == "-"){// Todu (待实现)}else if(where == "~"){// Todu (待实现)}else{chdir(where.c_str()); // 切换到指定目录}}return true;
}
-
无参数时切换到用户家目录
-
有参数时切换到指定目录
-
特殊符号"-"和"~"待实现
5.2 echo命令
void Echo()
{if(g_argc == 2) // 只有一个参数的情况{std::string opt = g_argv[1]; // 获取echo的参数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; // 直接输出参数}}
}
-
处理特殊变量
$?
:输出上一条命令的退出码 -
处理环境变量引用
$VAR
:输出环境变量值 -
其他情况直接输出参数
6. 命令行处理函数
6.1 生成提示符
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());
}void PrintCommandPrompt()
{char prompt[COMMAND_SIZE]; // 提示符缓冲区MakeCommandLine(prompt, sizeof(prompt)); // 生成提示符printf("%s", prompt); // 打印提示符fflush(stdout); // 刷新输出缓冲区
}
-
DirName()
: 从完整路径中提取最后一个目录名 -
MakeCommandLine()
: 按照格式生成提示符字符串 -
PrintCommandPrompt()
: 打印格式化的提示符
6.2 获取用户输入
bool GetCommandLine(char *out, int size)
{// 从标准输入读取一行命令char *c = fgets(out, size, stdin);if(c == NULL) return false; // 读取失败返回falseout[strlen(out)-1] = 0; // 去掉末尾的换行符if(strlen(out) == 0) return false; // 空命令返回falsereturn true; // 成功获取命令
}
-
使用
fgets()
从标准输入读取命令 -
去除末尾换行符
-
检查空命令情况
6.3 命令解析
bool CommandParse(char *commandline)
{
#define SEP " " // 定义命令分隔符(空格)g_argc = 0; // 重置参数计数器// 使用strtok分割命令行字符串g_argv[g_argc++] = strtok(commandline, SEP); // 第一个参数while((bool)(g_argv[g_argc++] = strtok(nullptr, SEP))); // 继续分割剩余部分g_argc--; // 修正计数器(因为循环结束后会多计数一次)return g_argc > 0 ? true:false; // 如果有参数返回true
}
-
使用
strtok()
按空格分割命令行 -
将分割后的参数存入全局数组
g_argv
-
更新参数计数器
g_argc
6.4 重定向检查
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; // 从命令末尾开始检查// 检查重定向符号: > >> <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--; // 继续向前检查}}
}
1. TrimSpace 函数
void TrimSpace(char cmd[], int &end)
{while(isspace(cmd[end])) // 跳过空白字符{end++;}
}
功能:
-
跳过字符串中从指定位置
end
开始的所有空白字符 -
空白字符包括:空格(' ')、制表符('\t')、换行符('\n')等(由
isspace()
函数定义)
参数:
-
cmd[]
: 待处理的命令行字符串 -
end
: 引用传递的整数,表示当前检查位置
工作原理:
-
使用
isspace()
检测cmd[end]
是否为空白字符 -
如果是,则
end++
移动到下一个字符 -
循环直到遇到非空白字符或字符串结束
作用:
-
在重定向符号和文件名之间可能有多个空格,此函数用于跳过这些空格
-
例如处理
command > file.txt
这种情况
2. RedirCheck 函数
void RedirCheck(char cmd[])
{redir = NONE_REDIR; // 重置重定向状态filename.clear(); // 清空文件名int start = 0;int end = strlen(cmd)-1; // 从命令末尾开始检查// 检查重定向符号: > >> <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--; // 继续向前检查}}
}
功能:
-
从命令行字符串末尾向前扫描,检测重定向符号(
<
,>
,>>
) -
设置重定向类型和文件名
-
修改原命令字符串,去除重定向部分
参数:
-
cmd[]
: 命令行字符串(会被修改)
全局变量影响:
-
redir
: 设置重定向类型(NONE_REDIR/INPUT_REDIR/OUTPUT_REDIR/APPEND_REDIR) -
filename
: 设置重定向的文件名
详细工作流程:
-
初始化:
-
重置重定向状态为
NONE_REDIR
-
清空
filename
-
设置
end
为字符串最后一个字符的索引
-
-
从后向前扫描循环:从命令行末尾向前查找重定向符号
-
处理输入重定向
<
:-
当找到
<
字符时:-
将
<
位置设为字符串结束符\0
(cmd[end] = 0
) -
end++
移动到下一个字符 -
调用
TrimSpace
跳过可能存在的空格 -
设置
redir = INPUT_REDIR
-
filename
指向空格后的字符串(即文件名) -
退出循环
-
-
-
处理输出重定向
>
和>>
:-
当找到
>
字符时:-
检查前一个字符是否也是
>
(即>>
追加模式)-
如果是
>>
:-
将前一个
>
位置设为字符串结束符 -
设置
redir = APPEND_REDIR
-
-
如果是单个
>
:-
设置
redir = OUTPUT_REDIR
-
-
-
将
>
位置设为字符串结束符 -
end++
移动到下一个字符 -
调用
TrimSpace
跳过可能存在的空格 -
filename
指向空格后的字符串(即文件名) -
退出循环
-
-
-
未找到重定向符号:
-
end--
继续向前扫描 -
如果扫描完整个字符串都没找到重定向符号,则保持
redir = NONE_REDIR
-
示例分析:
案例1:command > output.txt
-
从末尾找到
>
-
不是
>>
,所以是普通输出重定向 -
在
>
处截断命令,命令变为command
-
跳过
>
后的空格 -
设置
filename = "output.txt"
-
设置
redir = OUTPUT_REDIR
案例2:command >> log.txt
-
从末尾找到
>
-
发现前一个字符也是
>
,是追加模式 -
在第一个
>
处截断命令,命令变为command
-
跳过
>>
后的空格 -
设置
filename = "log.txt"
-
设置
redir = APPEND_REDIR
案例3:command < input.txt
-
从末尾找到
<
-
在
<
处截断命令,命令变为command
-
跳过
<
后的空格 -
设置
filename = "input.txt"
-
设置
redir = INPUT_REDIR
7. 命令执行
7.1 内建命令检查
bool CheckAndExecBuiltin()
{std::string cmd = g_argv[0]; // 获取命令名if(cmd == "cd") // cd命令{Cd();return true;}else if(cmd == "echo") // echo命令{Echo();return true;}else if(cmd == "export") // export命令(待实现){}else if(cmd == "alias") // alias命令(待实现){// std::string nickname = g_argv[1];// alias_list.insert(k, v);}return false; // 不是内建命令返回false
}
-
检查命令是否为内建命令(cd, echo, export, alias)
-
如果是则执行相应函数并返回true
-
否则返回false
7.2 执行外部命令
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{}// 执行命令execvp(g_argv[0], g_argv); // 执行程序exit(1); // 如果execvp失败则退出}int status = 0;// 父进程等待子进程结束pid_t rid = waitpid(id, &status, 0);if(rid > 0){lastcode = WEXITSTATUS(status); // 记录子进程退出状态}return 0;
}
1. 函数总体结构
int Execute()
{// 1. 创建子进程// 2. 子进程处理:// a. 重定向设置// b. 执行命令// 3. 父进程等待子进程结束// 4. 记录子进程退出状态
}
2. 进程创建 (fork)
pid_t id = fork(); // 创建子进程
-
fork()
系统调用创建一个与父进程几乎完全相同的子进程 -
返回值:
-
在父进程中返回子进程的 PID
-
在子进程中返回 0
-
出错时返回 -1
-
3. 子进程处理部分
if(id == 0) // 子进程
{// 重定向处理和命令执行
}
3.1 重定向处理
输入重定向 (<
)
if(redir == INPUT_REDIR) // 输入重定向
{fd = open(filename.c_str(), O_RDONLY); // 以只读方式打开文件if(fd < 0) exit(1); // 打开失败则退出(状态码1)dup2(fd, 0); // 将文件描述符复制到标准输入(0)close(fd); // 关闭原文件描述符
}
-
open()
打开指定文件,返回文件描述符 -
dup2(fd, 0)
将文件描述符复制到标准输入(文件描述符0) -
关闭原文件描述符避免资源泄漏
重要结论
-
不会自动恢复:关闭原
fd
后,文件描述符0不会自动重新连接到原来的标准输入设备 -
重定向是持久的:除非再次显式调用
dup2
来重定向,否则文件描述符0将一直保持指向重定向的文件 -
子进程特性:这种改变只影响当前进程(子进程),不会影响父进程(shell本身)的标准输入
输出重定向 (>
)
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);
}
-
O_CREAT
: 如果文件不存在则创建 -
O_WRONLY
: 只写方式打开 -
O_TRUNC
: 如果文件存在则截断为0长度 -
权限模式
0666
(rw-rw-rw-) -
重定向到标准输出(文件描述符1)
追加重定向 (>>
)
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);
}
-
O_APPEND
: 以追加模式打开,写入内容添加到文件末尾 -
其他参数与输出重定向相同
3.2 命令执行
execvp(g_argv[0], g_argv); // 执行程序
exit(1); // 如果execvp失败则退出
-
execvp()
执行指定程序:-
第一个参数是要执行的程序名
-
第二个参数是参数数组(以NULL结尾)
-
'v' 表示参数以数组形式传递
-
'p' 表示使用PATH环境变量查找程序
-
-
如果
execvp()
成功,不会返回 -
如果失败,执行
exit(1)
退出子进程
4. 父进程处理部分
int status = 0;
// 父进程等待子进程结束
pid_t rid = waitpid(id, &status, 0);
if(rid > 0)
{lastcode = WEXITSTATUS(status); // 记录子进程退出状态
}
return 0;
-
waitpid()
等待指定子进程结束:-
第一个参数是要等待的子进程ID
-
第二个参数存储子进程退出状态
-
第三个参数是选项(0表示阻塞等待)
-
-
WEXITSTATUS(status)
从状态值中提取退出码 -
将退出码保存到
lastcode
全局变量中,供后续命令(如echo $?)使用
5. 错误处理
-
文件打开失败:子进程直接退出(状态码1或2)
-
execvp
失败:子进程退出(状态码1) -
waitpid
错误:未做特殊处理(rid <= 0时忽略)
6. 关键系统调用说明
-
fork()
: 创建进程 -
open()
: 打开/创建文件 -
dup2()
: 复制文件描述符 -
close()
: 关闭文件描述符 -
execvp()
: 执行程序 -
waitpid()
: 等待子进程结束 -
exit()
: 终止进程
7. 完整执行流程示例
以执行 ls -l > output.txt
为例:
-
父进程调用
fork()
创建子进程 -
子进程:
-
检测到
OUTPUT_REDIR
标志 -
打开(或创建)
output.txt
文件 -
将标准输出重定向到该文件
-
执行
execvp("ls", ["ls", "-l", NULL])
-
-
ls
程序的输出被写入output.txt
-
ls
执行结束后,子进程终止 -
父进程通过
waitpid()
获取子进程退出状态 -
父进程记录退出状态到
lastcode
8. 主函数
int main()
{InitEnv(); // 初始化环境变量while(true) // 主循环{PrintCommandPrompt(); // 打印提示符char commandline[COMMAND_SIZE]; // 命令缓冲区if(!GetCommandLine(commandline, sizeof(commandline))) // 获取用户输入continue; // 获取失败则继续循环RedirCheck(commandline); // 检查重定向if(!CommandParse(commandline)) // 解析命令continue;if(CheckAndExecBuiltin()) // 检查并执行内建命令continue;Execute(); // 执行外部命令}return 0;
}
-
初始化环境变量
-
进入主循环:
-
打印提示符
-
获取用户输入
-
检查重定向
-
解析命令
-
检查并执行内建命令
-
执行外部命令
-
三、总结
大概实现的效果:
这个简单的shell实现包含以下主要功能:
-
命令行提示符显示(用户名、主机名、当前目录)
-
基本命令解析和执行
-
内建命令实现(cd, echo)
-
输入/输出重定向支持
-
环境变量管理
可以扩展的功能包括:
-
管道支持
-
后台进程执行
-
更完整的alias和export实现
-
命令历史记录
-
通配符扩展
-
信号处理等