文章目錄
- 1.打印命令提示符
- 2.獲取用戶輸入指令
- 3.重定向分析
- 4.命令行參數表,環境變量表,初始化
- 5.命令解析
- 6.命令執行
- 6.1.創建子進程
- 6.2 處理內建命令
- 6.3 文件重定向
- 7.源碼
前言
在實現shell的時候我們先創建自己myshell目錄,在目錄中創建myshell.cc文件,因為shell本來是用c語言寫的,但為了方便我們這里使用c和c++混編。
首先我們做一個整體框架:
int main()
{//shell啟動的時候從系統獲得環境變量//我們的環境變量信息應該統一從父shell取InitEnv();while(1){//1.打印提示信息//printf("[%s@%s %s]#", GetUserName(),GetHostName(),GetPwd());PrintCommandPrompt();//2.獲取命令行提示符char commandline[COMMAND_SIZE];if(!GetCommandLine(commandline,sizeof(commandline))){continue;}//3.重定向分析 "ls -a > file.txt" -> "ls -a" "file.txt"//> >> < 判斷重定向的方向//> 輸出 >> 拼接輸出 < 輸入RedirCheck(commandline);//printf("redir = %d filename = %s\n", redir, filename.c_str());//4.命令行分析if(!CommandParse(commandline)){continue;}//PrintArgv();//5.檢測是否是內鍵命令if(CheckAndExecBuiltin()){continue;}//6.執行命令Execute();}//清理工作clear();return 0;
}
1.打印命令提示符
首先我們需要給用戶顯示提示信息,就像我們在使用shell時所看到的提示信息一樣,如下:
對它進行分析:
所以我們可以這樣定義宏,一個是方便打印的,一個是命令長度:
#define FORMAT "[%s@%s %s]#"
#define COMMAND_SIZE 1024
對于用戶名,主機號我們可以通過getenv從環境變量
中得到,但是獲取當前路徑我們不能使用getenv,因為我們myshell的環境變量使用的是父進程的環境變量,我們在當前使用cd命令切換路徑環境變量中的pwd并不會有改變。
所以獲取當前路徑,我們可以使用getcwd,直接查詢操作系統的文件系統,獲取當前進程的工作目錄的絕對路徑
。而不用依賴環境變量。當然我們最好單獨設計一個GetPwd把它封裝起來,這樣也方便把當前路徑加入到環境變量中。如下:
//for test
char cwd[1024];
char cwdenv[1024];//last exit code
int lastcode = 0;const char* GetPwd()
{//const char* pwd = getenv("PWD");const char* pwd = getcwd(cwd,sizeof(cwd));if(pwd != NULL){snprintf(cwdenv,sizeof(cwdenv),"PWD=%s",pwd);putenv(cwdenv);}return pwd == NULL ? "None" : pwd;
}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* GetHome()
{const char* home = getenv("HOME");return home == NULL ? "" : home;
}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,"[%s@%s %s]#",GetUserName(),GetHostName(),GetPwd());//snprintf(cmd_prompt,size,FORMAT,GetUserName(),GetHostName(),GetPwd());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);
}
2.獲取用戶輸入指令
獲取用戶輸入,因為用戶輸入的命令行參數是一個字符串,中間含有空格。所以我們不用scanf,cin進行輸入。這里我們使用fgets
。
bool GetCommandLine(char* out,int size)
{ char* c = fgets(out, size,stdin);if(c == nullptr){return false; }//去掉\nc[strlen(out) -1] = 0;if(strlen(c) == 0){return false;}return true;
}
3.重定向分析
在用戶輸入的指令中可能含有重定向操作,所以我們要提前特殊處理一下字符串,并把它做一個分割。
既然是重定向,也就是我們打開需要重向到的那個文件,所以我們需要獲取打開方式和文件名。
重定向有三種:
-
<:輸入重定向(以讀的方式打開文件)
-
>:輸出重定向(以寫的方式打開文件)
-
>>:追加重定向(以追加的方式打開文件)
所以我們可以使用宏來標記這些情況。
//3.關于重定向的內容
#define NONE_REDIR 0
#define INPUT_REDIR 1
#define OUTPUR_REDIR 2
#define APPEND_REDIR 3int redir = NONE_REDIR;
string filename;
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;//"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 = OUTPUR_REDIR;}cmd[end++] = 0;TrimSpace(cmd,end);filename = cmd + end;break;}else{end--;}}}
4.命令行參數表,環境變量表,初始化
在shell中有兩張表命令行參數表和環境變量表
,實質都是字符串數組。
- 命令行參數表:用來儲存用戶輸入的命令行參數。
- 環境變量表:用來儲存當前進程的屬性和狀態。
所以我們可以這樣做一個全局變量:
//shell自定義的全局變量
//1.命令行參數表
#define MAXARGC 128
char* g_argv[MAXARGC];
int g_argc = 0;//2.環境變量表
#define MAXENVS 100
char* g_env[MAXENVS];
int g_envs = 0;
環境變量表需要我們在程序啟動時就將它導入, 當然程序啟動后環境變量默認是父進程的,所以我們可以重新開辟空間把原環境變量的數據拷貝過來,然后再把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++;}//for testg_env[g_envs++] = (char*)"MIHAYOU=666"; g_env[g_envs] = NULL; //導成環境變量for(int i = 0;g_env[i];i++){putenv(g_env[i]);}environ = g_env;
}
5.命令解析
剛才我們獲取到了用戶的輸入得到一個字符串,需要把它一個一個按空格分開,來得到一張命令行參數表。方便后面做進程替換。
//命令行分析 "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--;return g_argc > 0 ? true : false;
}
6.命令執行
6.1.創建子進程
shell執行命令的實質就是進程替換
,我們在做進程替換的時候不想結束父進程,那么需要我們創建一個新的子進程,讓子進程來做替換。
6.2 處理內建命令
有一些命令比如cd,是一個內建命令,子進程是無法完成的,需要系統來執行。我們可以使用chdir來完成,chdir函數聲明如下:
bool Cd()
{if(g_argc == 1){string home = GetHome();if(home.empty()){return true;}chdir(home.c_str());}else{string where = g_argv[1];chdir(where.c_str());}return true;
}
它的作用是進入某個目錄,需要傳一個目錄的路徑。
這里也支持echo的一些操作
void Echo()
{if(g_argc == 2){//echo "hello"//echo $?//echo $PATHstring opt = g_argv[1];if(opt == "$?"){cout << lastcode << endl;}else if(opt[0] == '$'){string env_name = opt.substr(1);const char* env_value = getenv(env_name.c_str());if(env_value){cout << env_value << endl;}}else{cout << opt << endl;}}
}
bool CheckAndExecBuiltin()
{string cmd = g_argv[0];if(cmd == "cd"){Cd();return true;}else if(cmd == "echo"){Echo();return true;}return false;
}
6.3 文件重定向
- pcd文件數組:儲存了這個pcb打開的所有文件信息地址。
- 文件描述符(
記fd
):pcb的文件數組中的一個下標,0下標的文件為標準輸入流,1下標的文件為標準輸出流,2下標的文件為標準錯誤流,這三個都是系統默認打開的文件。 - 重定向:系統在對文件進行操作時只認fd,
所以重定向的實質就是一個fd位置的信息被其他fd的信息覆蓋。
" >,>>,指的都是從原來的標準輸出(fd=1)重定向到某個文件,< 從原來的標準輸入(fd=0)重定向到某個文件。所以這里我們只需要打開新的文件并獲取到它的fd,然后使用dup2把新文件的地址信息覆蓋到fd=1的文件上就行。然后關閉新文件的fd。"
int Execute()
{pid_t id = fork();if(id == 0){int fd = 0;//子進程檢測重定向if(redir == INPUT_REDIR){fd = open(filename.c_str(), O_RDONLY);if(fd < 0){exit(1);}dup2(fd,0);close(fd);}else if(redir == OUTPUR_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(3);}dup2(fd,1);close(fd);}else{}//進程替換不會改變重定向的內容//子進程執行execvp(g_argv[0],g_argv);//程序替換后面的程序就不執行了exit(1);}//父進程等待int status = 0;pid_t rid = waitpid(id,&status,0);////取消報警//(void)rid;if(rid > 0){lastcode = WEXITSTATUS(status);}return 0;
}
7.源碼
#include <iostream>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <string>
#include <sys/stat.h>
#include <fcntl.h>
using namespace std;#define COMMAND_SIZE 1024
#define FORMAT "[%s@%s %s]#"//shell自定義的全局變量
//1.命令行參數表
#define MAXARGC 128
char* g_argv[MAXARGC];
int g_argc = 0;//2.環境變量表
#define MAXENVS 100
char* g_env[MAXENVS];
int g_envs = 0;//for test
char cwd[1024];
char cwdenv[1024];//last exit code
int lastcode = 0;//3.關于重定向的內容
#define NONE_REDIR 0
#define INPUT_REDIR 1
#define OUTPUR_REDIR 2
#define APPEND_REDIR 3int redir = NONE_REDIR;
string filename;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",pwd);putenv(cwdenv);}return pwd == NULL ? "None" : pwd;
}const char* GetHome()
{const char* home = getenv("HOME");return home == NULL ? "" : home;
}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,"[%s@%s %s]#",GetUserName(),GetHostName(),GetPwd());//snprintf(cmd_prompt,size,FORMAT,GetUserName(),GetHostName(),GetPwd());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 == nullptr){return false; }//去掉\nc[strlen(out) -1] = 0;if(strlen(c) == 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--;return g_argc > 0 ? true : false;
}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++;}//for testg_env[g_envs++] = (char*)"MIHAYOU=666"; 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){string home = GetHome();if(home.empty()){return true;}chdir(home.c_str());}else{string where = g_argv[1];chdir(where.c_str());}return true;
}void Echo()
{if(g_argc == 2){//echo "hello"//echo $?//echo $PATHstring opt = g_argv[1];if(opt == "$?"){cout << lastcode << endl;}else if(opt[0] == '$'){string env_name = opt.substr(1);const char* env_value = getenv(env_name.c_str());if(env_value){cout << env_value << endl;}}else{cout << opt << endl;}}
}bool CheckAndExecBuiltin()
{string cmd = g_argv[0];if(cmd == "cd"){Cd();return true;}else if(cmd == "echo"){Echo();return true;}return false;
}void PrintArgv()
{for(int i = 0;g_argv[i];i++){printf("argv[%d]->%s\n", i,g_argv[i]);}printf("size = %d\n", g_argc);
}void clear()
{for(int i = 0;g_env[i];i++){free(g_env[i]);g_env[i] = NULL;}
}int Execute()
{pid_t id = fork();if(id == 0){int fd = 0;//子進程檢測重定向if(redir == INPUT_REDIR){fd = open(filename.c_str(), O_RDONLY);if(fd < 0){exit(1);}dup2(fd,0);close(fd);}else if(redir == OUTPUR_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(3);}dup2(fd,1);close(fd);}else{}//進程替換不會改變重定向的內容//子進程執行execvp(g_argv[0],g_argv);//程序替換后面的程序就不執行了exit(1);}//父進程等待int status = 0;pid_t rid = waitpid(id,&status,0);////取消報警//(void)rid;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;//"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 = OUTPUR_REDIR;}cmd[end++] = 0;TrimSpace(cmd,end);filename = cmd + end;break;}else{end--;}}}int main()
{//shell啟動的時候從系統獲得環境變量//我們的環境變量信息應該統一從父shell取InitEnv();while(1){//1.打印提示信息//printf("[%s@%s %s]#", GetUserName(),GetHostName(),GetPwd());PrintCommandPrompt();//2.獲取命令行提示符char commandline[COMMAND_SIZE];if(!GetCommandLine(commandline,sizeof(commandline))){continue;}//3.重定向分析 "ls -a > file.txt" -> "ls -a" "file.txt"//> >> < 判斷重定向的方向//> 輸出 >> 拼接輸出 < 輸入RedirCheck(commandline);//printf("redir = %d filename = %s\n", redir, filename.c_str());//4.命令行分析if(!CommandParse(commandline)){continue;}//PrintArgv();//5.檢測是否是內鍵命令if(CheckAndExecBuiltin()){continue;}//6.執行命令Execute();}//清理工作clear();return 0;
}