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 系統調用層面

重定向主要通過以下系統調用實現:

  1. open():打開文件獲取文件描述符

  2. dup()/dup2():復制文件描述符

  3. close():關閉文件描述符

  4. fork()?和?exec():創建新進程并執行程序

2.2 Shell 實現原理

當 Shell 解析到重定向符號時:

  1. 解析階段

    • Shell 識別重定向符號(>, <, >>等)

    • 確定需要重定向的文件描述符(默認為0或1)

  2. 準備階段

    • 對目標文件執行 open() 系統調用

    • 使用 dup2() 復制文件描述符

  3. 執行階段

    • 使用 fork() 創建子進程

    • 在子進程中執行 dup2() 完成重定向

    • 調用 exec() 執行目標命令

3、不同類型重定向的機制

3.1 輸出重定向 (>)

實現流程

  1. 打開或創建目標文件(O_WRONLY|O_CREAT|O_TRUNC)

  2. 使用 dup2(fd, STDOUT_FILENO) 將標準輸出重定向到文件

  3. 關閉原始的標準輸出文件描述符

示例代碼等價

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 輸入重定向 (<)

實現流程

  1. 打開源文件(O_RDONLY)

  2. 使用 dup2(fd, STDIN_FILENO)

  3. 關閉原始的標準輸入文件描述符

3.4 錯誤重定向 (2>)

使用 dup2() 將文件描述符2重定向:

dup2(fd, STDERR_FILENO);

4、高級重定向原理

4.1 文件描述符復制 (n>&m)

dup2() 工作原理(先了解即可)

  • 關閉文件描述符n(如果已打開)

  • 使n成為m的副本,指向同一個打開文件表項

  • 兩個描述符共享文件偏移量和狀態標志

4.2 管道重定向 (|)(先了解即可)

實現機制

  1. 使用 pipe() 系統調用創建匿名管道(返回兩個文件描述符)

    • pipefd[0]:讀取端

    • pipefd[1]:寫入端

  2. 第一個進程將 STDOUT 重定向到 pipefd[1]

  3. 第二個進程將 STDIN 重定向到 pipefd[0]

5、內核處理流程(重要!!!)

  1. 進程創建時

    • 繼承父進程的文件描述符表

    • 默認打開0,1,2指向終端設備

  2. 重定向發生時

    • 修改進程的文件描述符表

    • 不改變系統級的打開文件表

  3. I/O操作時

    • 通過文件描述符索引到打開文件表

    • 最終訪問實際文件或設備

6、特殊文件與設備

  • /dev/null:黑洞設備,丟棄所有寫入的數據

  • /dev/zero:提供無限的零字節流

  • /dev/stdin/dev/stdout/dev/stderr:指向當前進程的標準流

7、性能考慮

  1. 緩沖機制

    • 全緩沖:文件重定向通常使用全緩沖

    • 行緩沖:終端輸出通常使用行緩沖

    • 無緩沖:錯誤輸出通常無緩沖

  2. 原子操作

    • 追加模式 (>>) 使用 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;         // 記錄上一條命令的退出狀態碼

這部分定義了:

  1. 命令緩沖區大小和提示符格式

  2. 命令行參數存儲結構

  3. 環境變量存儲結構

  4. 別名映射表(使用unordered_map實現)

  5. 重定向相關狀態和文件名

  6. 工作目錄和環境變量緩沖區

  7. 上一條命令的退出狀態碼

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: 引用傳遞的整數,表示當前檢查位置

工作原理:
  1. 使用?isspace()?檢測?cmd[end]?是否為空白字符

  2. 如果是,則?end++?移動到下一個字符

  3. 循環直到遇到非空白字符或字符串結束

作用:
  • 在重定向符號和文件名之間可能有多個空格,此函數用于跳過這些空格

  • 例如處理?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: 設置重定向的文件名

