摘要:本期的目的是向大家介紹shell的概念和基本原理,并且在此基礎(chǔ)上動(dòng)手做一個(gè)簡(jiǎn)單shell解釋器。同時(shí),還將就用到的一些 linux環(huán)境編程的知識(shí)做一定講解。
本文適合的讀者對(duì)象
?????? 對(duì)linux環(huán)境上的c語(yǔ)言開發(fā)有一定經(jīng)驗(yàn);
對(duì)linux環(huán)境編程(比如進(jìn)程、管道)有一點(diǎn)了解。
概述
本章的目的是帶大家了解shell的基本原理,并且自己動(dòng)手做一個(gè)shell解釋器。為此,
首先,我們解釋什么是shell解釋器。
其次,我們要大致了解shell解釋器具有哪些功能;
最后,我們具體講解如何實(shí)現(xiàn)一個(gè)簡(jiǎn)單的 shell 解釋器,并對(duì)需要用到一些 linux環(huán)境編程的知識(shí)做一定講解,并提醒你如果想深入掌握,應(yīng)該去看哪些資料。
Shell解釋器是什么?
Shell解釋器是一個(gè)程序。對(duì),是一個(gè)程序,而且,它就在我們的身邊。在linux系統(tǒng)中,當(dāng)我們輸入用戶名和密碼登陸之后,我們就開始執(zhí)行一個(gè)shell解釋器程序,通常是 /bin/bash,當(dāng)然也可以是別的,比如/bin/sh。(詳細(xì)概念請(qǐng)看第一期中的shell有關(guān)部分)
提示:在 /etc/passwd 文件中,每個(gè)用戶對(duì)應(yīng)的最后一項(xiàng),就指定了該用戶登陸之后,要執(zhí)行的shell解釋器程序。
在 linux 字符界面下,輸入
man bash
調(diào)出 bash 的幫助頁(yè)面
幫助的最開始就對(duì)bash下了一個(gè)定義:
bash 是一個(gè)兼容于 sh 的命令語(yǔ)言解釋器,它從標(biāo)準(zhǔn)輸入或者文件中讀取命令并執(zhí)行。它的意圖是實(shí)現(xiàn) IEEE POSIX標(biāo)準(zhǔn)中對(duì) shell和工具所規(guī)范的內(nèi)容。
Shell解釋器的作用
在登陸 linux 系統(tǒng)之后,屏幕上就會(huì)出現(xiàn)一行提示符,在我的機(jī)器上,是這樣的:
?????? [root@stevens root]#
這行提示符就是由bash解釋器打印出來(lái)的,這說(shuō)明,現(xiàn)在已經(jīng)處于 bash 的控制之下了,也同時(shí)提示用戶,可以輸入命令。用戶輸入命令,并回車確認(rèn)后,bash分析用戶的命令,如果用戶的命令格式正確,那么bash就按照用戶的意思去做一些事情。
比如,用戶輸入:
[root@stevens root]#? echo “hello, world”
那么,bash就負(fù)責(zé)在屏幕上打印一行“hello world”。
如果,用戶輸入:
[root@stevens root]#? cd /tmp
那么,bash就把用戶的當(dāng)前目錄改變?yōu)?/tmp。
所以,shell解釋器的作用就是對(duì)用戶輸入的命令進(jìn)行“解釋”,有了它,用戶才可以在 linux 系統(tǒng)中任意揮灑。沒(méi)有它的幫助,你縱然十八般本領(lǐng)在身,也施展不出。
bash每次在“解釋”完用戶命令之后,又打印出一行提示符,然后繼續(xù)等待用戶的下一個(gè)命令。這種循環(huán)式的設(shè)計(jì),使得用戶可以始終處于 bash 的控制之下。除非你輸入 exit、logout明確表示要退出 bash。
Shell語(yǔ)法梗概
我們不停的命令 bash 做這做那,一般情況下它都很聽話,按你的吩咐去做??捎袝r(shí)候,它會(huì)對(duì)你說(shuō):“嗨,老兄,你的命令我理解不了,無(wú)法執(zhí)行”。例如,你輸入這樣的命令:
[root@stevesn root]# aaaaaa
bash會(huì)告訴你:
bash: aaaaaa: command not found
是的,你必須說(shuō)的讓它能聽懂,否則它就給你這么一句抱怨,當(dāng)然也還會(huì)有其它的牢騷。
那么,什么樣格式的命令,它才能正確理解執(zhí)行了?這就要引出shell 的語(yǔ)言規(guī)范了。
Shell作為一個(gè)命令語(yǔ)言解釋器,有一套自己的語(yǔ)言規(guī)范,凡是符合這個(gè)規(guī)范的命令,它就可以正確執(zhí)行,否則就會(huì)報(bào)錯(cuò)。這個(gè)語(yǔ)言規(guī)范是在 IEEE POSIX的第二部分:“shell和tools規(guī)范”中定義的。關(guān)于這份規(guī)范,可以在這里看到。
官方的東西,總是冗長(zhǎng)而且晦澀,因?yàn)樗龅矫婷婢愕角也荒苡衅凭`。如果讀者有興趣,可以仔細(xì)研究這份規(guī)范。而我們的目的只是理解shell的實(shí)現(xiàn)思想,然后去實(shí)現(xiàn)一個(gè)簡(jiǎn)單的 shell 解釋器,所以沒(méi)必要陷入枯燥的概念之中。
現(xiàn)在請(qǐng)繼續(xù)在 linux 字符界面下輸入 man bash,調(diào)出 bash 的幫助頁(yè)面,然后找到 “shell語(yǔ)法”那一部分,我們就是以這里的描述作為實(shí)現(xiàn)的依據(jù)。
在 bash幫助的“shell 語(yǔ)法”一節(jié),是這樣來(lái)定義shell 語(yǔ)法的:
l???????? 簡(jiǎn)單命令
簡(jiǎn)單命令是(可選的)一系列變量賦值, 緊接著是空白字符分隔的詞和重定向符號(hào), 最后以一個(gè)控制操作符結(jié)束. 第一個(gè)詞指明了要執(zhí)行的命令, 它被作為第 0 個(gè)參數(shù). 其余詞被作為這個(gè)命令的參數(shù).
?????? 這個(gè)定義可以這樣來(lái)理解:
1、? 可以有變量賦值,例如
a=10 b=20 export a b
2、? “詞”是以空白字符分隔開的,空白字符包括制表符(tab)和空格,例如:
ls /tmp
就是兩個(gè)詞,一個(gè) ls,一個(gè) /tmp
3、可以出現(xiàn)重定向符號(hào),重定向符號(hào)是“>”和“<”,例如:
echo “hello world” > /tmp/log
4、? 簡(jiǎn)單命令結(jié)束于控制操作符,控制操作符包括:
||? &?? &&???? ;?? ;;? ( )?? |? <newline>
例如,用戶輸入:
ls /tmp
用戶最后敲的回車鍵就是控制操作符 newline,表示要結(jié)束這個(gè)簡(jiǎn)單命令。
如果用戶輸入:
echo “100” ; echo “200”
那么這是兩個(gè)簡(jiǎn)單命令,第一個(gè)結(jié)束于“;”,第二個(gè)結(jié)束于newline。
5、? 簡(jiǎn)單命令的第一個(gè)詞是要執(zhí)行的命令,其余的詞都是這個(gè)命令的參數(shù),例如:
echo “hello world” echo
第一個(gè)echo 是命令,第二個(gè)詞“hello world”是參數(shù)1,第三個(gè)詞 echo 是參數(shù)2,而不再作為一個(gè)命令了。
簡(jiǎn)單命令是 shell 語(yǔ)法中最小的命令,通過(guò)簡(jiǎn)單命令的組合,又可以得到管道命令和列表命令。
l???????? 管道(命令)
管道是一個(gè)或多個(gè)簡(jiǎn)單命令的序列,兩個(gè)簡(jiǎn)單命令之間通過(guò)管道符號(hào)(“|”)來(lái)分隔
例如
echo “hello world” | wc –l
就是一個(gè)管道,它由兩個(gè)簡(jiǎn)單命令組成,兩個(gè)簡(jiǎn)單命令之間用管道符號(hào)分隔開。
我們可以看到,管道符號(hào)“|”也是屬于上面提到的控制操作符。
根據(jù)這個(gè)定義,一個(gè)簡(jiǎn)單命令也同時(shí)是一個(gè)管道。
管道的作用是把它前面的那個(gè)簡(jiǎn)單命令的輸出作為后面那個(gè)簡(jiǎn)單命令的輸入,就上面這個(gè)例子來(lái)說(shuō):
echo “hello world” 本來(lái)是要在標(biāo)準(zhǔn)輸出(屏幕)上打印 “hello world” 的,但是管道現(xiàn)在不讓結(jié)果輸出到屏幕上,而是“流”到 wc –l 這個(gè)簡(jiǎn)單命令,wc –l 就把“流”過(guò)來(lái)的數(shù)據(jù)作為它的標(biāo)準(zhǔn)輸入進(jìn)行計(jì)算,從而統(tǒng)計(jì)出結(jié)果是 1 行。
關(guān)于管道更詳細(xì)的內(nèi)容,我們?cè)诤竺婢唧w實(shí)現(xiàn)管道的時(shí)候再說(shuō)明。
l???????? 列表(命令):
列表是一個(gè)或多個(gè)管道組成的序列,兩個(gè)管道之間用操作符 ;, &, &&, 或 || 分隔。我們看到,這幾個(gè)操作符都屬于控制操作符。
例如
echo “hello world” | wc –l ; echo “nice to meet you”
就是一個(gè)列表,它由兩個(gè)管道組成,管道之間用分號(hào)(;)隔開
分號(hào)這種控制操作符僅僅表示一種執(zhí)行上的先后順序。
l???????? 復(fù)合命令
?????? 這個(gè)定義比較復(fù)雜,實(shí)現(xiàn)起來(lái)也有相當(dāng)難度,在咱們這個(gè)示例程序中,就不實(shí)現(xiàn)了。
以上是 shell 語(yǔ)法規(guī)范的定義,我們的 shell 程序就是要以此規(guī)范為依據(jù),實(shí)現(xiàn)對(duì)簡(jiǎn)單命令、管道和列表的解釋。對(duì)于列表中的控制操作符,我們只支持分號(hào)(;),其它的留給讀者自己來(lái)實(shí)現(xiàn)。
?????? 接下來(lái),我們具體介紹如何實(shí)現(xiàn)一個(gè)簡(jiǎn)單的 shell解釋器。
實(shí)現(xiàn)shell實(shí)例
程序主框架
?????? 主程序很簡(jiǎn)單,它在做一些必要的初始化工作之后,進(jìn)入這樣一個(gè)循環(huán):
u?????? 打印提示符并等待用戶輸入
u?????? 獲取用戶輸入
u?????? 分析用戶輸入
u?????? 解釋執(zhí)行;
如果用戶輸入 logout或者 exit 之后,才退出這個(gè)循環(huán)。
用類似偽代碼的形式表示如下:
while(1) {
?????? print_prompt();
?????? get_input();
?????? parse_input();
?????? if(“l(fā)ogout” || “exit”)
????????????? break;
?????? do_cmd();
}
讀取用戶輸入
如何獲取用戶輸入?一種方法是通過(guò) getchar() 從標(biāo)準(zhǔn)輸入每次讀一個(gè)字符,如果讀到的字符是 ‘\n’,說(shuō)明用戶鍵入了回車鍵,那么就把此前讀到的字符串作為用戶輸入的命令。
代碼如下:
int len = 0;
int ch;
char buf[300];
ch = getchar();
while(len < BUFSIZ && ch != '\n') {
?????? buf[len++] = ch;
?????? ch = getchar();
}
if(len == BUFSIZ) {
?????? printf("command is too long\n");
?????? break;
}
buf[len] = '\n';
len++;
buf[len] = 0;
但是,我們注意到,在 bash 中,可以用“<-”和“->”鍵在命令行中左右移動(dòng),可以用上下鍵調(diào)用以前使用的命令,可以用退格鍵來(lái)刪除一個(gè)字符,還可以用 tab 鍵來(lái)進(jìn)行命令行補(bǔ)全。我們的shell如果也要支持這些功能,那么就必須對(duì)這些鍵進(jìn)行處理。這樣僅僅對(duì)用戶輸入的讀取就非常麻煩了。
實(shí)際上,任何需要一個(gè)獲取用戶輸入的程序,都會(huì)涉及到同樣的問(wèn)題,如何象bash 那樣處理鍵盤?GNU readline 庫(kù)就是專門解決這個(gè)問(wèn)題的,它把對(duì)鍵盤的操作完全封裝起來(lái),對(duì)外只提供一個(gè)簡(jiǎn)單的調(diào)用接口。有了它,對(duì)鍵盤的處理就不再讓人頭疼了。
關(guān)于 readline 庫(kù)的詳細(xì)信息,可以通過(guò) man readline 來(lái)看它的幫助頁(yè)面。在我們的 shell 程序中,我是這樣來(lái)使用 readline的。
char* line;
char prompt[200];
while(1) {
?????? set_prompt(prompt);
?????? if(!(line = readline(prompt)))
????????????? break;
?????? 。。。。。。
}
首先通過(guò) set_prompt() 來(lái)設(shè)置要輸出的提示符,然后以提示符作為參數(shù)調(diào)用 readline(),這個(gè)函數(shù)等待用戶輸入,并動(dòng)態(tài)創(chuàng)建一塊內(nèi)存來(lái)保存用戶輸入的數(shù)據(jù),可以通過(guò)返回的指針 line 得到這塊內(nèi)存。在每次處理完用戶輸入的命令之后,我們必須自己負(fù)責(zé)來(lái)釋放這塊內(nèi)存。
有了 readline 之后,我們就可以象 bash 那樣使用鍵盤了。
在通過(guò) readline 獲取用戶輸入之后,下一步就是對(duì)用戶輸入的命令進(jìn)行分析。
命令行分析
對(duì)命令行的分析,實(shí)際上是一個(gè)詞法分析過(guò)程。學(xué)過(guò)編譯原理的朋友,都聽說(shuō)過(guò) lex 和yacc 的大名,它們分別是詞法分析和語(yǔ)法分析工具。Lex 和 yacc 都有GNU的版本(open source 的思想實(shí)在是太偉大了,什么好東東都有免費(fèi)的用),分別是 flex 和 bison。
所謂“工欲善其事,必先利其器”,既然有這么好的工具,那我們就不必辛辛苦苦自己進(jìn)行詞法分析了。對(duì),我們要用 lex 來(lái)完成枯燥的命令行詞法分析工作。
“去買本《lex與yacc》(中國(guó)電力出版社)來(lái)看吧。第一次學(xué)當(dāng)然稍微有點(diǎn)難度,不過(guò)一旦掌握了,以后再碰到類似問(wèn)題,就可以多一個(gè)利器,可以節(jié)省勞動(dòng)力了。
在我們的這個(gè) shell 程序中,用 flex 來(lái)完成詞法分析工作。相對(duì)語(yǔ)法分析來(lái)說(shuō),詞法分析要簡(jiǎn)單的多。由于我們只是做一個(gè)簡(jiǎn)單的 shell,因此并沒(méi)有用到語(yǔ)法分析,而實(shí)際上在 bash 的實(shí)現(xiàn)代碼中,就用到了語(yǔ)法分析和 yacc。
關(guān)于 lex 的細(xì)節(jié),在這里我就不能多說(shuō)了。Lex程序,通常分為三個(gè)部分,其中進(jìn)行語(yǔ)法分析工作的就是它的第二部分: “規(guī)則”。規(guī)則定義了在詞法分析過(guò)程中,遇到什么樣的情況,應(yīng)該如何處理。
詞法分析的思路,就是根據(jù)前面定義的“shell語(yǔ)法規(guī)范”來(lái)把用戶輸入的命令行拆解成
首先,我們要把用戶輸入的命令,以空白字符(tab鍵或者空格)分隔成一個(gè)個(gè)的參數(shù),并把這些參數(shù)保存到一個(gè)參數(shù)數(shù)組中。但是,這其中有幾種特殊情況。
一、如果遇到的字符是“;”、“>”、“<”或“|”,由于這些符號(hào)是管道或者列表中所用到的分隔符,因此必須把它們當(dāng)作一個(gè)單獨(dú)的參數(shù)。
二、以雙引號(hào)(”)括起來(lái)的字符串要作為一個(gè)單獨(dú)的參數(shù),即使其中出現(xiàn)了空白字符、“;”、“>”、“<”、“|”。其實(shí),在POSIX標(biāo)準(zhǔn)中,對(duì)引號(hào)的處理相當(dāng)復(fù)雜,不僅包括雙引號(hào)(”),還有單引號(hào)(’)、反引號(hào)(`),在什么情況下,應(yīng)該用什么樣的引號(hào)以及對(duì)引號(hào)中的字符串應(yīng)該如何解釋,都有一大堆的條款。我們這里只是處理一種極簡(jiǎn)單的情況。
其次,如果我們遇到換行符(’\n’),那么就結(jié)束本次命令行分析。根據(jù)前面定義的 shell 語(yǔ)法規(guī)范,最上層的是列表命令,因此下一步是把所有的參數(shù)作為一個(gè)列表命令來(lái)處理。
根據(jù)這個(gè)思路,我們來(lái)看對(duì)應(yīng)的 lex 規(guī)則。
%%
"\""??????????? {BEGIN QUOTE;}
<QUOTE>[^\n"]+? {add_arg(yytext);}
<QUOTE>"\""???? {BEGIN 0;}
<QUOTE>\n?????? {BEGIN 0; do_list_cmd(); reset_args();}
";"???????????? {add_simple_arg(yytext);}
">"???????????? {add_simple_arg(yytext);}
"<"???????????? {add_simple_arg(yytext);}
"|"???????????? {add_simple_arg(yytext);}
[^ \t\n|<>;"]+? {add_arg(yytext);}
\n????????????? {do_list_cmd(); reset_args();}
.?????????????? ;
%%
我們對(duì)這些規(guī)則逐條解釋:
1-4這4條規(guī)則,目的是為了在命令行中支持引號(hào),它們用到了 lex 規(guī)則的狀態(tài)特性。
1、"\""??????????? {BEGIN QUOTE;}
2、<QUOTE>[^\n"]+? {add_arg(yytext);}
3、<QUOTE>"\""???? {BEGIN 0;}
4、<QUOTE>\n?????? {BEGIN 0; do_list_cmd(); reset_args();}
1、? 如果掃描到引號(hào)( “),那么進(jìn)入 QUOTE 狀態(tài)。在這個(gè)狀態(tài)下,即使掃描到空白字符或“;”、“>”、“<”、“|”,也要當(dāng)作普通的字符。
2、? 如果處于 QUOTE狀態(tài),掃描到除引號(hào)和回車以外的字符,那么調(diào)用 add_arg()函數(shù),把整個(gè)字符串加入到參數(shù)數(shù)組中。
3、? 如果處于QUOTE狀態(tài),掃描到引號(hào),那么表示匹配了前面的引號(hào),于是恢復(fù)到默認(rèn)狀態(tài)。
4、? 如果處于QUOTE狀態(tài),掃描到回車,那么結(jié)束了本次掃描,恢復(fù)到默認(rèn)狀態(tài),并執(zhí)行 do_list_cmd(),來(lái)執(zhí)行對(duì)列表命令的處理。
以下幾條規(guī)則,是在處于默認(rèn)狀態(tài)的情況下的處理。
5、";"????????????? {add_simple_arg(yytext);}
6、">"????????????? {add_simple_arg(yytext);}
7、"<"????????????? {add_simple_arg(yytext);}
8、"|"?????????????? {add_simple_arg(yytext);}
9、[^ \t\n|<>;"]+????? {add_arg(yytext);}
10、\n?????????????? {do_list_cmd(); reset_args();}
5、? 如果遇到分號(hào)(;),因?yàn)檫@是一個(gè)列表命令結(jié)束的操作符,所以作為一個(gè)單獨(dú)的參數(shù),執(zhí)行 add_simple_arg(),將它加入?yún)?shù)數(shù)組。
6、? 如果遇到 >,因?yàn)檫@是一個(gè)簡(jiǎn)單命令結(jié)束的操作符,所以作為一個(gè)單獨(dú)的參數(shù),執(zhí)行 add_simple_arg(),將它加入?yún)?shù)數(shù)組。
7、? 如果遇到 <,因?yàn)檫@是一個(gè)簡(jiǎn)單命令結(jié)束的操作符,所以作為一個(gè)單獨(dú)的參數(shù),執(zhí)行 add_simple_arg(),將它加入?yún)?shù)數(shù)組。
8、? 如果遇到管道符號(hào)(|),因?yàn)檫@是一個(gè)管道命令結(jié)束的操作符,所以作為一個(gè)單獨(dú)的參數(shù),執(zhí)行 add_simple_arg(),將它加入?yún)?shù)數(shù)組。
9、? 對(duì)于不是制表符(tab)、換行符(’\n’)、| 、<、>和分號(hào)(;)以外的字符序列,作為一個(gè)普通的參數(shù),加入?yún)?shù)數(shù)組。
10、????????????? 如果遇到換行符,那么結(jié)束本次掃描,執(zhí)行 do_list_cmd(),來(lái)執(zhí)行對(duì)列表命令的處理。
11、????????????? 對(duì)于任意其它字符,忽略
通過(guò) lex 的“規(guī)則”把用戶輸入的命令行分解成一個(gè)個(gè)的參數(shù)之后,都要執(zhí)行 do_list_cmd() 來(lái)執(zhí)行對(duì)列表命令的處理。
命令處理
首先是對(duì)處于“shell語(yǔ)法規(guī)范”中最上層的列表命令的處理。
l???????? 列表命令的處理過(guò)程:
依次檢查參數(shù)數(shù)組中的每一個(gè)參數(shù),如果是分號(hào)(;),那么就認(rèn)為分號(hào)前面的所有參數(shù)組成了一個(gè)管道命令,調(diào)用 do_pipe_cmd() 來(lái)執(zhí)行對(duì)管道命令的處理。如果掃描到最后,不再有分號(hào)出現(xiàn),那么把剩下的所有參數(shù)作為一個(gè)管道命令處理。
代碼很簡(jiǎn)單:
static void do_list_cmd()
{
?????? int i = 0;
?????? int j = 0;
?????? char* p;
?????? while(argbuf[i]) {
????????????? if(strcmp(argbuf[i], ";") == 0) {//? ;
???????????????????? p = argbuf[i];
???????????????????? argbuf[i] = 0;
???????????????????? do_pipe_cmd(i-j, argbuf+j);
???????????????????? argbuf[i] = p;
???????????????????? j = ++i;
????????????? } else
???????????????????? i++;
?????? }
?????? do_pipe_cmd(i-j, argbuf+j);
}
接下來(lái)是對(duì)管道命令的處理。
管道命令的處理
管道是進(jìn)程間通信(IPC)的一種形式,關(guān)于管道的詳細(xì)解釋在《unix高級(jí)環(huán)境編程》第14章:進(jìn)程間通信以及《unix網(wǎng)絡(luò)編程:第2卷:進(jìn)程間通信》第4章:管道和FIFO中可以看到。
我們還是來(lái)看一個(gè)管道的例子:
[root@stevens root]#? echo “hello world”|wc –c |wc –l
在這個(gè)例子中,有三個(gè)簡(jiǎn)單命令和兩個(gè)管道。
第一個(gè)命令是 echo “hello world”,它在屏幕上輸出 hello world。由于它后面是一個(gè)管道,因此,它并不在屏幕上輸出結(jié)果,而是把它的輸出重定向到管道的寫入端。
第二個(gè)命令是 wc –c,它本來(lái)需要指定輸入源,由于它前面是一個(gè)管道,因此它就從這個(gè)管道的讀出端讀數(shù)據(jù)。也就是說(shuō)讀到的是 hello world,wc –c 是統(tǒng)計(jì)讀到的字符數(shù),結(jié)果應(yīng)該是12。由于它后面又出現(xiàn)一個(gè)管道,因此這個(gè)結(jié)果不能輸出到屏幕上,而是重定向到第二個(gè)管道的寫入端。
第三個(gè)命令是 wc –l。它同樣從第二個(gè)管道的讀出端讀數(shù)據(jù),讀到的是12,然后它統(tǒng)計(jì)讀到了幾行數(shù)據(jù),結(jié)果是1行,于是在屏幕上輸出的最終結(jié)果是1。
在這個(gè)例子中,第一個(gè)命令只有一個(gè)“后”管道,第三個(gè)命令只有一個(gè)“前”管道,而第二個(gè)命令既有“前”管道,又有“后”管道。
在我們處理管道命令的do_pipe_cmd()函數(shù)中,它的處理過(guò)程是:
首先定義兩個(gè)管道 prefd 和 postfd,它們分別用來(lái)保存“前”管道和“后”管道。此外,還有一個(gè)變量 prepipe 來(lái)指示“前”管道是否有效。
然后依次檢查參數(shù)數(shù)組中每一個(gè)參數(shù),如果是管道符號(hào)(|),那么就認(rèn)為管道符號(hào)前面所有的參數(shù)組成了一個(gè)簡(jiǎn)單命令,并創(chuàng)建一個(gè)“后”管道。如果沒(méi)有“前”管道(管道中第一個(gè)簡(jiǎn)單命令是沒(méi)有“前”管道的),那么只傳遞“后”管道來(lái)調(diào)用do_simple_cmd(),否則,同時(shí)傳遞“前”管道和“后”管道來(lái)調(diào)用 do_simple_cmd()。
執(zhí)行完以后,用“前”管道來(lái)保存當(dāng)前的“后”管道,并設(shè)置“前”管道有效標(biāo)識(shí)prepipe,繼續(xù)往后掃描。如果掃描到最后,不再有管道符號(hào)出現(xiàn),那么只傳遞“前”管道來(lái)調(diào)用do_simple_cmd()。
代碼如下:
int i = 0, j = 0, prepipe = 0;
int prefd[2], postfd[2];
char* p;
while(argv[i]) {
?????? if(strcmp(argv[i], "|") == 0) { // pipe
????????????? p = argv[i];
????????????? argv[i] = 0;
????????????? pipe(postfd);???????? //create the post pipe
????????????? if(prepipe)?????
???????????????????? do_simple_cmd(i-j, argv+j, prefd, postfd);
????????????? else
???????????????????? do_simple_cmd(i-j, argv+j, 0, postfd);
????????????? argv[i] = p;
????????????? prepipe = 1;
????????????? prefd[0] = postfd[0];
????????????? prefd[1] = postfd[1];
????????????? j = ++i;
?????? } else
????????????? i++;
}
if(prepipe)
?????? do_simple_cmd(i-j, argv+j, prefd, 0);
else
?????? do_simple_cmd(i-j, argv+j, 0, 0);
最后,我們分析簡(jiǎn)單命令的處理過(guò)程。
簡(jiǎn)單命令處理過(guò)程
我們已經(jīng)看到,對(duì)列表命令和管道命令的處理,實(shí)際只是一個(gè)分解過(guò)程,最終命令的執(zhí)行還是要由簡(jiǎn)單命令來(lái)完成。
在簡(jiǎn)單命令的處理過(guò)程中,必須考慮以下情況:
1、區(qū)分內(nèi)部命令和外部命令
根據(jù)簡(jiǎn)單命令的定義,它的第一個(gè)參數(shù)是要執(zhí)行的命令,后面的參數(shù)作為該命令的參數(shù)。要執(zhí)行的命令有兩種情況:
一種是外部命令,也就是對(duì)應(yīng)著磁盤上的某個(gè)程序,例如 wc、ls等等。對(duì)這種外部命令,我們首先要到指定的路徑下找到它,然后再執(zhí)行它。
二是內(nèi)部命令,內(nèi)部命令并不對(duì)應(yīng)磁盤上的程序,例如cd、echo等等,它需要shell自己來(lái)決定該如何執(zhí)行。例如對(duì) cd 命令,shell就應(yīng)該根據(jù)它后面的參數(shù)改變當(dāng)前路徑。
對(duì)于外部命令,需要?jiǎng)?chuàng)建一個(gè)子進(jìn)程來(lái)執(zhí)行它,而對(duì)于內(nèi)部命令,則沒(méi)有這個(gè)必要。
外部命令的執(zhí)行,是通過(guò) exec 函數(shù)來(lái)完成的。有六種不同形式的 exec 函數(shù),它們可以統(tǒng)稱為 exec 函數(shù)。我們使用的是 execv()。關(guān)于 exec的細(xì)節(jié),請(qǐng)看《unix環(huán)境高級(jí)編程》第8章:進(jìn)程控制。
對(duì)于內(nèi)部命令,我們目前支持五種,分別是:
exit:退出shell解釋器
cd:改變目錄
echo:回顯
export:導(dǎo)入或顯示環(huán)境變量
history:顯示命令歷史信息
這幾個(gè)內(nèi)部命令分別由 do_exit()、do_cd()、do_echo()、do_export()、do_history()來(lái)實(shí)現(xiàn)。
2、處理重定向
在簡(jiǎn)單命令的定義中,包括了對(duì)重定向的支持。重定向有多種情況,最簡(jiǎn)單的是輸入重定向和輸出重定向,分別對(duì)應(yīng)著“<”和“>”。
輸入重定向,就是把“<”后面指定的文件作為標(biāo)準(zhǔn)輸入,例如:
wc < xxx
?????? 表示把 xxx 這個(gè)文件的內(nèi)容作為 wc 命令的輸入。
輸出重定向,就是把“>”后面指定的文件作為標(biāo)準(zhǔn)輸出,例如:
echo “hello world” > xxx
表示把 echo “hello world” 的結(jié)果輸入到 xxx 文件中,而不是屏幕上。
為了支持重定向,我們首先對(duì)簡(jiǎn)單命令的參數(shù)進(jìn)行掃描,如果遇到“<”或者“>”那么就認(rèn)為遇到了重定向,并把“<”或者“>”符號(hào)后面的參數(shù)作為重定向的文件名稱。
對(duì)于輸入重定向,首先是以只讀方式打開“<”后面的文件,并獲得文件描述符,然后將該文件描述符復(fù)制給標(biāo)準(zhǔn)輸入。
對(duì)于輸出重定向,首先是以寫方式打開“>”后面的文件,并獲得文件描述符,然后將該文件描述符復(fù)制給標(biāo)準(zhǔn)輸出。
具體實(shí)現(xiàn)在 predo_for_redirect() 函數(shù)中:
3、管道的實(shí)現(xiàn)
管道的實(shí)現(xiàn)實(shí)際上也是一種重定向的處理。對(duì)于“前”管道,類似于輸入重定向,不同的是,它是把一個(gè)指定的描述符(“前”管道的輸出端)復(fù)制給標(biāo)準(zhǔn)輸入。對(duì)于“后”管道,類似于輸出重定向,不同的是,它把一個(gè)指定的描述符(“后”管道的輸入端)復(fù)制給標(biāo)準(zhǔn)輸出。
在對(duì)管道的處理上,還必須要注意管道和輸入或輸出重定向同時(shí)出現(xiàn)的情況,如果是一個(gè)“前”管道和一個(gè)輸入重定向同時(shí)出現(xiàn),那么優(yōu)先處理輸入重定向,不再?gòu)摹扒啊惫艿乐凶x取數(shù)據(jù)了。同樣,如果一個(gè)“后”管道和一個(gè)輸出重定向同時(shí)出現(xiàn),那么優(yōu)先處理輸出重定向,不再把數(shù)據(jù)輸出到“后”管道中。
?????? 至此,我們已經(jīng)描述了實(shí)現(xiàn)一個(gè)簡(jiǎn)單的 shell 解釋器的全部過(guò)程,相應(yīng)的代碼和 makefile 在我們的網(wǎng)站上可以下載。希望大家能夠結(jié)合代碼和這篇文章,親自動(dòng)手做一次,以加深對(duì)shell 解釋器的理解。
更多文章、技術(shù)交流、商務(wù)合作、聯(lián)系博主
微信掃碼或搜索:z360901061

微信掃一掃加我為好友
QQ號(hào)聯(lián)系: 360901061
您的支持是博主寫作最大的動(dòng)力,如果您喜歡我的文章,感覺(jué)我的文章對(duì)您有幫助,請(qǐng)用微信掃描下面二維碼支持博主2元、5元、10元、20元等您想捐的金額吧,狠狠點(diǎn)擊下面給點(diǎn)支持吧,站長(zhǎng)非常感激您!手機(jī)微信長(zhǎng)按不能支付解決辦法:請(qǐng)將微信支付二維碼保存到相冊(cè),切換到微信,然后點(diǎn)擊微信右上角掃一掃功能,選擇支付二維碼完成支付。
【本文對(duì)您有幫助就好】元
