目錄
?編輯
一、引言與動機
📝背景
📝主要內容概括
二、全局數據
三、環境變量的初始化
? 代碼實現
?四、構造動態提示符
? 打印提示符函數
? 提示符生成函數?
?獲取用戶名函數
?獲取主機名函數
?獲取當前目錄名函數
五、命令的讀取與解析
?讀取用戶輸入函數
?命令解析函數
六、內建命令的檢測與執行
💡先來回答兩個疑問:
?檢測函數
?cd的實現
??echo的實現
七、重定向的處理
?讀取函數
?去空格函數:
?執行函數
八、執行流程?
九、源碼
一、引言與動機
📝背景
我們從之前的文章中學習了linux的相關知識,包括但不限于進程管理、文件的重定向以及環境變量,基于此我們來自制一個簡化版的shell,也是對前期學習的內容的一個運用。
📝主要內容概括
我們將實現以下功能:
-
全局數據和環境變量初始化
-
命令提示符的構建
-
命令行輸入與解析機制
-
內建命令(
cd
、echo
)的實現 -
輸入/輸出重定向的處理
-
外部程序的執行流程(
fork
+execvp
+waitpid
)
二、全局數據
#define COMMAND_SIZE 1024
#define FORMAT "[%s@%s %s]# ",格式化字符串
char *g_argv[MAXARGC];
、int g_argc
:保存切分后的命令及參數
char *g_env[MAX_ENVS]
、int g_envs
:復制并維護環境變量列表
std::unordered_map<std::string,std::string> alias_list
:(預留的)別名映射重定向相關:
int redir; std::string filename;
記錄當前目錄:
char cwd[1024]; char cwdenv[1024];
記錄上次命令退出碼:
int lastcode;
?
三、環境變量的初始化
? 代碼實現
這里主要是為了后續實現的功能提供自己環境變量,這樣也可使得修改更加方便。
void InitEnv()
{ extern char **environ;//從#include <cstdlib>中獲取環境變量表 memset(g_env, 0, sizeof(g_env));//清空自建的環境變量表 g_envs = 0; //1. 獲取環境變量 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;//注意末尾置空//2. 導成環境變量 for(int i = 0; g_env[i]; i++) { putenv(g_env[i]); } environ = g_env; //3.配置為全局的變量
}
?四、構造動態提示符
我們在使用linux時常要變換所在路徑,所以我們這里也實現一個動態變換的提示符:
示例效果:
[alice@myhost project]# //為了區分這里用#
? 打印提示符函數
刷新緩沖區來打印。
void PrintCommandPrompt()
{char prompt[COMMAND_SIZE];MakeCommandLine(prompt, sizeof(prompt));printf("%s", prompt);fflush(stdout);
}
? 提示符生成函數?
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());
}
這里提一下snprintf函數:
在 C/C++ 中,snprintf 是一個用于格式化字符串的函數:
int snprintf(char *str, size_t size, const char *format, ...);
參數:
str:目標字符數組,格式化的字符串將寫入此處。
size:目標字符數組的最大長度(包括結尾的空字符 \0),用于限制寫入字符數以防止緩沖區溢出。
format:格式化字符串,類似于 printf 的格式說明符(如 %d, %s, %f 等)。
...:可變參數列表,對應格式化字符串中的占位符。
返回值:
成功時:返回格式化后字符串的長度(不包括結尾的 \0),即使部分字符因 size 限制未寫入。
失敗時:返回負值(某些實現中可能不同,需檢查文檔)。
敲黑板:
返回的長度是完整格式化字符串的長度,即使因 size 限制只寫入部分字符。
?獲取用戶名函數
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;
}
?獲取當前目錄名函數
這里我們要做一下根目錄的判斷,如果是根目錄就直接返回就行,如果不是根目錄就找出/后面的字符串,沒找到就報錯。
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);
}
實現展示:
五、命令的讀取與解析
?讀取用戶輸入函數
這里需要注意將\n刪除。
bool GetCommandLine(char* out, int size)
{char* c = fgets(out, size, stdin);if (c == NULL) return false;out[strlen(out) - 1] = 0; // 清理\nif (strlen(out) == 0) return false;return true;
}
簡單提一下fgets函數:
在 C/C++ 中,fgets 是一個用于從文件中讀取字符串的函數。
char *fgets(char *str, int size, FILE *stream);
參數:
str:目標字符數組,用于存儲讀取的字符串(包括換行符 \n 和結尾的空字符 \0)。
size:最多讀取的字符數(包括 \0),防止緩沖區溢出。
stream:文件流指針(如 stdin、文件句柄等)。
返回值:
成功:返回 str(指向讀取的字符串)。
失敗或文件末尾(EOF):返回 nullptr。
敲黑板:
讀取到換行符 \n 或文件末尾會停止,換行符(如果存在)會包含在 str 中。?
?命令解析函數
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;
}
?這里簡單提一下strtok函數:
在 C/C++ 中,strtok 是一個用于字符串分割的函數。
char *strtok(char *str, const char *delim);
參數:
str:要分割的字符串(第一次調用時傳入,之后傳入 nullptr 以繼續處理同一字符串)。
delim:包含分隔符的字符串,每個字符都被視為一個分隔符。
返回值:
成功:返回指向下一個 token 的指針。
失敗或無更多 token:返回 nullptr。
敲黑板:
strtok 會修改原字符串(在分隔符處插入 \0),因此輸入字符串必須是可修改的(非 const 或字符串字面量)。?
實現展示:
六、內建命令的檢測與執行
💡先來回答兩個疑問:
第一個疑問:什么是內建命令
內建命令是在shell自身實現的命令,不依賴系統外部的可執行文件。例如:
cd
:切換當前目錄
alias
:設置別名
export
:設置環境變量
echo
:打印信息
exit
:退出 shell
我們這里會實現前三個。
第二個疑問:為什么內建命令單獨執行
主要原因是他們只有在當前shell進程中執行才可以真正的影響到shell的狀態。
比如:cd
改變當前目錄(影響 shell),export
改變環境變量(供子進程使用)以及exit
終止當前shell,這類命令交給子進程執行就完全失去作用了。
?檢測函數
bool CheckAndExecBuiltin()
{std::string cmd = g_argv[0];if (cmd == "cd"){Cd();return true;}else if (cmd == "echo"){Echo();return true;}else if (cmd == "alias"){std::string nickname = g_argv[1];alias_list.insert(k, v);}else if (cmd == "export"){// todo}return false;
}
?cd的實現
bool 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 == "-"){// Todo}else if (where == "~"){// Todo}else{chdir(where.c_str());}}return true;
}
實現展示:
??echo的實現
void Echo()
{ if (g_argc >= 2) { for (int i = 1; i < g_argc; ++i) { std::string opt = g_argv[i]; if (opt == "$?") { std::cout << lastcode; } 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; } else { std::cout << opt; } if (i < g_argc - 1) std::cout << " "; } std::cout << std::endl; }
}
七、重定向的處理
?讀取函數
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--;}}
}
?去空格函數:
void TrimSpace(char cmd[], int& end)
{while (isspace(cmd[end])){end++;}
}
?執行函數
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{// todo}// 進程替換,會影響重定向的結果嗎?不影響//childexecvp(g_argv[0], g_argv); // 進程替換函數,執行成功后續代碼不執行,失敗就調用exit(1)exit(1);}int status = 0;// fatherpid_t rid = waitpid(id, &status, 0); // 阻塞等待子進程退出if (rid > 0){lastcode = WEXITSTATUS(status); // 更新進程退出碼}return 0;
}
這里說明一下這幾個函數:
第一個函數:int dup2(int oldfd, int newfd);
作用:在 Linux 環境下,dup2 是一個 POSIX 系統調用,用于復制文件描述符,常用于重定向文件描述符(如標準輸入、輸出或錯誤輸出)。
參數:
oldfd:要復制的現有文件描述符(如打開的文件、管道、標準輸入/輸出等)。
newfd:目標文件描述符編號,oldfd 將被復制到此編號。
返回值:
成功:返回 newfd(目標文件描述符)。
失敗:返回 -1,并設置 errno 表示錯誤原因(如 EBADF 表示無效文件描述符)。?
敲黑板:
這里可能你會有一個疑問,那就是為什么是oldfd復制到newfd而不是newfd復制到oldfd,這里我們一定要清楚這里的新舊指的是這個文件是否被使用或是否被打開,那么就是被使用的(oldfd)復制到未被使用的(newfd)。
第二個函數:int open(const char *pathname, int flags, mode_t mode);
作用:在 Linux 環境下,open 是一個 POSIX 系統調用,用于打開文件或創建文件,獲取文件描述符以進行讀寫操作。
參數:
pathname:要打開或創建的文件路徑(絕對或相對路徑)。
flags:控制文件打開方式的標志(如只讀、只寫、讀寫等)。
常用標志:
O_RDONLY:只讀。
O_WRONLY:只寫。
O_RDWR:讀寫。
O_CREAT:如果文件不存在則創建。
O_TRUNC:如果文件存在且為寫模式,清空文件內容。
O_APPEND:寫入時追加到文件末尾。
多個標志可通過位或(|)組合使用。
mode:指定新創建文件的權限(如 0644),僅在 flags 包含 O_CREAT 時有效。
返回值:
成功:返回文件描述符(非負整數)。
失敗:返回 -1,并設置 errno 表示錯誤(如 ENOENT 表示文件不存在)。?
第三個函數:int execvp(const char *file, char *const argv[]);?
作用:在 Linux 環境下,execvp 是一個 POSIX 系統調用,用于執行新程序,替換當前進程的鏡像。
參數:
file:要執行的程序名(可以是命令名如 "ls",無需完整路徑,execvp 會搜索 PATH 環境變量)。argv:指向參數數組的指針,包含程序名和傳遞給程序的參數,以 nullptr 結尾。
返回值:
成功:不返回(當前進程鏡像被替換)。
失敗:返回 -1,并設置 errno 表示錯誤(如 ENOENT 表示程序不存在)。
實現展示:
sort < unsorted.txt
ls -a -l > file.txt
ls -a -l >> file.txt
八、執行流程?
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;
}
?主循環:
打印提示符
讀取一行用戶輸入(回車前)
分析是否有重定向,截斷原命令并提取文件名
將命令行拆分為
g_argc/g_argv[]
檢測并執行內建命令(若是則跳過后續步驟)
啟動子進程執行外部命令
九、源碼
#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]# "#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 3int 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;
}const char* GetHostName()
{const char* hostname = getenv("HOSTNAME");return hostname == NULL ? "None" : hostname;
}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;
}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;for (int i = 0; g_env[i]; i++){putenv(g_env[i]);}environ = g_env;
}bool 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;
}void Echo()
{if (g_argc >= 2){for (int i = 1; i < g_argc; ++i){if (std::string(g_argv[i]) == "$?"){std::cout << lastcode;}else if (g_argv[i][0] == '$'){const char* env_value = getenv(g_argv[i] + 1);if (env_value)std::cout << env_value;}else{std::cout << g_argv[i];}if (i < g_argc - 1)std::cout << " ";}std::cout << std::endl;}else{std::cout << 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;out[strlen(out) - 1] = 0; if (strlen(out) == 0) return false;return true;
}bool CommandParse(char* commandline)
{
#define SEP " "g_argc = 0;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()
{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"){}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{}execvp(g_argv[0], g_argv);exit(1);}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;
}