詳細工作流程:
  1. 初始化

    • 重置重定向狀態為?NONE_REDIR

    • 清空?filename

    • 設置?end?為字符串最后一個字符的索引

  2. 從后向前掃描循環:從命令行末尾向前查找重定向符號

  3. 處理輸入重定向?<

    • 當找到?<?字符時:

      • 將?<?位置設為字符串結束符?\0cmd[end] = 0

      • end++?移動到下一個字符

      • 調用?TrimSpace?跳過可能存在的空格

      • 設置?redir = INPUT_REDIR

      • filename?指向空格后的字符串(即文件名)

      • 退出循環

  4. 處理輸出重定向?>?和?>>

    • 當找到?>?字符時:

      • 檢查前一個字符是否也是?>(即?>>?追加模式)

        • 如果是?>>

          • 將前一個?>?位置設為字符串結束符

          • 設置?redir = APPEND_REDIR

        • 如果是單個?>

          • 設置?redir = OUTPUT_REDIR

      • 將?>?位置設為字符串結束符

      • end++?移動到下一個字符

      • 調用?TrimSpace?跳過可能存在的空格

      • filename?指向空格后的字符串(即文件名)

      • 退出循環

  5. 未找到重定向符號

    • end--?繼續向前掃描

    • 如果掃描完整個字符串都沒找到重定向符號,則保持?redir = NONE_REDIR

示例分析:

案例1command > output.txt

  1. 從末尾找到?>

  2. 不是?>>,所以是普通輸出重定向

  3. 在?>?處截斷命令,命令變為?command

  4. 跳過?>?后的空格

  5. 設置?filename = "output.txt"

  6. 設置?redir = OUTPUT_REDIR

案例2command >> log.txt

  1. 從末尾找到?>

  2. 發現前一個字符也是?>,是追加模式

  3. 在第一個?>?處截斷命令,命令變為?command

  4. 跳過?>>?后的空格

  5. 設置?filename = "log.txt"

  6. 設置?redir = APPEND_REDIR

案例3command < input.txt

  1. 從末尾找到?<

  2. 在?<?處截斷命令,命令變為?command

  3. 跳過?<?后的空格

  4. 設置?filename = "input.txt"

  5. 設置?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. 關鍵系統調用說明
  1. fork(): 創建進程

  2. open(): 打開/創建文件

  3. dup2(): 復制文件描述符

  4. close(): 關閉文件描述符

  5. execvp(): 執行程序

  6. waitpid(): 等待子進程結束

  7. exit(): 終止進程

7. 完整執行流程示例

以執行?ls -l > output.txt?為例:

  1. 父進程調用?fork()?創建子進程

  2. 子進程:

    • 檢測到?OUTPUT_REDIR?標志

    • 打開(或創建)?output.txt?文件

    • 將標準輸出重定向到該文件

    • 執行?execvp("ls", ["ls", "-l", NULL])

  3. ls?程序的輸出被寫入?output.txt

  4. ls?執行結束后,子進程終止

  5. 父進程通過?waitpid()?獲取子進程退出狀態

  6. 父進程記錄退出狀態到?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;
}
  • 初始化環境變量

  • 進入主循環:

    1. 打印提示符

    2. 獲取用戶輸入

    3. 檢查重定向

    4. 解析命令

    5. 檢查并執行內建命令

    6. 執行外部命令


三、總結

大概實現的效果:

這個簡單的shell實現包含以下主要功能:

  1. 命令行提示符顯示(用戶名、主機名、當前目錄)

  2. 基本命令解析和執行

  3. 內建命令實現(cd, echo)

  4. 輸入/輸出重定向支持

  5. 環境變量管理

可以擴展的功能包括:

  • 管道支持

  • 后臺進程執行

  • 更完整的alias和export實現

  • 命令歷史記錄

  • 通配符擴展

  • 信號處理等

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/news/915186.shtml
繁體地址,請注明出處:http://hk.pswp.cn/news/915186.shtml
英文地址,請注明出處:http://en.pswp.cn/news/915186.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

Django由于數據庫版本原因導致數據庫遷移失敗解決辦法

