一、前言
來制作一個簡易的 [Shell 命令]行解釋器。
首先這是與 Shell 的互動::

用下圖的[時間軸]來表示事件的發(fā)生次序。其中時間從> > 左向右。shell 由標(biāo)識為 sh 的方塊代表,它隨著時間的流逝從左向右移動。shell 從用戶讀入字符串 "ls"。shell 建立一個新的進(jìn)程,然后在那個進(jìn)程中運(yùn)行 ls 程序并等待那個進(jìn)程結(jié)束。

然后 shell 讀取新的一行輸入,建立一個新的進(jìn)程,在這個進(jìn)程中運(yùn)行程序 并等待這個進(jìn)程結(jié)束。所以要寫一個 shell,需要循環(huán)以下過程:
1. 獲取命令行
2. 解析命令行
3. 建立一個子進(jìn)程(fork)
4. 替換子進(jìn)程(execvp)
5. 父進(jìn)程等待子進(jìn)程退出(wait)
二、準(zhǔn)備工作
1.輸出提示符

這里的提示字符為用戶名 @主機(jī)名 當(dāng)前路徑# 直接打印出來作為提示所用
printf("用戶名@主機(jī)名當(dāng)前路徑#");
這里沒有 n,會有緩沖區(qū)的問題,類似于我們之前所說的進(jìn)度條所遇到的問題,可以用 fflush(stdout) 刷新緩沖區(qū)。
2. 輸入和獲取命令
輸入
我們需要輸入一連串命令,其中可能出現(xiàn)空格,所以不能使用 gets 函數(shù),需要用到 fgets 函數(shù),同時,可以定義一個 lineCommand[NUM] 數(shù)組
#defineNUM1024 charlineCommand[NUM]; char*s=fgets(lineCommand,sizeof(lineCommand)-1,stdin); assert(s!=NULL);
但是打印的時候卻多換了一行,這是我們把 n 也讀取到了,直接進(jìn)行處理即可, 清除最后一個 n
lineCommand[strlen(lineCommand)-1]=0;
可以通過打印看看效果和測試是否有 BUG
printf("test:%s
",lineCommand);

獲取
輸入之后,我們自然需要去進(jìn)行獲取,我們需要分割命令行,這個地方用 strtok。把字符串切割成若干個子串:
strtok: 第一次直接傳遞參數(shù),第二次則必須傳 NULL。且在最終 strtok 會返回 NULL。


3.shell 運(yùn)行原理
同時,在理解一下 shell 的運(yùn)行原理:shell 內(nèi)部提取命令行做分析,然后調(diào)用 exec. shell 執(zhí)行命令必須通過創(chuàng)建子進(jìn)程,如果不創(chuàng)建子進(jìn)程會把我們所有的 shell 全部替換,所以執(zhí)行命令時一般磁盤上的程序必須創(chuàng)建子進(jìn)程。
4. 內(nèi)建命令
我們在運(yùn)行自己寫的 shell 的時候,發(fā)現(xiàn)輸入 cd … 輸入 cd path 等命令時發(fā)現(xiàn)路徑并沒有改變!

沒有發(fā)生改變是因為自己寫的 shell 執(zhí)行很多命令都要 fork() 創(chuàng)建子進(jìn)程,讓子進(jìn)程執(zhí)行的 cd,子進(jìn)程有自己的工作目錄,所以更改的子進(jìn)程的目錄,子進(jìn)程執(zhí)行完畢,繼續(xù)用的是父進(jìn)程,既 shell,并沒有影響父進(jìn)程,所以并沒有改變。
對于 cd, 我們可以采用內(nèi)建命令:不需要創(chuàng)建子進(jìn)程執(zhí)行,讓 shell 自己執(zhí)行命令,稱為內(nèi)建命令。本質(zhì)就是執(zhí)行系統(tǒng)接口,我們可以調(diào)用一個系統(tǒng)接口 chdir,可解決上述問題:


5. 替換
采用 execvp 進(jìn)行替換進(jìn)程
pid_tid=fork(); assert(id!=-1); if(id==0) { execvp(myargv[0],myargv); exit(1); }
三、整體代碼
#include
#include
#include
#include
#include
#include
#include
#defineNUM1024
#defineOPT_NUM64
charlineCommand[NUM];
char*myargv[OPT_NUM];//指針數(shù)組
intlastcode=0;
intlastsig=0;
intmain()
{
while(1)
{
//1.輸出提示符
printf("lj@VM-8-2-centos當(dāng)前路徑#");
fflush(stdout);
//2.獲取用戶輸入的命令,輸入的時候,用戶最后還輸入了
char*s=fgets(lineCommand,sizeof(lineCommand)-1,stdin);
assert(s!=NULL);
(void)s;//避免Linux認(rèn)為s變量未使用,導(dǎo)致警告
//清除最后一個
;例如:abcd
lineCommand[strlen(lineCommand)-1]=0;
//printf("test:%s
",lineCommand);
//"ls-a-l-i"-->字符串分割-->"ls""-a""-l""-i"
myargv[0]=strtok(lineCommand,"");
inti=1;
if(myargv[0]!=NULL&&(strcmp(myargv[0],"ls")==0))
{
myargv[i++]=(char*)"--color=auto";
}
//如果沒有子串了,strtok會返回NULL,即myargv[end]=NULL
while(myargv[i++]=strtok(NULL,""));
//如果是cd命令,不需要創(chuàng)建子進(jìn)程,讓shell自己執(zhí)行對應(yīng)的命令,本質(zhì)就是執(zhí)行系統(tǒng)接口
//像這種不需要讓我們的子進(jìn)程來執(zhí)行,而是讓shell自己執(zhí)行的命令—內(nèi)建命令
//其中echo是一個自建命令
if(myargv[0]!=NULL&&(strcmp(myargv[0],"cd")==0))
{
if(myargv[1]!=NULL)chdir(myargv[1]);
continue;
}
if(myargv[0]!=NULL&&myargv[1]!=NULL&&(strcmp(myargv[0],"echo")==0))
{
if(strcmp(myargv[1],"$?")==0)
{
printf("%d,%d
",lastcode,lastsig);
}
else
{
printf("%s
",myargv[i]);
}
continue;
}
//利用條件編譯測試是否成功
#ifdefDEBUG
for(inti=0;myargv[i];++i)
{
printf("myargv[%d]:%s
",i,myargv[i]);
}
#endif
//執(zhí)行命令
pid_tid=fork();
assert(id!=-1);
if(id==0)
{
execvp(myargv[0],myargv);
exit(1);
}
intstatus=0;
pid_tret=waitpid(id,&status,0);
assert(ret>0);
(void)ret;
lastcode=(status>>8)&0xFF;
lastsig=status&0x7F;
}
return0;
}

審核編輯:湯梓紅
-
Linux
+關(guān)注
關(guān)注
88文章
11763瀏覽量
219088 -
命令行
+關(guān)注
關(guān)注
0文章
83瀏覽量
10766 -
Shell
+關(guān)注
關(guān)注
1文章
375瀏覽量
25401 -
進(jìn)程
+關(guān)注
關(guān)注
0文章
211瀏覽量
14542 -
解釋器
+關(guān)注
關(guān)注
0文章
103瀏覽量
6993
原文標(biāo)題:Linux 實現(xiàn)簡易的 Shell 命令行解釋器
文章出處:【微信號:良許Linux,微信公眾號:良許Linux】歡迎添加關(guān)注!文章轉(zhuǎn)載請注明出處。
發(fā)布評論請先 登錄
在STM32實現(xiàn)命令行
Linux圖形界面的原理與構(gòu)成和Linux命令行和vi編輯器的使用手冊
Linux桌面系統(tǒng)初級教程之Shell命令行操作的資料概述
Linux 命令行教程好書推薦
mini shell命令行調(diào)試工具(單片機(jī)、c語言)
Linux實現(xiàn)簡易的Shell命令行解釋器
評論