目錄
一、模塊框架圖
二、實現目標
三、實現原理
四、全局變量?
五、環境變量函數?
六、初始化環境變量表函數
七、輸出命令行提示符模塊
八、提取命令輸入模塊
九、填充命令行參數表模塊
十、檢測并處理內建命令模塊
十一、執行命令模塊
十二、源碼
一、模塊框架圖
二、實現目標
- 要能處理普通命令
- 要能處理內建命令
- 要能幫助我們理解內建命令/本地變量/環境變量這些概念
- 要能幫助我們理解shell的允許原理
三、實現原理
#?考慮下面這個與shell典型的互動:
[root@localhost epoll]# lsclient.cpp readme.md server.cpp utility.h[root@localhost epoll]# psPID TTY
TIME CMD3451 pts/0
3514 pts/0
00:00:00 bash00:00:00 ps
# ?下圖的時間軸來表?事件的發?次序。其中時間從左向右。shell由標識為sh的?塊代表,它隨著時間的流逝從左向右移動。shell從??讀?字符串"ls"。shell建??個新的進程,然后在那個進程中運?ls程序并等待那個進程結束。
#?然后shell讀取新的??輸?,建??個新的進程,在這個進程中運?程序 并等待這個進程結束。
四、全局變量?
- 我們的shell內部有兩張表命令行參數表和環境變量表
- 同時我們還要定義一張哈希表方便處理別名
- 定義兩個數組用來方便處理記錄路徑
- lastcode記錄上一次的進程退出碼、
- 宏定義大小方便開辟數組 以及命令行輸出格式
#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;// for test
char cwd[1024];
char cwdenv[1024];// last exit code
int lastcode = 0;
五、環境變量函數?
- ?環境標量函數直接調用getenv獲取在返回即可
- 注意GetPWD需要ssnprintf格式化寫入即可
- DirName直接從后面查找分割符\ 然后返回之后的字符串即可
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 = getenv("PWD");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;
}
string DirName(const char* pwd)
{
#define SEP "/"string ret = pwd;int index = ret.rfind(SEP);if (ret == SEP){return SEP;}if (index == string::npos){return "BUG?";}string t = ret.substr(index+1);return t;
}
六、初始化環境變量表函數
- 直接把enriron指向的環境變量表拷貝到我們的環境變量表里面 為了區分我們的shell和系統的我們在末尾添加上haha區分
- 再讓environ指向我們的環境變量表即可。
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++] = "haha";g_env[g_envs] = NULL;for (int i = 0; g_env[i]; i++){putenv(g_env[i]);cout << g_env[i] << endl;}environ = g_env;
}
七、輸出命令行提示符模塊
- 先定義一個字符輸出存儲命令行提示符
- 然后snprintf格式化寫入字符數組中 在輸出即可。
void PrintCommandPrompt()
{char cmd_prompt[COMMAND_SIZE];MakeCommandLine(cmd_prompt, sizeof(cmd_prompt));std::cout << cmd_prompt;
}
void MakeCommandLine(char cmd_prompt[], int size)
{snprintf(cmd_prompt, size, FORMAT, GetUserName(), GetHostName(), DirName(GetPwd()).c_str());
}
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 = getenv("PWD");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;
}
string DirName(const char* pwd)
{
#define SEP "/"string ret = pwd;int index = ret.rfind(SEP);if (ret == SEP){return SEP;}if (index == string::npos){return "BUG?";}string t = ret.substr(index+1);return t;
}
八、提取命令輸入模塊
- 這里先fgets獲取標準輸入到字符數組中
- 然后構造字符串刪除erase\0 然后去map中判斷是否為別名
- 如果是直接把value的值拷貝到數組中即可 然后return ture退出
- 不是別名則把用戶回車的\n字符消除 同時如果用戶只回車此時n==0
- 不做處理返回false 其他返回true;
bool GetCommandLine(char* commandline, int size)
{const char* ret=fgets(commandline, size, stdin);if (ret == NULL){return false;}std::string a = ret;a.erase(a.size() - 1, 1);if (alias_list.count(a)){strcpy(commandline, alias_list[a].c_str());return true;}int n = strlen(commandline);commandline[n - 1] = 0;if (n == 0){return false;}return true;
}
九、填充命令行參數表模塊
- 先strtok獲取指向第一個空格字符串
- while的那個ret不為空時填充g_argv參數表
- 繼續分割填充,直到找不到空格,說明字符串分割完畢
- 最后填充在g_argv表最后填充NULL即可
- 根據g_argv大小判斷是否填充成功 成功返回ture 反之返回flase?
bool CommandParse(char* commandline)
{
#define SEP " "g_argc = 0;char* ret = strtok(commandline, SEP);if (ret == NULL){return false;}while (ret){g_argv[g_argc++] = ret;ret = strtok(nullptr, SEP);}g_argv[g_argc] = ret;for (int i = 0; g_argv[i]; i++){cout << g_argv[i] << " ";}cout << endl;return g_argc > 0 ? true : false;
}
十、檢測并處理內建命令模塊
#?先根據g_argv表的第一個命令 判斷分流檢測處理
- cd命令如果只有cd 那就直接獲取家目錄的環境變量字符串
- chdir修改當前命令為家目錄即可
- 否則直接chdir修改當前目錄的路徑為g_argv[1]
- echo命令判斷分流
- echo $?直接返回lastcode退出碼 再設置wield0即可
- echo $xxx 直接獲取xxx的環境變量 再輸出即可
- echo xxx 直接打印xxx字符串g_argv[1]即可
- export命令先判斷是否填寫了要導入的環境變量
- 沒有直接返回ture不做處理 否則直接putenv導入g_argv[1]環境變量即可
- alias命令 這里只處理不帶命令選項的替換
- strtok分割=前后字符串然后 存儲到map中即可
bool CheckAndExecBuiltion()
{std::string t = g_argv[0];if (t == "cd"){return Cd();}else if (t == "echo"){return Echo();}else if (t == "export"){return Export();}else if (t == "alias"){cout << "開始替換" << endl;return Alias();}else{return false;}
}
bool Cd()
{std::string t;if (g_argc == 1){t = GetHome();if (t == ""){return true;}chdir(t.c_str());}else{t =g_argv[1];chdir(t.c_str());}return true;
}
bool Echo()
{std::string t = g_argv[1];if (t=="$?"){std::cout << lastcode << std::endl;lastcode = 0;}else if (t[0] == '$'){char* ret = getenv(t.substr(1).c_str());if (ret){cout << ret << endl;}}else{cout << t << endl;}return true;
}
bool Export()
{if (g_argc != 2){return true;}putenv(g_argv[1]);return true;
}
bool Alias()
{
#define SEP "="char* t = g_argv[1];char* ret = strtok(t, SEP);if (ret == NULL){return false;}string a=std::string(ret),b = std::string(strtok(NULL, SEP));alias_list[a] = b;cout << alias_list[a] <<" "<<alias_list.count(a)<<endl;cout << a<< "->" << b << endl;return true;
}
十一、執行命令模塊
- 直接創建子進程,子進程通過execvp程序替換執行命令,執行完后exit退出
- 然后父進程waitpid等待子進程,同時把lastcode更新即可
int Execute()
{pid_t id = fork();if (id == 0){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;
}
十二、源碼
#include<iostream>
#include<cstdio>
#include<cstring>
#include<cstdlib>
#include<unistd.h>
#include<sys/types.h>
#include<sys/wait.h>
#include<unordered_map>#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;// for test
char cwd[1024];
char cwdenv[1024];// last exit code
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 = getenv("PWD");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;//本來要從配置文件中來//1. 獲取環境變量for (int i = 0; environ[i]; i++){// 1.1 申請空間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"; //for_testg_env[g_envs] = NULL;//2. 導成環境變量for (int i = 0; g_env[i]; i++){putenv(g_env[i]);std::cout << g_env[i] << std::endl;}environ = g_env;
}//command
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];// cd - / cd ~if(where == "-"){// Todo}else if(where == "~"){// Todo}else{chdir(where.c_str());}}return true;
}void Echo()
{if(g_argc == 2){// echo "hello tata"// echo $?// echo $PATHstd::string opt = g_argv[1];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;}}
}bool Export()
{if (g_argc != 2){return false;}putenv(g_argv[1]);return true;
}bool Alias()
{char* t = g_argv[1];char* ret = strtok(t, "=");if (ret == NULL){return false;}string a=std::string(ret),b = std::string(strtok(NULL, "="));alias_list[a] = b;std::cout << alias_list[a] << " " << alias_list.count(a) << std::endl;std::cout << a << "->" << b << std::endl;return true;
}//絕對路徑改為目錄名
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); // +1是為了去掉目錄名前面的/
}//制作命令行提示符
void MakeCommandLine(char cmd_prompt[], int size)
{snprintf(cmd_prompt, size, FORMAT, GetUserName(), GetHostName(), Dirname(GetPwd()).c_str());
}//打印命令行提示符
void PrintCommand()
{char prompt[COMMAND_SIZE];MakeCommandLine(prompt, sizeof(prompt));printf("%s", prompt);fflush(stdout);
}//判斷輸入內容
bool GetCommandLine(char *out, int size)
{//scanf()是以空格作為分隔符的,但是我們的命令是一整個字符串//ls -a -l => "ls -a -l\n”字符串char *c = fgets(out, size, stdin);if(c == NULL) return false;out[strlen(out)-1] = 0; // 清理\nif(strlen(out) == 0) return false;return true;
}//命令行分析 "ls -a -l" => "ls" "-a" "-l"
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--; // NULL不算,去掉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("g_argc:%d\n", g_argc);
}//處理并執行內建命令
bool CheckAndExcuteBuiltn()
{std::string cmd = g_argv[0];if(cmd == "cd"){Cd();return true;}else if(cmd == "echo"){Echo();return true;}else if(cmd == "export"){Export();return true;}else if(cmd == "alias"){std::cout << "開始替換" << std::endl;return Alias();}elsereturn false;
}//執行命令
int Execute()
{pid_t id = fork();if(id == 0){//childexecvp(g_argv[0], g_argv);exit(1);}int status = 0;//fathepid_t rid = waitpid(id, &status, 0);if(rid > 0){lastcode = WEXITSTATUS(status);}return 0;
}int main()
{// shell啟動的時候需要從系統中獲取環境變量// 我們的環境變量信息應該從父shell統一來InitEnv();while(true)//持續運行{// 1. 輸出命令行提示符PrintCommand();// 2. 獲取用戶輸入的命令char commandline[COMMAND_SIZE];if(!GetCommandLine(commandline, sizeof(commandline)))continue;//什么都沒輸入直接回車,繼續運行printf("echo %s\n", commandline);// 3. 命令行分析 "ls -a -l" => "ls" "-a" "-l"if(!CommandParse(commandline))continue;//PrintArgv(); //測試// 4. 檢測并處理內建命令if(CheckAndExcuteBuiltn())continue;// 5. 執行命令Execute();}// ClearUp();return 0;
}