目錄
1、目標
2、顯示命令提示符
2.1 getenv
2.2 getcwd
2.3 putenv
3、獲取用戶輸入的命令
4、解析命令
5、處理內建命令
6、處理外部命令
7、完整代碼
7.1 myshell.cpp
7.2 Makefile
1、目標
實現一個Linux的myshell,有以下基本的功能。
- 顯示命令提示符
- 獲取用戶輸入的命令
- 解析命令
- 處理內建命令
- 處理外部命令
myshell有一張命令行參數表和環境變量表(繼承bash的,其實應該要從配置文件中獲取,但比較麻煩)。
2、顯示命令提示符
這里就要了解一下:getenv(),getcwd(),putenv()了。
2.1 getenv
獲取環境變量。
char *getenv(const char *name);
-
根據環境變量名(如 "PATH")返回其對應的值(字符串)。
-
如果變量不存在,返回 NULL。
2.2 getcwd
獲取當前工作路徑。
char *getcwd(char *buf, size_t size);
-
將當前工作目錄的絕對路徑寫入 buf,并返回 buf。
-
需確保 buf?足夠大(否則返回 NULL,errno = ERANGE)。
注意:
進程的環境變量表中的PWD,是根據進程的CWD(進程當前的工作路徑,在/proc/pid/下可以看到),進行更新的。?
2.3 putenv
設置環境變量。
int putenv(char *string);
-
設置環境變量,格式為 "KEY=VALUE"?的字符串。如果已存在相同的KEY,就覆蓋VALUE。
-
成功返回 0,失敗返回非零。
注意:
putenv(),傳的是指針,要放在環境變量表里,生命周期要和程序一樣長,所以傳全局變量。
改變這個全局變量,環境變量表中也會改變(當時不太理解,中坑了。)
const char* getUserName()
{const char* USER = getenv("USER");return USER == NULL?"None":USER;
}const char* getHostName()
{const char* HOSTNAME = getenv("HOSTNAME");return HOSTNAME == NULL?"None":HOSTNAME;
}std::string getCwd()
{char cwdenv[1024];char* cwd = getcwd(cwdenv,sizeof(cwdenv));return cwd == NULL? "None":cwdenv; // 因為返回的是局部變量,所以使用string,進行拷貝
}char pwd[1024];
void cmdPrompt()
{std::string path = getCwd();// 更新環境變量PWD,不能putenv局部變量。snprintf(pwd,sizeof(pwd),"PWD=%s",path.c_str());putenv(pwd);if(path.size() != 1) // 不是/根目錄{int index = path.rfind('/');path = path.substr(index+1);}std::cout<<"["<<getUserName()<<"@"<<getHostName()<<" "<<path<<"]"<<"$$";
}
3、獲取用戶輸入的命令
bool getCmd(char cmd[],int size)
{char* s = fgets(cmd,size,stdin);if(s != NULL){int len = strlen(cmd);cmd[len-1] = '\0'; // fgets會讀取'\n',需要處理}return s != NULL;
}
獲取成功,返回true,獲取失敗,返回false。?
4、解析命令
bool parseCmd(char cmd[],char* argv[])
{const char* delim = " ";char* token = strtok(cmd,delim);int i = 0;while(token != NULL){argv[i] = token;token = strtok(NULL,delim);++i;}argv[i] = NULL; // 進程替換,要求以NULL結尾return i != 0;
}
char *strtok(char *str, const char *delim);
-
str:要分割的字符串。第一次調用時傳入原始字符串,后續調用傳入 NULL。
-
delim:包含所有可能分隔符的字符串。
-
返回分割出的子字符串的指針。如果沒有更多子字符串,則返回 NULL。
-
strtok 會在找到的分隔符位置插入 '\0' 字符,因此會修改原始字符串。
5、處理內建命令
內建命令,需要父進程自己執行(如:cd,需要改變自己的路徑),或父進程自己執行,效率更高(如:echo)。?
std::string getCwd()
{char cwdenv[1024];char* cwd = getcwd(cwdenv,sizeof(cwdenv));return cwd == NULL? "None":cwdenv; // 因為返回的是局部變量,所以使用string,進行拷貝
}const char* getHome()
{const char* HOME = getenv("HOME");return HOME == NULL?"None":HOME;
}const char* getOldPwd()
{const char* OLDPWD = getenv("OLDPWD");return OLDPWD == NULL?"None":OLDPWD;
}char oldPwd[1024];
void cd(char* argv[])
{ std::string cwdenv = getCwd();if(argv[1] == NULL || strcmp(argv[1],"~") == 0){snprintf(oldPwd,sizeof(oldPwd),"OLDPWD=%s",cwdenv.c_str());putenv(oldPwd); // 更新OLDPWDchdir(getHome());}else if(strcmp(argv[1],"-") == 0){chdir(getOldPwd());snprintf(oldPwd,sizeof(oldPwd),"OLDPWD=%s",cwdenv.c_str());putenv(oldPwd);}else{snprintf(oldPwd,sizeof(oldPwd),"OLDPWD=%s",cwdenv.c_str());putenv(oldPwd); // 更新OLDPWDchdir(argv[1]);}
}int lastExitno = 0;
void echo(char* argv[])
{if(argv[1] == NULL)return;std::string cmd = argv[1];// echo $?// echo $PATH// echo "hello world"if(cmd[0] == '$'){if(cmd.substr(1) == "?"){std::cout<<lastExitno<<std::endl;lastExitno = 0;}else{if(getenv(cmd.substr(1).c_str()))std::cout<<getenv(cmd.substr(1).c_str())<<std::endl;}}else{std::cout<<cmd<<std::endl;}
}bool executeBuiltIn(char* argv[])
{std::string cmd = argv[0];if(cmd == "cd"){cd(argv);return true;}else if(cmd == "echo"){echo(argv);return true;}// ...else{}return false;
}
注意:
else if(strcmp(argv[1],"-") == 0){chdir(getOldPwd());snprintf(oldPwd,sizeof(oldPwd),"OLDPWD=%s",cwdenv.c_str());putenv(oldPwd);}
如果之前putenv(oldPwd),傳的是指針,
若先snprintf(oldPwd,sizeof(oldPwd),"OLDPWD=%s",cwdenv.c_str());
那么,環境變量表中的OLDPWD就已經是CWD了,那么chdir(getOldPwd());就出錯了。
6、處理外部命令
外部命令,防止父進程掛了,所以創建子進程,進行程序替換(可執行任意程序)。?
int executeExternal(char* argv[])
{pid_t id = fork();if(id == -1)return 1;if(id == 0){execvp(argv[0],argv);exit(2);}int status = 0;pid_t wid = waitpid(id,&status,0);if(wid == id && WIFEXITED(status)) // 子進程退出,且是正常退出lastExitno = WEXITSTATUS(status);return 0;
}
7、完整代碼
7.1 myshell.cpp
#include <iostream>
#include <stdio.h>
#include <string>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>const char* getUserName()
{const char* USER = getenv("USER");return USER == NULL?"None":USER;
}const char* getHostName()
{const char* HOSTNAME = getenv("HOSTNAME");return HOSTNAME == NULL?"None":HOSTNAME;
}const char* getHome()
{const char* HOME = getenv("HOME");return HOME == NULL?"None":HOME;
}std::string getCwd()
{char cwdenv[1024];char* cwd = getcwd(cwdenv,sizeof(cwdenv));return cwd == NULL? "None":cwdenv; // 因為返回的是局部變量,所以使用string,進行拷貝
}const char* getOldPwd()
{const char* OLDPWD = getenv("OLDPWD");return OLDPWD == NULL?"None":OLDPWD;
}char pwd[1024];
void cmdPrompt()
{std::string path = getCwd();// 更新環境變量PWD,不能putenv局部變量。snprintf(pwd,sizeof(pwd),"PWD=%s",path.c_str());putenv(pwd);if(path.size() != 1) // 不是/根目錄{int index = path.rfind('/');path = path.substr(index+1);}std::cout<<"["<<getUserName()<<"@"<<getHostName()<<" "<<path<<"]"<<"$$";
}bool getCmd(char cmd[],int size)
{char* s = fgets(cmd,size,stdin);if(s != NULL){int len = strlen(cmd);cmd[len-1] = '\0'; // fgets會讀取'\n',需要處理}return s != NULL;
}bool parseCmd(char cmd[],char* argv[])
{const char* delim = " ";char* token = strtok(cmd,delim);int i = 0;while(token != NULL){argv[i] = token;token = strtok(NULL,delim);++i;}argv[i] = NULL; // 進程替換,要求以NULL結尾return i != 0;
}char oldPwd[1024];
void cd(char* argv[])
{ std::string cwdenv = getCwd();if(argv[1] == NULL || strcmp(argv[1],"~") == 0){snprintf(oldPwd,sizeof(oldPwd),"OLDPWD=%s",cwdenv.c_str());putenv(oldPwd); // 更新OLDPWDchdir(getHome());}else if(strcmp(argv[1],"-") == 0){chdir(getOldPwd());snprintf(oldPwd,sizeof(oldPwd),"OLDPWD=%s",cwdenv.c_str());putenv(oldPwd);}else{snprintf(oldPwd,sizeof(oldPwd),"OLDPWD=%s",cwdenv.c_str());putenv(oldPwd); // 更新OLDPWDchdir(argv[1]);}
}int lastExitno = 0;
void echo(char* argv[])
{if(argv[1] == NULL)return;std::string cmd = argv[1];// echo $?// echo $PATH// echo "hello world"if(cmd[0] == '$'){if(cmd.substr(1) == "?"){std::cout<<lastExitno<<std::endl;lastExitno = 0;}else{if(getenv(cmd.substr(1).c_str()))std::cout<<getenv(cmd.substr(1).c_str())<<std::endl;}}else{std::cout<<cmd<<std::endl;}
}bool executeBuiltIn(char* argv[])
{std::string cmd = argv[0];if(cmd == "cd"){cd(argv);return true;}else if(cmd == "echo"){echo(argv);return true;}// ...else{}return false;
}int executeExternal(char* argv[])
{pid_t id = fork();if(id == -1)return 1;if(id == 0){execvp(argv[0],argv);exit(2);}int status = 0;pid_t wid = waitpid(id,&status,0);if(wid == id && WIFEXITED(status)) // 子進程退出,且是正常退出lastExitno = WEXITSTATUS(status);return 0;
}int main()
{while(true){// 命令提示符cmdPrompt();// 獲取用戶輸入命令char cmd[1024] = {0};if(!getCmd(cmd,sizeof(cmd)))continue;// 解析命令char* argv[1024] = {0};if(!parseCmd(cmd,argv))continue;// 內建命令if(executeBuiltIn(argv))continue;// 執行命令executeExternal(argv);}return 0;
}
7.2 Makefile
TARGET := myshell
SRCS := myshell.cpp
SUFFIX := .cpp
OBJS := $(SRCS:$(SUFFIX)=.o)
CC := g++$(TARGET): $(OBJS)$(CC) -o $@ $^%.o: %$(SUFFIX)$(CC) -c $<.PHONY: clean
clean:rm -f $(OBJS) $(TARGET)