shell
的原理
我們知道,我們程序啟動時創建的進程,它的父進程都是bash
也就是shell
命令行解釋器;
那bash
都做了哪些工作呢?
根據已有的知識,我們可以簡單理解為:
- 輸出命令行提示符
- 獲取并解析我們輸入的指令
- 執行內建命令或者創建子進程執行命令
就如下圖所示,bash
讀取我們輸入的命令,并進行解析;然后創建子進程執行命令(bash
等待子進程退出)。
自定義shell
實現
根據上述bash
的工作原理,我們現在實現一個簡單的自定義shell
;
要想實現一個自定義shell
,我們就要執行以下過程:
- 獲取命令行
- 解析命令行
- 創建子進程,讓子進程執行命令(使用程序替換)
shell
等待子進程退出
當然,還存在一部分內建命令,它是由bash
自主實現的;我們要進行特殊處理;
1. 輸出命令行提示符
在實現自定義shell
之前,我們來看
我們的bash
在每次都會輸出命令行提示符,然后等待我們用戶輸入;
看這個命令行提示符,它包含以下信息:
- 用戶名
USER
;- 主機名
HOSTNAME
;- 當前工作路徑
PWD
;這些在我們的環境變量表中都能夠找到,所以我們就可以使用
getenv
來獲取。
所以這個就非常容易實現了,直接按照格式輸出即可;
這樣我們需要獲取環境變量USER
、HOSTNAME
、PWD
等;
但是我們會發現bash
輸出的命令行提示符中的當前工作路徑只有當前文件,而我們通過環境變量PWD
獲取的是當前工作目錄的絕對路徑,所以我們這里要進行一下分割;
詳細代碼如下:
//命令行提示符格式
#define CLP "[%s@%s %s]#"
//命令行提示符的最大長度
#define MAX_CLP 100
//獲取環境變量
const char* GetUser(){return getenv("USER");
}
const char* GetHostName(){return getenv("HOSTNAME");
}
const char* GetPwd()
{return getenv("PWD");
}
//分割路徑
//"/home/lxb/linux/MYSHELL" --> "MYSHELL"
string DirPwd(char s[])
{
#define SLASH "/"string str = s;if(str == SLASH) return str; auto pos = str.rfind(SLASH);if(pos == std::string::npos) return "err";return str.substr(pos+1);
}
//生成命令行提示符
void CommandLinePrompt(char buffer[])
{sprintf(buffer,CLP,GetUser(),GetHostName(),DirPwd(GetPwd());
}
//輸出命令行提示符
void PrintCommandPrompt()
{char buffer[100];CommandLinePrompt(buffer);printf("%s",buffer);fflush(stdout);
}
2. 獲取用戶輸入的信息
輸出了命令行提示符,接下來就要獲取用戶輸入的信息了,也就是輸入的命令;
在用戶輸入時,是會輸入空格的,所以這里我們不能使用scanf/cin
進行輸入;我們要使用fgets
進行輸入。
而也可能存在只輸入一個回車的情況,所以我們要進行特殊判斷:當只輸入一個回車時就再次輸出命令行提示符,然后等待用戶輸入。
輸入:
//命令行信息最大長度
#define MAX_COMLINE 1024
char* GetCommandLine(char buff[]){char* c = fgets(buff,MAX_COMLINE,stdin);buff[strlen(buff)-1] = 0;//處理回車return c;
}
這里來測試一下輸出命令行提示符和獲取用戶輸入信息;
如果獲取用戶輸入信息成功,那就輸出獲取的輸入信息,如果失敗或者只輸入了一個回車就再次輸出命令行提示符,然后等待用戶輸入。
int main()
{while(1){//1. 輸出命令行提示符PrintCommandPrompt();//2. 獲取用戶輸入信息char buff[MAX_COMLINE];char* c = GetCommandLine(buff);if(c == NULL)//讀取用戶輸入信息失敗continue;if(strlen(buff) == 0)//只輸入了空格continue;printf("%s\n",buff);}return 0;
}
3. 命令行解析
獲取了用戶輸入的信息,但是我們獲得的是一個字符串,而我們要想執行用戶輸入的命令,要先對這個字符串進行解析;生成對應的命令行參數表,才能夠去執行。
命令行參數個數g_argc
,命令行參數表g_argv
;我們可以設置成全局的,這樣每次通過修改argc
和argv
中最后一個指針為NULL
即可。
這里,我們可以使用
strtok
函數進行分割命令行參數;簡單描述一下
strtok
,在str
字符串中查找sep
字符串的內容,找到并將其修改成\0
并返回指向這個字符串的指針。
在分割完成之后,我們直接讓g_argv
命令行參數表指向對應位置即可。
#define MAX_ARGC 50
//命令行參數表
int g_argc;
char* g_argv[MAX_ARGC];
//解析命令行參數
//"ls -a -l"--> "ls" "-a" "-l"
void PrasCommandLine(char buff[]){ g_argc = 0; const char* sep = " "; for(g_argv[g_argc] = strtok(buff,sep);g_argv[g_argc] != NULL; g_argv[g_argc] = strtok(NULL,sep)) g_argc++;
}
這里還是測試,命令行解析是否成功。
4. 創建子進程執行命令
解析命令行,生成命令行參數表之后,現在就是去執行命令了;
我們的shell
并不是自己去執行,而是創建子進程,然后讓子進程去執行命令,shell
等待子進程退出。
void CreateChildExecute(){ int id = fork(); if(id < 0) { perror("fork"); exit(1); } else if (id == 0){ //child execvp(g_argv[0],g_argv); exit(2); } //parent wait(NULL);
}
這里我們使用的程序替換函數是
execvp
,我們有命令行參數表(數組),而且我們輸入的系統命令是不帶路徑的;
看一下運行效果:
擴展部分
在上述描述中,簡單的shell
運行就OK了;
但是上述我們沒有考慮內建命令
、環境變量表
等這些東西;
環境變量表
在bash
啟動時,它的環境變量表從我們系統的配置文件中來,但是我們這里沒辦法從系統配置文件中讀;所以我們這里就只能從父進程bash
獲取環境變量表;
這里即從
bash
中獲取環境變量;但是拿到了環境變量表,進程中還是保存的來自父進程
bash
的環境變量;environ
還是執行bash
的環境變量表。我們需要導出環境變量,使用
putenv
來導出環境變量;然后讓environ
執行我們的環境遍歷表。
//環境變量表最大數量
#define MAX_GENV 500
int g_argc;
char* g_argv[MAX_GARGC];
//環境變量表
int g_envs;
char* g_env[MAX_GENV];
//導入環境變量
void EnvInit(){ extern char** environ; memset(g_env,0,sizeof(g_env)); g_envs = 0; //環境變量表要從系統文件中來 //這從bash中獲取 for(int i = 0;environ[i]!=NULL;i++){ g_env[i] = (char*) malloc(strlen(environ[i])+1); if(g_env[i] == NULL){ perror("malloc"); exit(3); } strcpy(g_env[i], environ[i]); g_envs++; } g_env[g_envs] = NULL; //導出環境變量 for(int i = 0;i < g_envs;i++){ putenv(g_env[i]); } environ = g_env;
}
在我們程序啟動時,從父進程bash
獲取環境變量即可。
內建命令
內建命令,指
bash
不創建子進程去執行,而是bash
自己去執行的命令;我們現在知道內建命令有
cd
、export
、echo
等。
cd
cd
命令,仔細想一想,肯定不會是子進程執行的;因為子進程執行它修改的是子進程的工作路徑。
我們要讓shell
去執行cd
命令,肯定不能使用程序替換了,我們可以使用chdir
系統調用來修改當前工作路徑;
cd
命令:
cd
:會進入用戶的家目錄cd ~
:進入用戶的家目錄cd
where
:進入指定路徑cd -
:進入上次的工作路徑
void CD(){std::string oldpwd = getenv("PWD");std::string where;if(g_argc == 1){where = GetHome();if(where.empty()) return;chdir(where.c_str()); }else{where = g_argv[1];if(strcmp("-", g_argv[1]) == 0){where = getenv("OLDPWD");}else if(strcmp("~", g_argv[1]) == 0){where = GetHome();if(where.empty()) return;}chdir(where.c_str());//修改環境變量}
}
當然呢,這里存在一個問題,當我們
cd -
進入上次各種目錄時就會發現,它進入的一直都是同一個目錄;因為我們這里沒有修改環境變量
OLDPWD
。
echo
echo
命令也是內建命令,我們知道,echo $?
可以查看最近一次進程退出時的退出碼;
但是在我們的shell
中,如果讓子進程去執行echo $?
,它則是直接輸出$?
。
echo $?
,查看最近一次進程退出時的退出碼;而這些退出碼在哪里呢?肯定不會在子進程中,那就在
bash
中了;
所以在我們的shell
中,我們可以定義一個全局變量,每次執行一次命令就對其進行一次修改。
//最近一次進程退出時的退出碼
int last_code;
void Echo(){ if(g_argc == 2){ std::string str = g_argv[1]; if(str == "$?"){ std::cout<<last_code<<std::endl; } else if(str[1] == '$'){ std::string env_name = str.substr(1); const char* s = getenv(env_name.c_str()); if(s) std::cout<<s<<std::endl; } else{ std::cout<<str<<std::endl; } }
}
這里,設置了
last_code
,那在每次執行命令之后,都要進行更新last_code
。
除此之外呢,還有非常多的內建命令,比如export
、unset
等;這里就不實現了。
別名alias
如果測試我們可以發現,bash
支持ll
,而我們的shell
是不支持的;
我們知道
ll
是別名,所以如果想要我們shell
支持別名,我們就要在shell
中新增一張別名表;然后維護這張別名表,就可以支持
ll
等指令的別名了。
這里就不實現了,可以使用unordered_map
或者map
來存儲這張別名表。
到這里本篇文章大致內容就結束了;
本篇文章自定義實現
shell
,幫助理解進程,以及bash
是如何工作的
附源碼:
#include <iostream>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <unistd.h>
#include <cstdbool>
#include <sys/types.h>
#include <sys/wait.h>
#include <string>
//命令行提示符格式
#define CLP "[%s@%s %s]# "
#define MAX_CLP 100
//命令行信息最大長度
#define MAX_COMLINE 1024
//命令行參數最大個數
#define MAX_GARGC 50
//環境變量表最大數量
#define MAX_GENV 500
int g_argc;
char* g_argv[MAX_GARGC];
//環境變量表
int g_envs;
char* g_env[MAX_GENV];
//最近一次進程退出時的退出碼
int last_code = 0;
//導入環境變量
void EnvInit(){extern char** environ;memset(g_env,0,sizeof(g_env));g_envs = 0;//環境變量表要從系統文件中來 //這從bash中獲取for(int i = 0;environ[i]!=NULL;i++){g_env[i] = (char*) malloc(strlen(environ[i])+1);if(g_env[i] == NULL){perror("malloc");exit(3);}strcpy(g_env[i], environ[i]);g_envs++; }g_env[g_envs] = NULL;//導出環境變量for(int i = 0;i < g_envs;i++){putenv(g_env[i]);}environ = g_env;
}
//獲取環境變量
char* GetUser(){return getenv("USER");
}
char* GetHostName(){return getenv("HOSTNAME");
}
//路徑切割
std::string DirPwd(const char s[])
{
#define SLASH "/"std::string str = s;if(str == SLASH) return str;auto pos = str.rfind(SLASH);if(pos == std::string::npos) return "err";return str.substr(pos+1);
}
const char* GetPwd()
{//return getenv("PWD");return DirPwd(getenv("PWD")).c_str();
}
const char* GetHome(){return getenv("HOME");
}
//生成命令行提示符
void CommandLinePrompt(char buffer[])
{sprintf(buffer,CLP,GetUser(),GetHostName(),GetPwd());//sprintf(buffer,CLP,GetUser(),GetHostName(),DirPwd(GetPwd()).c_str());
}
void PrintCommandPrompt()
{char buffer[100];CommandLinePrompt(buffer);printf("%s",buffer);fflush(stdout);
}
char* GetCommandLine(char buff[]){char* c = fgets(buff,MAX_COMLINE,stdin);buff[strlen(buff)-1] = 0;return c;
}
void PrasCommandLine(char* buff){g_argc = 0;const char* sep = " ";for(g_argv[g_argc] = strtok(buff,sep); g_argv[g_argc] != NULL; g_argv[g_argc] = strtok(NULL,sep)){g_argc++;}
}
void CreateChildExecute(){int id = fork();if(id < 0){perror("fork");exit(1); }else if (id == 0){//childexecvp(g_argv[0],g_argv);exit(2);}//parentint status = 0;int rid = wait(&status);if(rid > 0)last_code = WEXITSTATUS(status);
}
void Cd(){std::string oldpwd = getenv("PWD");std::string where;if(g_argc == 1){where = GetHome();if(where.empty()) return; chdir(where.c_str());}else{where = g_argv[1];if(strcmp("-", g_argv[1]) == 0){where = getenv("OLDPWD");}else if(strcmp("~", g_argv[1]) == 0){where = GetHome();if(where.empty()) return;}chdir(where.c_str());//修改環境變量}//std::string old = std::string("OLDPWD=") + oldpwd;//char* arr = (char*)malloc(old.size()+1);//for(size_t i = 0;i<old.size();i++){// arr[i] = old[i];//}//arr[old.size()] = 0;//putenv(arr);
}
void Echo(){if(g_argc == 2){ std::string str = g_argv[1];if(str == "$?"){std::cout<<last_code<<std::endl;}else if(str[1] == '$'){std::string env_name = str.substr(1);const char* s = getenv(env_name.c_str());if(s)std::cout<<s<<std::endl;}else{std::cout<<str<<std::endl;}}
}
//判斷內建命令
bool BinCommand(){std::string str = g_argv[0];if(str == "cd"){Cd();last_code = 0;return true;}else if(str == "echo"){Echo();last_code = 0;return true;}return false;
}
void PrintArgv(){for(int i = 0;i < g_argc; i++){printf("g_argv[%d] : %s\n",i,g_argv[i]);}
}
void PrintEnv(){for(int i = 0; i < g_envs;i++){printf("g_env[%d] : %s\n",i,g_env[i]);}
}
int main()
{//獲取環境變量表EnvInit();//PrintEnv();while(1){ //1. 輸出命令行提示符PrintCommandPrompt();//2. 獲取用戶輸入信息char buff[MAX_COMLINE];char* c = GetCommandLine(buff);if(c == NULL)//讀取用戶輸入信息失敗continue;if(strlen(buff) == 0)//只輸入了空格continue;//3. 命令行解析PrasCommandLine(buff);//4.內建命令if(BinCommand())continue;//5. 創建子進程執行命令CreateChildExecute();}return 0;
}