文章目錄
- 前言
- 一、頭文件和全局變量
- 頭文件
- 全局變量
- 二、輔助函數
- 獲取用戶名
- 獲取主機名
- 獲取當前工作目錄
- 獲取最后一級目錄名
- 生成命令行提示符
- 打印命令行提示符
- 三、命令處理
- 獲取用戶輸入
- 解析命令行
- 執行外部命令
- 四、內建命令
- 添加環境變量
- 檢查和執行內建命令
- 五、初始化
- 初始化環境變量
- 主循環
- 總結
前言
??MyShell源代碼公開
??本篇是對之前知識的一個綜合運用,也是檢驗你是否對前置知識有個較為透徹的理解的好時機
一、頭文件和全局變量
頭文件
#include <iostream>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <string>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
全局變量
const int basesize = 1024;
const int argvnum = 64;
const int envnum = 64;// 全局的命令行參數表
char *gargv[argvnum];
int gargc = 0;// 全局的變量
int lastcode = 0;// 我的系統的環境變量
char *genv[envnum];// 全局的當前shell工作路徑
char pwd[basesize];
char pwdenv[basesize];
- basesize:緩沖區基本大小
- argvnum 和 envnum:參數和環境變量的最大數量
- gargv 和 gargc:存儲解析后的命令參數
- lastcode:存儲上一條命令的退出狀態碼
- genv:存儲環境變量
- pwd 和 pwdenv:存儲當前工作目錄
二、輔助函數
獲取用戶名
string GetUserName()
{string name = getenv("USER");return name.empty() ? "None" : name;
}
- 通過 getenv(“USER”) 獲取當前用戶名
- 如果獲取失敗返回 “None”
獲取主機名
string GetHostName()
{string hostname = getenv("HOSTNAME");return hostname.empty() ? "None" : hostname;
}
- 通過 getenv(“HOSTNAME”) 獲取主機名
- 如果獲取失敗返回 “None”
獲取當前工作目錄
string GetPwd()
{if(nullptr == getcwd(pwd, sizeof(pwd))) return "None";snprintf(pwdenv, sizeof(pwdenv),"PWD=%s", pwd);putenv(pwdenv); // PWD=XXXreturn pwd;
}
- 使用 getcwd() 獲取當前工作目錄
- 如果失敗返回 “None”
- 將當前目錄設置到環境變量 PWD 中
- 返回當前目錄路徑
獲取最后一級目錄名
string LastDir()
{string curr = GetPwd();if(curr == "/" || curr == "None") return curr;size_t pos = curr.rfind("/");if(pos == std::string::npos) return curr;return curr.substr(pos+1);
}
- 獲取當前目錄
- 如果是根目錄或無效目錄直接返回
- 查找最后一個 ‘/’ 的位置
- 返回最后一個 ‘/’ 之后的部分
生成命令行提示符
string MakeCommandLine()
{char command_line[basesize];snprintf(command_line, basesize, "[%s@%s %s]# ",\GetUserName().c_str(), GetHostName().c_str(), LastDir().c_str());return command_line;
}
- 生成類似 [user@host dirname]# 的提示符
打印命令行提示符
void PrintCommandLine() // 1. 命令行提示符
{printf("%s", MakeCommandLine().c_str());fflush(stdout);
}
- 打印提示符
- fflush(stdout) 確保立即顯示
三、命令處理
獲取用戶輸入
bool GetCommandLine(char command_buffer[], int size)
{char *result = fgets(command_buffer, size, stdin);if(!result){return false;}// 因為 command_line 里有一個 \n,我們把它替換成 \0 即可command_buffer[strlen(command_buffer)-1] = '\0';if(strlen(command_buffer) == 0) return false;return true;
}
- 使用 fgets 讀取用戶輸入
- 移除末尾的換行符
- 檢查是否為空輸入
解析命令行
??獲取用戶輸入后,我們需要將接收到的字符串拆分為命令及其參數。
??將接收到的字符串拆開
??通過 strtok 函數,我們可以將一個字符串按照特定的分隔符打散,依次返回子串
void ParseCommandLine(char command_buffer[], int len)
{(void)len;memset(gargv, 0, sizeof(gargv));gargc = 0;const char *sep = " ";gargv[gargc++] = strtok(command_buffer, sep);while((bool)(gargv[gargc++] = strtok(nullptr, sep)));gargc--;
}
- 重置參數數組和計數器
- 使用 strtok 以空格為分隔符分割命令
- 將分割后的參數存入 gargv 數組
- 調整 gargc 為實際參數數量
執行外部命令
bool ExecuteCommand()
{pid_t id = fork();if(id < 0) return false;if(id == 0){execvpe(gargv[0], gargv, genv);exit(1);}int status = 0;pid_t rid = waitpid(id, &status, 0);if(rid > 0){if(WIFEXITED(status)){lastcode = WEXITSTATUS(status);}else{lastcode = 100;}return true;}return false;
}
- 創建子進程
- 子進程使用 execvpe 執行命令
- 父進程等待子進程結束
- 保存子進程退出狀態到 lastcode
四、內建命令
??內建命令是指直接內置在操作系統內核中的一些命令,與普通的外部命令(外部程序文件)不同。這些內建命令是直接由shell解釋器(如Bash、Zsh等)所處理,而不需要通過外部文件的方式來執行。這些內建命令通常在操作系統的shell環境中被頻繁使用,并且執行速度更快,因為它們不需要創建新的進程來執行
??在Unix和類Unix操作系統中,通常會有一些內建命令,比如cd、echo、exit等。這些命令不需要單獨的可執行文件,而是直接由shell內核提供支持。當用戶在shell中輸入這些命令時,shell會直接處理它們,而不需要通過搜索系統路徑來找到可執行文件
??值得一提的是,某些shell也允許用戶通過自定義的方式添加新的內建命令,這樣用戶可以根據自己的需求來擴展shell的內建功能
添加環境變量
void AddEnv(const char *item)
{int index = 0;while(genv[index]){index++;}genv[index] = (char*)malloc(strlen(item)+1);strncpy(genv[index], item, strlen(item)+1);genv[++index] = nullptr;
}
- 找到環境變量數組的末尾
- 分配內存并復制新環境變量
- 確保數組以 NULL 結尾
檢查和執行內建命令
bool CheckAndExecBuiltCommand()
{if(strcmp(gargv[0], "cd") == 0){if(gargc == 2){chdir(gargv[1]);lastcode = 0;}else{lastcode = 1;}return true;}else if(strcmp(gargv[0], "export") == 0){if(gargc == 2){AddEnv(gargv[1]);lastcode = 0;}else{lastcode = 2;}return true;}else if(strcmp(gargv[0], "env") == 0){for(int i = 0; genv[i]; i++){printf("%s\n", genv[i]);}lastcode = 0;return true;}else if(strcmp(gargv[0], "echo") == 0){if(gargc == 2){if(gargv[1][0] == '$'){if(gargv[1][1] == '?'){printf("%d\n", lastcode);lastcode = 0;}}else{printf("%s\n", gargv[1]);lastcode = 0;}}else{lastcode = 3;}return true;}return false;
}
支持的內建命令有:
- cd:改變工作目錄
- export:設置環境變量
- env:顯示所有環境變量
- echo:打印內容或上一條命令的退出碼
五、初始化
初始化環境變量
void InitEnv()
{extern char **environ;int index = 0;while(environ[index]){genv[index] = (char*)malloc(strlen(environ[index])+1);strncpy(genv[index], environ[index], strlen(environ[index])+1);index++;}genv[index] = nullptr;
}
從父進程復制環境變量
主循環
int main()
{InitEnv();char command_buffer[basesize];while(true){PrintCommandLine();if( !GetCommandLine(command_buffer, basesize) ){continue;}ParseCommandLine(command_buffer, strlen(command_buffer));if ( CheckAndExecBuiltCommand() ){continue;}ExecuteCommand();}return 0;
}
主循環流程:
- 打印提示符
- 獲取用戶輸入
- 解析命令
- 嘗試執行內建命令
- 如果不是內建命令,則執行外部命令
總結
??感覺如何呢!