在django開發中&#xff0c;一般我們初始化一個項目之后&#xff0c;創建應用一般就會生成如下的目錄&#xff1a;django-admin startproject myproject python manage.py startapp blogmyproject/ ├── manage.py └── myproject/ | ├── __init__.py | ├── se…

C++STL系列之vector

前言 vector是變長數組&#xff0c;有點像數據結構中的順序表&#xff0c;它和list也是經常被拿出作對比的&#xff0c; vector使用動態分配數組來存儲它的元素。當新元素插入時候&#xff0c;這個數組需要被重新分配大小&#xff0c;如果擴容&#xff0c;因為要開一個新數組把…

Functional C++ for Fun Profit

Lambda Conf上有人講C函數式編程。在Functional Conf 2019上&#xff0c;就有主題為“Lambdas: The Functional Programming Companion of Modern C”的演講。演講者介紹了現代C中函數式編程相關內容&#xff0c;講解了如何使用Lambda表達式編寫符合函數式編程原則的C代碼&…

Python基礎理論與實踐:從零到爬蟲實戰

引言Python如輕舟&#xff0c;載你探尋數據寶藏&#xff01;本文從基礎理論&#xff08;變量、循環、函數、模塊&#xff09;啟航&#xff0c;結合requests和BeautifulSoup實戰爬取Quotes to Scrape&#xff0c;適合零基礎到進階者。文章聚焦Python基礎&#xff08;變量、循環、…

ThingJS開發從入門到精通:構建三維物聯網可視化應用的完整指南

文章目錄第一部分&#xff1a;ThingJS基礎入門第一章 ThingJS概述與技術架構1.1 ThingJS平臺簡介1.2 技術架構解析1.3 開發環境配置第二章 基礎概念與核心API2.1 核心對象模型2.2 場景創建與管理2.3 對象操作基礎第三章 基礎開發實戰3.1 第一個ThingJS應用3.2 事件系統詳解3.3 …

關于list

1、什么是listlist是一個帶頭結點的雙向循環鏈表模版容器&#xff0c;可以存放任意類型&#xff0c;需要顯式定義2、list的使用有了前面學習string和vector的基礎&#xff0c;學習和使用list會方便很多&#xff0c;因為大部分的內容依然是高度重合的。與順序表不同&#xff0c;…

Mysql 查看當前事務鎖

在 MySQL 中查看事務鎖&#xff08;鎖等待、鎖持有等&#xff09;&#xff0c;可以使用以下方法&#xff1a; 一、查看當前鎖等待情況&#xff08;推薦&#xff09; SELECTr.trx_id AS waiting_trx_id,r.trx_mysql_thread_id AS waiting_thread,r.trx_query AS waiting_query,b…

【Keil5-map文件】

Keil5-map文件■ map文件■ map文件

k8s 基本架構

基于Kubernetes(K8s)的核心設計&#xff0c;以下是其關鍵基本概念的詳細解析。這些概念構成了K8s容器編排系統的基石&#xff0c;用于自動化部署、擴展和管理容器化應用。### 一、K8s核心概念概覽 K8s的核心對象圍繞容器生命周期管理、資源調度和服務發現展開&#xff0c;主要包…

Bell不等式賦能機器學習:微算法科技MLGO一種基于量子糾纏的監督量子分類器訓練算法技術

近年來&#xff0c;量子計算&#xff08;Quantum Computing&#xff09; 和 機器學習&#xff08;Machine Learning&#xff09; 的融合成為人工智能和計算科學領域的重要研究方向。隨著經典計算機在某些復雜任務上接近計算極限&#xff0c;研究人員開始探索量子計算的獨特優勢…

Edge瀏覽器設置網頁自動翻譯

一.瀏覽網頁自動翻譯設置->擴展->獲取Microsoft Edge擴展->搜索“沉浸式翻譯”->獲取 。提示&#xff1a;如果采用其他的翻譯擴展沒找自動翻譯功能&#xff0c;所以這里選擇“沉浸式翻譯”二.基于Java WebElement時自動翻譯Java關鍵代碼&#xff1a;提示&#xff1…

