前言:在談論添加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實現
-
命令歷史記錄
-
通配符擴展
-
信號處理等