本章的代碼可以訪問這里獲取。
由于程序代碼是一體的,本章在分開講解各部分的實(shí)現(xiàn)時(shí),代碼可能有些跳躍,建議在講解各部分實(shí)現(xiàn)后看一下源代碼方便理解程序。
一、觀察Shell的運(yùn)行狀態(tài)
我們想要制作一個(gè)簡(jiǎn)單的Shell
解釋器,需要先觀察Shell是怎么運(yùn)行的,根據(jù)Shell
的運(yùn)行狀態(tài)我們?cè)偃ミM(jìn)行模擬實(shí)現(xiàn)。
我們可以先考慮下面的指令與Shell的互動(dòng):
我們仔細(xì)進(jìn)行分析可以發(fā)現(xiàn),Shell
執(zhí)行上面的命令時(shí),可以被理解為下面的過程。
當(dāng)然上面的命令都是普通命令,所以Shell
都是通過創(chuàng)建子進(jìn)程的方式來執(zhí)行的,對(duì)于一些內(nèi)建命令(Shell
自己去執(zhí)行命令)我們現(xiàn)在還不考慮,在后面的部分我們?cè)龠M(jìn)行進(jìn)一步的討論內(nèi)建命令應(yīng)該怎么去處理。
二、簡(jiǎn)單的Shell解釋器制作原理
通過觀察Shell
的運(yùn)行狀態(tài),我們知道然后Shell
讀取新的一行輸入,建立一個(gè)新的子進(jìn)程,在這個(gè)子進(jìn)程中運(yùn)行程序并等待這個(gè)進(jìn)程結(jié)束。
所以要寫一個(gè)shell,需要循環(huán)以下過程:
- 獲取命令行
- 解析命令行
- 建立一個(gè)子進(jìn)程(
fork
) - 替換子進(jìn)程(
execvp
),執(zhí)行替換后的程序 - 父進(jìn)程等待子進(jìn)程退出(
wait
)
1、獲取命令行
我們?cè)谠?code>Shell中輸入的命令本質(zhì)上就是輸入一個(gè)字符串,因此我們想要獲取命令行,可以先創(chuàng)建一個(gè)字符數(shù)組commandstr
,然后使用C語言的fgets
函數(shù)從鍵盤中進(jìn)行讀取數(shù)據(jù)到字符數(shù)組里面,這樣我們就獲取了一個(gè)命令行了。
注意:
- 這里不能使用
scanf
函數(shù) ,這里的命令會(huì)包含空格,會(huì)導(dǎo)致scanf
讀取不到完整的數(shù)據(jù)。fgets
函數(shù)會(huì)將我們輸入的命令時(shí)的最后一個(gè)的\n
符也給讀取到字符數(shù)組內(nèi),我們需要特殊處理將\n
進(jìn)行用\0
進(jìn)行覆蓋
//這里包含的頭文件是我們整個(gè)程序需要用到的所有頭文件
#include<stdio.h>
#include<unistd.h>
#include<assert.h>
#include<string.h>
#include<sys/types.h>
#include<sys/wait.h>
#include<stdlib.h>
//這里的N用于定義字符數(shù)組的大小
#define N 128
int main()
{
//存儲(chǔ)命令行的字符數(shù)組
char commandstr[N] = "";
//Shell要一直運(yùn)行接受命令,所以這里必須是死循環(huán)!
while(1)
{
//模擬Shell的提示符
printf("[hong@machine MiniShell]# ");
//從標(biāo)準(zhǔn)輸入流中讀取字符串
char* s = fgets(commandstr, sizeof(commandstr), stdin);
assert(s); //判斷fgets是否讀取成功
//處理\n 示例字符串:ls -a -l\n\0
commandstr[strlen(commandstr) - 1] = '\0';
}
2、解析命令行
雖然我們通過前一步已經(jīng)拿到命令行,但是我們還不能直接使用,因?yàn)槲覀兡玫降淖址虚g可能有許多空格以及一些其他的問題,我們還需要將命令行的字符進(jìn)行切割提取出我們想要的子串,這樣才符合程序替換函數(shù)的要求。例如:將 ls -a -l
提取成 ls
,-a
, -l
。
對(duì)于字符串的切割,我們可以使用C語言提供的strtok
函數(shù),由于切割以后我們的字符串從一個(gè)變成了多個(gè),因此我們需要用一個(gè)字符串指針數(shù)組argv
,存儲(chǔ)每一部分切割后的首地址,同時(shí)這個(gè)argv
也可以直接傳遞給execvp
函數(shù)進(jìn)行程序替換了。
//在全局域中 定義切割符
#define SEP " "
//main函數(shù)的外部 定義一個(gè)命令行切割函數(shù)
int split(char commandstr[], char* argv[])
{
assert(commandstr);
assert(argv);
//第一次切割
argv[0] = strtok(commandstr, SEP);
if(argv[0] == NULL)
{
//返回 -1表示異常退出
return -1;
}
//循環(huán)切割
int i = 1;
while((argv[i++] = strtok(NULL, SEP)));
return 0;
}
//main函數(shù)內(nèi)部,while循環(huán)上面定義切割后的字符指針數(shù)組
char* argv[N] ={NULL};
//while循環(huán)內(nèi)部
//切割字符串 例如將"ls -a -l " 變?yōu)?"ls" "-a" "-l"
int n = split(commandstr, argv);
if(n == -1)
{
//切割失敗就終止本次循環(huán)
continue;
}
3、創(chuàng)建子進(jìn)程 進(jìn)行程序替換 父進(jìn)程等待
創(chuàng)建子進(jìn)程而我們可以使用fork
函數(shù)進(jìn)行創(chuàng)建,創(chuàng)建完以后進(jìn)程的執(zhí)行流由一個(gè)變成了兩個(gè),我們?cè)谧舆M(jìn)程中進(jìn)行程序替換可以使用execvp
命令,同時(shí)我們的argv[0]
就是程序名,argv
中存儲(chǔ)的就是命令按照什么方式進(jìn)行執(zhí)行。
最后我們的父進(jìn)程可以在外面進(jìn)行阻塞等待,然后獲取子進(jìn)程的退出碼和退出信息。
//main函數(shù)內(nèi)部,while循環(huán)上面定義退出碼變量
int last_status = 0;
//while循環(huán)內(nèi)部
//創(chuàng)建子進(jìn)程,進(jìn)行命令處理
pid_t id = fork();
assert(id >= 0);
if(id == 0)
{
//child process
execvp(argv[0], argv);
//如果執(zhí)行到這里說明程序替換失敗
exit(-1);
}
//父進(jìn)程等待子進(jìn)程
int status;
int pid = waitpid(id, &status, 0);
//等待成功就提取退出碼信息
if(pid >= 0)
{
last_status = WEXITSTATUS(status);
}
}
return 0;
4、實(shí)際運(yùn)行
我們可以執(zhí)行 ls
pwd
ps -axj
命令 看一看效果。
二、對(duì)簡(jiǎn)單的內(nèi)建命令進(jìn)行處理
我們知道內(nèi)建命令是讓Shell
自己執(zhí)行的命令,而不是讓子進(jìn)程執(zhí)行的命令,例如cd
命令就是內(nèi)建命令,因?yàn)槲覀円淖兊氖?code>Shell自己的工作目錄,而不是子進(jìn)程的工作目錄,類似的命令還有export
env
echo
命令。
由于上面我們寫的程序執(zhí)行命令時(shí)都是交給子進(jìn)程去做的,所以我們上面寫的程序是沒有辦法執(zhí)行內(nèi)建命令的,或者說能執(zhí)行內(nèi)建命令,但不是我們想要的結(jié)果或目的。
所以接下來我們要對(duì)這個(gè)簡(jiǎn)單的Shell
進(jìn)行改造,讓它能夠執(zhí)行一些簡(jiǎn)單的內(nèi)建命令,還有剛剛我們的ls
命令沒有色彩,我們也要進(jìn)行一些修改。
1、給ls命令加上色彩
在真正的Shell
中我們執(zhí)行的ls
命令其實(shí)是ls --color=auto
,ls
被我們真正的Shell
進(jìn)行了起別名。
我們?cè)谶\(yùn)行我們自己制作的Shell
時(shí)也可以加上--color=auto
。
//此段代碼應(yīng)該在切割字符串之后
//argv[0]就是我們的命令名
if(strcmp(argv[0], "ls") == 0)
{
int pos = 0;
//尋找指針數(shù)組的結(jié)尾
while(argv[pos++]);
//在NULL位置加上 --color=auto
argv[pos - 1] = "--color=auto";
//將后一個(gè)位置置空
argv[pos] = NULL;
}
這樣以后我們?cè)谖覀冏约褐谱鞯?code>Shell中執(zhí)行ls
命令時(shí)也會(huì)由顏色了!
2、支持cd命令
對(duì)于cd
命令如果讓父進(jìn)程進(jìn)行執(zhí)行,我們可以調(diào)用系統(tǒng)調(diào)用chdir
我們只需要傳遞一個(gè)參數(shù):路徑字符串,當(dāng)執(zhí)行成功時(shí)會(huì)返回0,執(zhí)行失敗會(huì)返回-1,并設(shè)置錯(cuò)誤碼。
//此段代碼應(yīng)該在ls添加顏色之后
else if(strcmp(argv[0], "cd") == 0)
{
//argv[1]里面存放的是路徑字符串
if(argv[1] == NULL)
{
printf("沒有正確的路徑!\n");
//設(shè)置錯(cuò)誤碼
last_status = -1;
continue;
}
//執(zhí)行系統(tǒng)調(diào)用改變父進(jìn)程的工作目錄
chdir(argv[1]);
continue;
}
3、支持export命令
export
命令可以將一個(gè)本地變量加入到環(huán)境變量表中,我們讓我們自己制作的Shell
完成expoprt
命令可以用C語言提供的函數(shù)putenv
函數(shù),但是在向環(huán)境變量表加入新的環(huán)境變量時(shí),我們要維護(hù)好我們加入到環(huán)境變量,這個(gè)環(huán)境變量不能夠被輕易的覆蓋,否則環(huán)境變量表在找我們的環(huán)境變量時(shí)就會(huì)找不到,所以我們還要?jiǎng)?chuàng)建一個(gè)我們自己維護(hù)的二維數(shù)組。
//在全局域中定義
// 自己維護(hù)的二維數(shù)組最多能向環(huán)境變量表幾個(gè)自定義的環(huán)境變量
#define MAX 64
//main函數(shù)內(nèi)部,while循環(huán)上面定義
//指向下一個(gè)要添加的環(huán)境變量的位置
int env_index = 0;
//要維護(hù)的二維數(shù)組
char envstr[MAX][N];
//此段代碼應(yīng)該在ls添加顏色之后
else if(strcmp(argv[0], "export") == 0)
{
//聲明putenv函數(shù)否則會(huì)編譯器會(huì)有警告
extern int putenv(char *string);
//argv[1]位置應(yīng)該是環(huán)境變量
if(argv[1] == NULL)
{
printf("沒有輸入變量!\n");
last_status = -1;
continue;
}
//將argv[1]位置的環(huán)境變量,拷貝到env_str中,否則下一次解析的命令會(huì)覆蓋環(huán)境變量
strcpy(envstr[env_index], argv[1]);
//將環(huán)境變量導(dǎo)入環(huán)境變量表
putenv(envstr[env_index++]);
}
4、支持env命令
對(duì)于env
命令我們只需要寫一個(gè)打印環(huán)境變量表的函數(shù)就能完成此命令了。文章來源:http://www.zghlxwxcb.cn/news/detail-431273.html
//main函數(shù)的外部 定義一個(gè)打印環(huán)境變量表的函數(shù)
void showEnv()
{
extern char** environ;
int i = 0;
while(environ[i])
{
printf("%d : %s\n", i, environ[i++]);
}
}
//此段代碼應(yīng)該在ls添加顏色之后
else if(strcmp(argv[0], "env") == 0)
{
showEnv();
continue;
}
5、支持echo命令
echo
命令可以用于打印環(huán)境變量,也可以打印退出碼,這取決于$
后面是不是?
是?
我們就可以打印last_status
,不是我們就用getenv
命令拿到環(huán)境變量的內(nèi)容。文章來源地址http://www.zghlxwxcb.cn/news/detail-431273.html
//此段代碼應(yīng)該在ls添加顏色之后
else if(strcmp(argv[0], "echo") == 0)
{
if(*argv[1] == '$')
{
if(*(argv[1] + 1) == '?')
{
printf("process exit code %d\n", last_status);
continue;
}
else
{
char* str = getenv(argv[1] + 1);
printf("%s\n",str);
continue;
}
}
}
到了這里,關(guān)于【Linux】教你用進(jìn)程替換制作一個(gè)簡(jiǎn)單的Shell解釋器的文章就介紹完了。如果您還想了解更多內(nèi)容,請(qǐng)?jiān)谟疑辖撬阉鱐OY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!