TCP/UDP協議深度解析(四):TCP的粘包問題以及異常情況處理

&#x1f50d; 開發者資源導航 &#x1f50d;&#x1f3f7;? 博客主頁&#xff1a; 個人主頁&#x1f4da; 專欄訂閱&#xff1a; JavaEE全棧專欄 本系列往期內容~ TCP/UDP協議深度解析&#xff08;一&#xff09;&#xff1a;UDP特性與TCP確認應答以及重傳機制 TCP/UDP協議深…

R 基礎語法

R 基礎語法 R 語言是一種針對統計計算和圖形表示而設計的編程語言&#xff0c;廣泛應用于數據分析、統計學習、生物信息學等領域。本文將為您介紹 R 語言的基礎語法&#xff0c;幫助您快速入門。 1. R 語言環境搭建 在開始學習 R 語言之前&#xff0c;您需要安裝并配置 R 語言環…

語義熵怎么增強LLM自信心的

語義熵怎么增強LLM自信心的 一、傳統Token熵的問題(先理解“痛點”) 比如模型回答“阿司匹林是否治療頭痛?”→ 輸出“是” 傳統Token熵:只看“詞的概率”,比如“是”這個詞的概率特別高(Token熵0.2,數值低說明確定性強 )。 但實際風險:醫學場景里,“是”的字面肯定…

javaweb的幾大常見漏洞

CTF javaweb中幾大常見漏洞(基于java-security靶場) 對于CTF而言&#xff0c;java類型的題目基本都是白盒代碼審計&#xff0c;在java類型的web題目增長的今天&#xff0c;java代碼審計能力在ctf比賽中尤為重要。 這篇博客主要是給大家介紹一下一些常見漏洞在java代碼里面大概是…

【設計模式C#】外觀模式(用于解決客戶端對系統的許多類進行頻繁溝通)

一種結構性設計模式。特點是將復雜的子系統調用邏輯封裝到一個外觀類&#xff0c;從而使客戶端更容易與系統交互。優點&#xff1a;簡化了接口的調用&#xff1b;降低了客戶端與子系統的耦合度&#xff1b;封裝了子系統的邏輯。缺點&#xff1a;引入了額外的類&#xff0c;可能…

【PTA數據結構 | C語言版】二叉堆的快速建堆操作

本專欄持續輸出數據結構題目集&#xff0c;歡迎訂閱。 文章目錄題目代碼題目 請編寫程序&#xff0c;將 n 個順序存儲的數據用快速建堆操作調整為最小堆&#xff1b;最后順次輸出堆中元素以檢驗操作的正確性。 輸入格式&#xff1a; 輸入首先給出一個正整數 c&#xff08;≤1…

【數據結構初階】--雙向鏈表(二)

&#x1f525;個人主頁&#xff1a;草莓熊Lotso &#x1f3ac;作者簡介&#xff1a;C研發方向學習者 &#x1f4d6;個人專欄&#xff1a; 《C語言》 《數據結構與算法》《C語言刷題集》《Leetcode刷題指南》 ??人生格言&#xff1a;生活是默默的堅持&#xff0c;毅力是永久的…

vue-cli 模式下安裝 uni-ui

目錄 easycom 自定義easycom配置的示例 npm安裝 uni-ui 準備 sass 安裝 uni-ui 注意 easycom 傳統vue組件&#xff0c;需要安裝、引用、注冊&#xff0c;三個步驟后才能使用組件。easycom將其精簡為一步。 只要組件路徑符合規范&#xff08;具體見下&#xff09;&#…

JavaSE-接口

概念在Java中&#xff0c;接口可以被看成是一種公共規范&#xff0c;是一種引用數據類型。語法1.接口的定義格式與類的定義格式基本相同&#xff0c;將class關鍵字替換為interface關鍵字&#xff1a;public interface IShape {}2.類與接口之間使用implements關鍵字來實現接口&a…