深入探究Linux shell的實(shí)現(xiàn)原理
一、打印命令行提示符
const char* getusername() // 獲取用戶名 { return getenv("USER"); } const char* gethostname() // 獲取主機(jī)名 { return getenv("HOSTNAME"); } const char* getpwd() // 獲取當(dāng)前所處的目錄 { char* pos = strrchr(getenv("PWD"), '/'); // 查找最后一個 ‘/' if(*(pos+1) != '\0') return pos+1; // 說明不是根目錄,返回最后一個文件夾 return pos; } void tooltip() // 打印命令行提示框 { printf(LEFT "%s@%s %s" RIGHT PROMPT" ", getusername(), gethostname(), getpwd()); }
代碼分析:獲取基礎(chǔ)信息本質(zhì)上是通過調(diào)用 getenv
接口來獲取對應(yīng)環(huán)境變量的值。借助 strrchr
函數(shù)來查找當(dāng)前路徑中的最后一個文件分隔符 /
,它有可能是文件分隔符也有可能是根目錄因此要單獨(dú)判斷。
二、讀取鍵盤輸入的指令
char command[1024]; // 存儲鍵盤輸入的指令 int getcommand(char* command, int size) // 讀取指令 { memset(command, '\0', size); char* ret = fgets(command, size, stdin); // 這里 ret 一定不為空,因?yàn)橹辽贂斎胍粋€回車,fgets 可以讀取回車 assert(ret != NULL); (void)ret;// “假裝使用一下ret,防止有些編譯器警告” // aaabc\n\0 command[strlen(command)-1] = '\0'; // 去掉結(jié)尾的 \n return 1; } int interact(char* command, int size) // 交互 { tooltip(); while(getcommand(command, size) && (strlen(command) == 0)) { tooltip(); } } int main() { interact(command, sizeof(command)); // 交互 printf("echo: %s\n", command); return 0; }
代碼分析:鍵盤輸入的指令本質(zhì)上就是一串字符串,這里不能用 scanf 來獲取字符串,因?yàn)?scanf 是不會讀取空格和回車的(遇到空格和回車就停止讀?。?,而我們一般的指令都是帶選項(xiàng)的,指令和選項(xiàng)之間一般會用空格隔開,用 scanf 會導(dǎo)致我們指令讀不全。這里使用 fgets 函數(shù)來讀取鍵盤輸入,其第一參數(shù)是存儲指令的空間的首地址;第二個參數(shù)是空間的大小;第三個參數(shù)是從哪個文件流中讀取,一個 C/C++ 程序默認(rèn)會打開三個文件流 stdin、stdout、stderr,這里選擇從 stdin 中讀取,也就是從標(biāo)準(zhǔn)輸入中讀取。gets 函數(shù)會在結(jié)尾自動幫我們添加 \0,并且當(dāng)讀取的字符個數(shù)大于存儲容量時,該函數(shù)會自動在結(jié)尾放 \0,因此我們可以不用考慮為 \0 預(yù)留空間或者認(rèn)為的在字符串結(jié)尾加 \0。其次該函數(shù)讀取成功返回 command 的首地址,否則返回 NULL,在當(dāng)前場景下,除非讀取錯誤,否則至少都會讀入一個 \n,一般我們輸入完指令就是敲回車,什么指令不輸也敲回車,因此正常情況下 ret 不可能為 NULL。這里還要考慮刪除掉讀取到的 \n,因?yàn)槲覀儾恍枰?,我們只要完整的指令?/p>
三、指令切割
#define SEPARATOR " " // 指令分隔符 char* argv[ARGC_LONG] = {NULL}; // 存儲指令和選項(xiàng)的起始地址 void commandcut(char* command, char** argv, int argvsize) // 指令切割 { memset(argv, 0, argvsize); // 清空 char cop_command[COMMAND_LONG] = {'\0'}; // 保證 command 串不被改變 for(int i = 0; command[i] != '\0'; i++) { cop_command[i] = command[i]; } // 開始切割子串 char* ret = strtok(cop_command, SEPARATOR); int i = 0; while(ret != NULL) { argv[i++] = ret; ret = strtok(NULL, " "); } } int main() { while(1) { // 1、交互獲取命令行參數(shù) interact(command, sizeof(command)); // 交互 // 到這里說明指令已經(jīng)獲取到了,接下來將指令打散 // 2、指令切割 commandcut(command, argv, sizeof(argv)); for(int i = 0; argv[i]; i++) { printf("[%d]: %s\n", i, argv[i]); } printf("echo: %s\n", command); } return 0; }
代碼分析:這一步主要是借助 strtok
函數(shù)將獲取到的指令切割成一個一個的子串,將所有子串的起始地址存儲在 argv
里面。注意 strtok
函數(shù)會改變原空間的內(nèi)容,因此創(chuàng)建了一段臨時的空間 cop_command
。
四、普通命令的執(zhí)行
void normalcommandexecution(char** _argv, int* _lastcode) // 普通命令的執(zhí)行 { pid_t id = fork(); if(id < 0) { perror("fork"); } else if(id == 0) { // child int ret = execvp(_argv[0], _argv); if(ret == -1) { perror("exeecp"); exit(EXIT_CODE); } } else { // father int status; pid_t ret = waitpid(id, &status, 0); // 阻塞等待 if(ret == id) { *_lastcode = WEXITSTATUS(status); } } } int main() { while(1) { // 1、交互獲取命令行參數(shù) interact(command, sizeof(command)); // 交互 // 到這里說明指令已經(jīng)獲取到了,接下來將指令打散 // 2、指令切割 commandcut(command, argv, sizeof(argv)); // 3、普通命令執(zhí)行 normalcommandexecution(argv, &lastcode); } return 0; }
代碼分析:對于 ls
這種普通指令(非內(nèi)建指令),先通過 fork
創(chuàng)建子進(jìn)程,然后再調(diào)用 execvp
接口進(jìn)行程序替換,去執(zhí)行輸入的指令。
五、內(nèi)建指令執(zhí)行
5.1 cd指令
bool isnormalcommand(char **_argv) // 指令判斷 { if (strcmp(_argv[0], "cd") == 0) return false; return true; } void changpwd(char** _argv) // 更改當(dāng)前工作目錄 { chdir(_argv[1]); // 更改當(dāng)前工作目錄 // getpwd(pwd, sizeof(pwd)); sprintf(getenv("PWD"), "%s", getcwd(pwd, sizeof(pwd))); // 修改環(huán)境變量 } void builtincommand(char **_argv) // 內(nèi)建命令執(zhí)行 { if (strcmp(_argv[0], "cd") == 0) { changpwd(_argv); } } int main() { while (1) { // 1、交互獲取命令行參數(shù) interact(command, sizeof(command)); // 交互 // 到這里說明指令已經(jīng)獲取到了,接下來將指令打散 // 2、指令切割 commandcut(command, argv, sizeof(argv)); // 3、指令判斷 // 3、普通命令執(zhí)行 if (isnormalcommand(argv)) // 普通指令 normalcommandexecution(argv, &lastcode); else // 內(nèi)建指令 builtincommand(argv); } return 0; }
代碼分析:要考慮內(nèi)建指令,那在指令切割之后要先對指令進(jìn)行判斷。內(nèi)建指令不需要創(chuàng)建子進(jìn)程去執(zhí)行,而是直接由當(dāng)前的 bash 進(jìn)程去執(zhí)行。比如說 cd 指令,執(zhí)行完 cd 指令后,我們要讓當(dāng)前的 bash 更改工作目錄,而不是讓其創(chuàng)建子進(jìn)程去執(zhí)行 cd 指令,那樣改變的就是子進(jìn)程的工作目錄??梢园l(fā)現(xiàn),一個指令執(zhí)行完后,如果會對 bash 產(chǎn)生影響,那么它就必須是內(nèi)建指令。其次關(guān)于 cd 指令,它改變了當(dāng)前的工作目錄,這一點(diǎn)該如何理解呢?我 myshell 就是一個可執(zhí)行程序,我的源代碼和編譯得到的可執(zhí)行文件始終都放在 /home/wcy/linux-s/2023-10-28a/myshell 目錄下,你 cd 命令憑什么能改變我的工作錄?其實(shí)并不然,這里改變工作目錄是:一個可執(zhí)行程序在變成進(jìn)程產(chǎn)生 PCB 對象后,PCB 里面維護(hù)了一個屬性就叫做當(dāng)前可執(zhí)行程序的工作目錄,cd 指令改變的其實(shí)就是這一屬性,并不是改變 myshell 程序的存儲位置,我們通過調(diào)用 chdir 系統(tǒng)調(diào)用來修改這一屬性。最后,因?yàn)槲覀兦懊媸峭ㄟ^環(huán)境變量來獲取當(dāng)前工作目錄,而環(huán)境變量在被當(dāng)前 myshell 進(jìn)程從父進(jìn)程繼承下來后是不會自動發(fā)生改變的,因此在執(zhí)行完 cd 指令后,我們要對 PWD 環(huán)境變量進(jìn)行修改,環(huán)境變量本質(zhì)上就是存儲在內(nèi)存中的一段字符串信息,因此我們可以采用 sprintf 函數(shù)對該字符串信息進(jìn)行修改。
5.2 export指令
#define USER_ENV_SIZE 100 // 允許用戶添加的環(huán)境變量個數(shù) #define USER_ENV_LONG 1024 // 用戶一個環(huán)境變量的最大長度 char userenv[USER_ENV_SIZE][USER_ENV_LONG]; // 保存用戶添加的環(huán)境變量 int userenvnum = 0; // 當(dāng)前用戶輸入的環(huán)境變量個數(shù) void exportcommand(char** _argv, char(*_userenv)[USER_ENV_LONG], int* _userenvnum) { // 將用戶輸入的環(huán)境變量存儲起來 strcpy(_userenv[*_userenvnum], _argv[1]); int ret = putenv(_userenv[(*_userenvnum)++]); if (ret == 0) perror("putenv"); }
代碼分析:只要 bash
不退出,我們每次添加的環(huán)境變量都應(yīng)該被保存起來,我們輸入的環(huán)境變量是被當(dāng)做指令保存在 command
里面,當(dāng)下一次輸入指令,上一次輸入的內(nèi)容就會被清空。putenv
添加環(huán)境變量,并不是把對應(yīng)的字符串拷貝到系統(tǒng)的表當(dāng)中,而是把該字符串的地址保存在系統(tǒng)的表中,因此我們要確保保存環(huán)境變量字符串的那個地址里的環(huán)境變量不會被修改,所以我們需要為用戶輸入的環(huán)境變量,也就是那一串字符串單獨(dú)開辟一塊空間進(jìn)行存儲,保證在內(nèi)次重新輸入指令的時候,不會影響到之前用戶添加的環(huán)境變量。因?yàn)榄h(huán)境變量本質(zhì)就是一個字符串,所以這里我們定義了一個字符二維數(shù)組來存儲用戶輸入的環(huán)境變量,先把用戶輸入的環(huán)境變量存入我們定義的這個數(shù)組,然后再調(diào)用 putenv
函數(shù)將數(shù)組中的內(nèi)容添加到當(dāng)前的環(huán)境變量。這樣就可以保證只要當(dāng)前 bash
不退出,用戶歷史上添加的環(huán)境變量都在。這里涉及到二維數(shù)組傳參的問題,再來回顧一下,數(shù)組名表示首元素地址,二維數(shù)組的首元素是一個一維數(shù)組,所以函數(shù)形參的類型是一個字符一維數(shù)組的地址,也就是 char(*)[USER_ENV_LONG]
。
5.3 echo指令
void echocommand(char **_argv, int _argc) { if (_argv[1][0] == '$') { char *ptr = _argv[1] + 1; printf("%s\n", getenv(ptr)); } else { int i = 1; while (i < _argc) { char *ret = strtok(_argv[i], "\""); while (ret != NULL) { printf("%s", ret); ret = strtok(NULL, "\""); } printf("%c", ' '); i++; } printf("\n"); } }
代碼分析:echo 指令需要考慮將輸入的 " 去掉,其次可能連續(xù)輸入多個字符串,還要考慮 echo 和 $ 配合使用是去打印環(huán)境變量的值。
小結(jié):當(dāng)我們登陸的時候,系統(tǒng)就是要啟動一個 shell 進(jìn)程,我們 shell 本身的環(huán)境變量是在用戶登錄的時候,shell 會讀取用戶目錄下的 .bash_profile 文件,里面保存了導(dǎo)入環(huán)境變量的方式。
六、結(jié)語
以上就是深入探究Linux shell的實(shí)現(xiàn)原理的詳細(xì)內(nèi)容,更多關(guān)于Linux shell的實(shí)現(xiàn)原理的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
shell腳本for循環(huán)實(shí)現(xiàn)文件和目錄遍歷
本文主要介紹了shell腳本for循環(huán)實(shí)現(xiàn)文件和目錄遍歷,首先進(jìn)行一個要遍歷的文件夾,然后循環(huán)查看每個文件,具有一定的參考價值,感興趣的小伙伴們可以參考一下2021-11-11使用ubuntu搭建公網(wǎng)個人郵件服務(wù)器(基于postfix,dovecot,mysql)
這篇文章主要介紹了基于ubuntu搭建公網(wǎng)個人郵件服務(wù)器(基于postfix,dovecot,mysql),免費(fèi)的郵箱每天發(fā)信數(shù)量是有限制的,所以呢就想著搭建一個自己的郵件服務(wù)器,需要的朋友可以參考下2019-06-06linux shell實(shí)現(xiàn)批量主機(jī)遠(yuǎn)程執(zhí)行命令腳本
這篇文章主要介紹了linux shell實(shí)現(xiàn)批量主機(jī)遠(yuǎn)程執(zhí)行命令腳本,文章通過代碼示例講解的非常詳細(xì),對大家的學(xué)習(xí)或工作有一定的幫助,需要的朋友可以參考下2024-09-09