深圳市住建局招標中心深圳關鍵詞排名seo
接受每一個人的批評,可是保留你自己的判斷。 ——莎士比亞
一段時間的沒有更新是由于最近開學期間比較的忙,同時也是由于剛開學的幾門課才學習的時候有點迷糊,需要在學校課堂上花的時間更多了,所以才沒有更新的,求放過。
簡單shell的實現(xiàn)
- 1、shell介紹
- 2、shell實現(xiàn)概括
- 3、shell實現(xiàn)困難
- 4、shell實現(xiàn)具體方式
- 4、1、main函數(shù)
- 4、2、MakeCommandLineAndPrint函數(shù)
- 4、3、GetUserCommand函數(shù)
- 4、4、SplitCommand函數(shù)
- 4、5、CheckBuildin函數(shù)
- 4、6、ExecuteCommand函數(shù)
- 5、總結(jié)
1、shell介紹
對于什么是shell問題來說,這是個好問題😊,但是其實如果你看過我之前的文章的話,應該能準確的理解什么是shell,如果想要看之前怎么介紹的話,就會到之前文章里看一看。這里的話就簡單講一下吧,shell簡單點來說,就是一個你的老板的一個秘書,這里的老板也能夠看作是內(nèi)核,你想要讓你的老板有什么行為的話,你的報告換句話說就是你得將你的命令行代碼給到你的老板的秘書,也就是shell,會通過shell來幫助你去找到老板,但是并不是直接就能夠找到,并且讓他去執(zhí)行,給到老板前,秘書也會自己考慮一下這個命令行的方式有沒有什么不妥的地方,如果有的話也就不會直接麻煩操作系統(tǒng),這樣的話,既保證了內(nèi)核的安全性,也保證了運行時候的效率,這里的效率提升就是因為能夠秘書在接收到幾次一樣的請求之后能夠不再去進行判斷,直接否定。
2、shell實現(xiàn)概括
對于shell實現(xiàn)來說,每一次的命令行輸入,都會對應著有著一段的運行結(jié)果。那對于這種方式來說,可以看作是一個在一個父進程的情況下,一個子進程在不斷的執(zhí)行不同的命令,或者換句話說是在不斷的替換進程(其中的環(huán)境變量是從父進程傳下來的)。
所以我們可以用進程替換的思想去實現(xiàn)一個shell進程(這里的這種進程要一直進行,這樣才能夠?qū)崿F(xiàn)執(zhí)行多次的命令行。
由于我們每次輸入的命令行指令都是會被bash讀到,然后尋找指定的命令行中提到的程序,然后執(zhí)行相關的選項。就像這篇文章講的那樣,我們的程序中能夠讀取到我們輸入的東西,所以為什么我們不能夠利用這點來實現(xiàn)每次的命令行輸入,將對應到進程替換成我們需要的進程,運行結(jié)束之后再退出來。
按照這樣的方法的話,我就能夠奠定了我們實現(xiàn)shell主要實現(xiàn)方向。
3、shell實現(xiàn)困難
1、對于shell來說,不僅僅是讀取到我們輸入到的命令行是什么,我們還需要在執(zhí)行之前,每次都會有一段的前置的信息,這一段的前置消息就是,分別對應著用戶名,主機名以及當前目錄,所以第一個目標就是要解決基本信息的獲取以及顯示。
2、除此之外,我們還需要將讀取到的命令行參數(shù)存放在數(shù)組之中,所以我們需要根據(jù)每一次的用戶的命令字符串,切分為不同的字符串數(shù)組,其中的要求就是依據(jù)空格為分界符號。
3、拆分后,分別的放在一個字符串數(shù)組之中。然后進行進程替換,這里的進程替換,選擇的函數(shù)是execvp,這個在之前的文章中講述過具體的使用方法,不知道的可以回顧一下,這個進程替換的系統(tǒng)調(diào)用函數(shù)能夠解決我們的問題。
4、當然如果我們知道內(nèi)建命令,那么我們還需要額外的去實現(xiàn)內(nèi)建命令構(gòu)建的操作。
4、shell實現(xiàn)具體方式
4、1、main函數(shù)
首先構(gòu)建一個main函數(shù)。
包含一下最主要的函數(shù),最主要的需要實現(xiàn)的功能。
為了方便后續(xù)的使用,我們把512定義為一個SIZE,簡單的認為這是一個大小的限制(就類似數(shù)組大小的限制)。
#define SIZE 512
int main()
{int quit = 0;while(!quit){// 1. 我們需要自己輸出一個命令行MakeCommandLineAndPrint();// 2. 獲取用戶命令字符串char usercommand[SIZE];int n = GetUserCommand(usercommand, sizeof(usercommand));if(n <= 0) return 1;// 3. 命令行字符串分割. SplitCommand(usercommand, sizeof(usercommand));// 4. 檢測命令是否是內(nèi)建命令n = CheckBuildin();if(n) continue;// 5. 執(zhí)行命令ExecuteCommand();}return 0;
}
4、2、MakeCommandLineAndPrint函數(shù)
讓每一個命令行都打印出自己的相關的信息。這個函數(shù)也不需要傳參,因為所有需要得到的都已經(jīng)存在于環(huán)境變量中了。所以為了能夠打印相關的信息,就要去讀取。所以我們就需要去編寫相關函數(shù)去編寫讀取的方法。
首先第一步是構(gòu)建一個框架。
void MakeCommandLineAndPrint()
{char line[SIZE];const char *uswename=GetUserName();const char *hostname=GetHostName();const char *cwd=GetCwd(); SkipPath(cwd);snprintf(line,sizeof(line),"[%s@%s %s]> ",username,hostname,strlen(cwd)==1 ? "/":cwd+1);printf("%s",line);fflush(stdout);
}
其次就是去實現(xiàn)每一個函數(shù)的具體意義。
首先我們來看SkipPath
為什么這里會有一個SkipPath呢?難道說每次得到的還不是我們正常使用的cwd嗎?那當然不是能夠直接使用的啊。所以對于這個函數(shù)來說就是為了處理一開始得到的不是我們最終想要的結(jié)果。如果不知道原本是什么的話,其實簡單說一下也就是從家目錄到當前目錄的所有的路徑都在環(huán)境變量的cwd中。所以我們才需要進行額外的處理。為了能夠不用多余的函數(shù)來增加我們shell的時間復雜度,并且為了能夠不傳指針就能夠?qū)崿F(xiàn)對于變量的改寫,我們需要使用到宏。因為宏是一個能夠在編譯的時候就能在原本的位置中展開,這也就不會造成重新開棧,重新消耗空間,考慮形參和實參的關系。
#define SkipPath(p) do {p+=(strlen(p)-1); while(*p!='/')p--;} while(0)
這里單獨的寫出來do{}while,來包含主要的程序,主要的作用是為了防止出現(xiàn)優(yōu)先級錯誤的情況。
其中的幾個得到環(huán)境變量相關信息的函數(shù)本質(zhì)上都是一樣的。大概看看應該能夠看懂。
const char *GetUserName()
{const char *name = getenv("USER");if(name == NULL) return "None";return name;
}
const char *GetHostName()
{const char *hostname = getenv("HOSTNAME");if(hostname == NULL) return "None";return hostname;
}
const char *GetCwd()
{const char *cwd = getenv("PWD");if(cwd == NULL) return "None";return cwd;
}
這樣的話,就能夠?qū)崿F(xiàn)我們編寫的shell的第一步了。
4、3、GetUserCommand函數(shù)
這個函數(shù)的話,是要讀取用戶輸入的字符串,當然用戶在輸入的時候是有空格的,所以對于該函數(shù),需要注意的是,這里不能夠是直接使用scanf函數(shù),而是要找到一個能夠按照行來拿到字符串的函數(shù)。這樣的話才能夠保證不會因為存在空格反而不能讀到正確的結(jié)果。
所以這個函數(shù)是什么呢?有沒有比較好的一個接口呢?我的建議是選擇一個char *fgets(char *s,int size,FILE *stream),如果能夠 正確返回,那么返回s的起始位置的地址。如果返回錯誤,就返回NULL。建議使用這個文件流相關的知識,那是因為之后的文章中馬上就要講解有關于文件流的知識。其中的size指的是s的大小。并且輸入的話,存放在的位置是在s中。==其中有一個不注意就會忘記的一點是,我們每次輸入的時候按回車才能實現(xiàn)fgets真正的讀完,所以說如果我們不干涉的話,在最后會有一個多余的回車。==所以我們需要進行改寫,將函數(shù)內(nèi)部傳入命令行之后進行sizeof結(jié)尾置零操作。
對于這個函數(shù)傳參的設計的話,應該是需要傳入兩個。
第一個參數(shù)是我們在main函數(shù)創(chuàng)建的一個專門存放命令行內(nèi)容的usercommand數(shù)組,這是因為這個數(shù)組在讀完數(shù)據(jù)之后還需要進行之后的操作,就比如說分割操作。
第二個參數(shù)就是我們用來得到這個字符串所占據(jù)的內(nèi)存大小,因為在fgets函數(shù)使用的時候需要用到。
這樣的話注意點,以及一些傳參的設計都已經(jīng)搞定了,下面就是真正的代碼的實現(xiàn)。
#define ZERO '\0'
int GetUserCommand(char command[], size_t n)
{char *s = fgets(command, n, stdin);if(s == NULL) return -1;command[strlen(command)-1] = ZERO;return strlen(command);
}
4、4、SplitCommand函數(shù)
對于分割命令行參數(shù)的函數(shù)來說,我們需要像之前那樣定義一個宏函數(shù)來幫助我們實現(xiàn)不用傳參的操作嗎?其實宏函數(shù)確實能夠?qū)崿F(xiàn),但是對于學習階段來說我們其實可以想一下,之前在介紹C語言中的字符串函數(shù)的時候,有一個函數(shù)其實能夠剛好符合我們的要求。strtok函數(shù),能夠根據(jù)特定的字符來找到字符串中每一個字符的位置,如果只執(zhí)行一次的話,找到的就是第一個要求的字符,如果接著執(zhí)行的話,就會在第一個基礎上往后找。根據(jù)函數(shù)的這個屬性的話,我們就能夠利用這個函數(shù)從前往后的一次尋找空格來自動幫我們分開字符串。當然找到了符合條件的情況下,就會返回從左到右的第一個子串,后續(xù)的會返回第一個結(jié)尾之后的第二個位置的子串。如果找不到符合條件的話,就返回NULL。
為什么就是需要我們?nèi)崿F(xiàn)一個字符串分割為多個呢?那是因為無論未來我們是用什么樣子的系統(tǒng)調(diào)用的程序替換都需要我們命令行輸入的一個一個打散的,而不是整個一起的方式去讀取。
其中的NUM是用來默認設置一個命令行參數(shù)的個數(shù)的,通常情況下來說一個指令后面加上的選項不會超過NUM默認的32個的,如果超過的話,可以自行修改NUM讓其能夠存放在gArgv[]之中
#define NUM 32
#define SEP " "
char *gArgv[NUM];
void SplitCommand(char command[], size_t n)
{(void)n;// "ls -a -l -n" -> "ls" "-a" "-l" "-n"gArgv[0] = strtok(command, SEP);int index = 1;while((gArgv[index++] = strtok(NULL, SEP))); // done, 故意寫成=,表示先賦值,在判斷. 分割之后,strtok會返回NULL,剛好讓gArgv最后一個元素是NULL, 并且while判斷結(jié)束
}
這里定義的SEP我們需要找到的目標的位置是空格,但是這里非常容易錯,那是因為strtok函數(shù)中的第二個參數(shù)是字符串而不是字符。
4、5、CheckBuildin函數(shù)
內(nèi)建命令的特點就是不需要考慮當前環(huán)境或者是默認的配置的條件,在什么地方shell都能夠運行出來相對于的結(jié)果。
對于現(xiàn)在的我來說我只認識兩個內(nèi)建命令。分別是cd命令,echo $?命令。這兩個我在之前講環(huán)境變量的時候講述過了其特點。所以要想這兩個命令的與眾不同,肯定是在函數(shù)結(jié)構(gòu)上的與眾不同。就比如之前的一些命令的話會存在于bin目錄之下,但是內(nèi)建命令可能就直接存在程序之中,這樣的話,不會受到環(huán)境的因素也能夠?qū)崿F(xiàn)相對應的指令。
所以根據(jù)內(nèi)建命令的特點,我寫了一個檢查內(nèi)建命令的函數(shù),如果滿足條件的話就會直接運行,不會先替換進程然后執(zhí)行,這樣就能夠避免環(huán)境改變造成無法執(zhí)行相關功能的問題。
函數(shù)的返回值設置為int類型,這樣做的話能夠判斷是否用戶輸入的為內(nèi)建命令,如果是內(nèi)建命令的話,就會執(zhí)行完,也就不會再去執(zhí)行下一個的ExecuteCommand函數(shù)。避免了重復執(zhí)行的錯誤。
char cwd[SIZE*2];
int lastcode = 0;
const char *GetHome()
{const char *home = getenv("HOME");if(home == NULL) return "/";return home;
}
void Cd()
{const char *path = gArgv[1];if(path == NULL) path = GetHome();//如果是空的話,會在直接返回家目錄// path 一定存在chdir(path);// 刷新環(huán)境變量char temp[SIZE*2];getcwd(temp, sizeof(temp));snprintf(cwd, sizeof(cwd), "PWD=%s", temp);putenv(cwd); // OK
}int CheckBuildin()
{int yes = 0;const char *enter_cmd = gArgv[0];if(strcmp(enter_cmd, "cd") == 0){yes = 1;Cd();}else if(strcmp(enter_cmd, "echo") == 0 && strcmp(gArgv[1], "$?") == 0){yes = 1;printf("%d\n", lastcode);lastcode = 0;}return yes;
}
這里容易錯的地方就是環(huán)境變量也是需要更新的,不能說我們進行了好幾次的cd或者其他命令之后環(huán)境變量因為沒有進行更新從而錯誤。
這樣的話能夠?qū)崿F(xiàn)簡單的內(nèi)建命令。那我們該怎么去執(zhí)行內(nèi)建命令之外的命令呢?當然是使用進程替換!
4、6、ExecuteCommand函數(shù)
進程替換,那就是說在該函數(shù)中需要使用到fork()函數(shù),并且還需要判斷使用哪一個系統(tǒng)調(diào)用函數(shù)來確定傳參條件。考慮之后還是使用execvp函數(shù)。下面是實現(xiàn)的代碼。
void ExecuteCommand()
{pid_t id = fork();if(id < 0) Die();else if(id == 0){// childexecvp(gArgv[0], gArgv);exit(errno);}else{// fahterint status = 0;pid_t rid = waitpid(id, &status, 0);if(rid > 0){lastcode = WEXITSTATUS(status);if(lastcode != 0) printf("%s:%s:%d\n", gArgv[0], strerror(lastcode), lastcode);}}
}
5、總結(jié)
這樣的話就簡單的把一個shell的指令完全的自我實現(xiàn)了,其中當然也會有很多的不足的地方,但是基本上的內(nèi)容都已經(jīng)實現(xiàn)。希望讀者能夠在本篇文章的基礎之上,學到更多,理解過多的關于shell編程的快樂,也希望能夠通過該篇文章,給自己的學習路上添磚加瓦,做到更好。