之前為家介紹了關于如何構(gòu)建一個Linux Shell的一、二、三、四。今天這里為大家介紹的是關于如何構(gòu)建Linux Shell的教程的第五部分。正如我們在前面的部分中所看到的,一個簡單的命令由一個或多個參數(shù)組成。第一個單詞包含我們要執(zhí)行的命令的名稱,其余單詞包含命令的參數(shù)。在外殼程序執(zhí)行命令之前,它將對命令字執(zhí)行字擴展。
單詞擴展是shell提取命令字,檢查它是否包含變量名,路徑名,命令和算術表達式,然后用其值替換每個名稱/命令/表達式的過程。
通常長于原始單詞的結(jié)果單詞可以在稱為字段拆分的過程中分解為一個或多個子單詞。
在這一部分中,我們將實現(xiàn)POSIX定義的7個詞擴展,它們是:波浪號擴展,參數(shù)擴展,算術擴展,命令替換,字段拆分,路徑名擴展和引號刪除。還有其他單詞擴展,例如大括號擴展和流程替換,這些沒有由POSIX定義,因此我們將不在這里討論。完成本課程后,如果您通過實現(xiàn)非POSIX單詞擴展來擴展Shell,將是一個很好的練習。
單詞擴展過程
當外殼程序執(zhí)行單詞擴展時,它將檢查命令行中的每個單詞以查看其是否包含可能的單詞擴展。擴展可以出現(xiàn)在單詞的任何位置:在單詞的開頭,中間或結(jié)尾。擴展可能還包括整個單詞。
單詞擴展之前有一個$標志。后面的字符$符號指示外殼要執(zhí)行的擴展類型。這些字符由外殼解釋如下:
一個或多個數(shù)字,指示位置參數(shù)的變量擴展。
之一@,*,#,?,-,$,!,要么0,它指示特殊參數(shù)的變量擴展。
一個或多個字母數(shù)字字符和/或下劃線,以字母或下劃線開頭,表示外殼變量名稱。
括號內(nèi)的變量名{和}。
算術展開,被(和)。
命令替換,由((和))。
Shell首先執(zhí)行波浪號擴展,參數(shù)擴展,命令替換和算術擴展,然后進行字段拆分和路徑名擴展。最后,shell從已擴展單詞中刪除了已成為原始單詞一部分的所有引號字符。
使用文字
當外殼程序執(zhí)行單詞擴展時,該過程可能會導致零個,一個或多個單詞。
structword_s
{
char*data;
intlen;
structword_s*next;
};
該結(jié)構(gòu)包含以下字段:
data=>表示此單詞的字符串。
len=>的長度data領域。
next=>指向下一個單詞的指針,或者NULL如果這是最后一個單詞。
當然,我們需要一些函數(shù)來分配和釋放我們的structword_s結(jié)構(gòu)。為此,我們將使用以下功能:
structword_s*make_word(char*str);
voidfree_all_words(structword_s*first);
第一個函數(shù)為該結(jié)構(gòu)分配內(nèi)存,創(chuàng)建單詞字符串的副本,然后返回新分配的單詞。第二個功能釋放單詞結(jié)構(gòu)列表使用的內(nèi)存。您可以在我們的網(wǎng)站中閱讀功能代碼wordexp.c源文件。
定義一些助手功能
如前所述,詞擴展是一個復雜的過程,為此,我們需要定義許多不同的功能。在深入研究單詞擴展的細節(jié)之前,讓我們先定義一些輔助函數(shù)。
以下列表顯示了我們的輔助函數(shù)的函數(shù)原型,所有這些我們將在wordexp.c源文件:
char*wordlist_to_str(structword_s*word);
voiddelete_char_at(char*str,size_tindex);
intis_name(char*str);
size_tfind_closing_quote(char*data);
size_tfind_closing_brace(char*data);
char*substitute_str(char*s1,char*s2,size_tstart,size_tend);
intsubstitute_word(char**pstart,char**p,size_tlen,char*(func)(char*),intadd_quotes);
以下是這些函數(shù)的功能細目:
wordlist_to_str()=>將擴展單詞的鏈接列表轉(zhuǎn)換為單個字符串。
delete_char_at()=>從字符串中刪除給定索引處的字符。
is_name()=>檢查字符串是否表示有效的變量名。
find_closing_quote()=>當單詞擴展包含開頭的引號",',要么`,我們需要找到匹配的用引號引起來的字符,該字符將引號引起來的字符串括起來。此函數(shù)返回單詞中結(jié)束引號字符的從零開始的索引。
find_closing_brace()=>與上面類似,不同之處在于它找到匹配的右括號。也就是說,如果左括號是{,(,要么[,此函數(shù)返回匹配項的從零開始的索引},),要么]字符。查找引號對對于處理參數(shù)擴展,算術擴展和命令替換很重要。
substitute_str()=>替換的子字符串s1,從位置上的角色開始start到那個位置end,s2串。當單詞擴展是較長單詞的一部分例如,${PATH}/ls,在這種情況下,我們只需要擴展${PATH},然后附加/ls到擴展字符串的末尾。
substitute_word()=>一個幫助程序函數(shù),它調(diào)用其他單詞擴展功能,我們將在以下各節(jié)中對其進行定義。
另外,我們將定義一些函數(shù)來幫助我們處理字符串。我們將在strings.c源文件:
char*strchr_any(char*string,char*chars);
char*quote_val(char*val,intadd_quotes);
intcheck_buffer_bounds(int*count,int*len,char***buf);
voidfree_buffer(intlen,char**buf);
這些功能的作用如下:
strchr_any()=>類似于strchr(),除了它會在給定的字符串中搜索任何給定的字符。
quote_val()=>執(zhí)行與引號刪除相反的操作,即將字符串轉(zhuǎn)換為帶引號的字符串。
的check_buffer_bounds()和free_buffer()函數(shù)將使我們的后端執(zhí)行器支持可變數(shù)量的命令參數(shù),而不是我們在第二部分中設置的硬限制255。
現(xiàn)在,讓我們編寫函數(shù)來處理每種類型的單詞擴展。
波浪號擴展
在波浪符號擴展期間,外殼程序?qū)⒉ɡ朔栕址鎿Q為用戶主目錄的路徑名。例如,~和~/波浪線擴展到當前用戶的主目錄,而~john被波浪擴展到用戶John的主目錄,依此類推。除后面的所有字符之外,代字號字符被稱為代字號前綴
要執(zhí)行波浪線擴展,我們將定義tilde_expand()函數(shù),具有以下原型:
char*tilde_expand(char*s);
該函數(shù)接受一個參數(shù):我們要擴展的代字號前綴。如果擴展成功,該函數(shù)返回一個的malloc表示波浪線擴展前綴“d字符串。否則返回NULL。下面是該函數(shù)為擴展代字號前綴所做的工作的快速分解:
如果前綴是~,獲得$HOME外殼變量。如果$HOME被定義而不是NULL,返回其值。否則,通過調(diào)用獲取當前的用戶ID(UID)getuid(),然后將UID傳遞給getpwuid()獲取與當前用戶相對應的密碼數(shù)據(jù)庫條目。的pw_dir密碼數(shù)據(jù)庫條目的“字段”包含函數(shù)返回的主目錄的路徑名。
如果前綴包含其他字符除了前導字符~,我們將這些字母作為要獲取其主目錄的用戶的名稱。我們稱之為getpwnam(),將其傳遞給用戶名,然后返回pw_dir領域。
如果我們無法檢索主目錄,則返回NULL。否則,我們將返回主目錄路徑的malloc副本。
參數(shù)擴展
在參數(shù)擴展中,外殼程序用變量的值替換外殼程序變量的名稱。參數(shù)擴展使外殼程序可以執(zhí)行諸如echo$PATH。在此示例中,外殼程序?qū)?PATH變量,將其替換為實際的可執(zhí)行路徑。
為了向shell發(fā)出我們要擴展shell變量的信號,我們在變量名稱前添加一個$標志。也就是說,擴大PATH,USER和SHELL變量,我們需要傳遞$PATH,$USER和$SHELL將單詞分別傳遞給shell或者,我們可以將這些變量擴展傳遞給shellshell,如下所示:${PATH},${USER}和${SHELL}。Shell變量名稱可以包含字母,數(shù)字和下劃線的任意組合。名稱可以包含大寫字母或小寫字母,盡管按照慣例,大寫名稱保留用于標準Shell變量。
我們可以使用參數(shù)擴展修飾符來控制外殼如何執(zhí)行參數(shù)擴展,該修飾符告訴外殼我們要擴展值的哪一部分,以及在沒有給定名稱的外殼變量的情況下該怎么做。下表總結(jié)了參數(shù)擴展修飾符由POSIX定義的修飾符在“描述”列中由POSIX單詞標記。大多數(shù)外殼程序都支持其他修飾符,我們將不在此處討論。有關非POSIX修飾符的更多信息,請參見您的Shell的手冊頁。
要執(zhí)行參數(shù)擴展,我們將定義var_expand()函數(shù),具有以下原型:
char*var_expand(char*orig_var_name);
該函數(shù)接受一個參數(shù):我們要擴展的參數(shù)。如果擴展成功,該函數(shù)將返回一個包含擴展值的malloc'd字符串。否則返回NULL。下面是該函數(shù)為擴展變量名以獲取其值而執(zhí)行的操作的快速細分:
如果變量名用大括號括起來,請刪除大括號,因為它們不是變量名本身的一部分。如果名稱以#,我們需要獲取變量名稱的長度。
如果變量名稱包含冒號,我們將使用它來將名稱與單詞或模式分開。單詞或圖案的使用如上表所示。獲取具有給定變量名稱的符號表條目。獲取符號表條目的值。
如果該值為空或為空,請使用擴展中提供的替代詞。
如果該值不為空,則將該值用作擴展結(jié)果。要使外殼執(zhí)行模式匹配${parameter#word}和${parameter%word}擴展,我們需要兩個幫助函數(shù):match_suffix()和match_prefix()。我們不會在這里討論這些功能,但是您可以從此鏈接中閱讀它們的代碼。
如果擴展修飾符為${parameter:=word},我們需要將符號表條目的值設置為剛剛擴展的值。
如果擴展以#,獲取擴展值的長度并將其用作最終結(jié)果。返回擴展值的malloc'd副本或其長度,視情況而定。
命令替換
在命令替換中,shell派生一個進程來運行命令,然后用命令的輸出替換命令替換擴展。例如,在以下循環(huán)中:
foriin$(ls);doecho$i;done
外殼分叉一個過程,其中l(wèi)s命令運行。該命令的輸出是當前目錄中的文件列表。Shell獲取該輸出,將其拆分為單詞列表,然后一次將這些單詞提供給循環(huán)。在循環(huán)的每次迭代中,變量$i被分配了列表中下一個文件的名稱。
此名稱將傳遞給echo命令,該命令在單獨的行上輸出名稱。
命令替換可以寫成$(command),要么`command`。要執(zhí)行命令替換,我們將定義command_substitute()函數(shù),具有以下原型:
char*command_substitute(char*orig_cmd);
該函數(shù)接受一個參數(shù):我們要執(zhí)行的命令。如果擴展成功,則該函數(shù)將返回一個malloc'd字符串,表示命令的輸出。如果擴展失敗,或者命令沒有輸出任何內(nèi)容,則函數(shù)返回NULL。
下面是該函數(shù)為擴展命令替換而執(zhí)行的操作的快速細分:
根據(jù)使用的格式,我們首先刪除$()或反引號``。這給我們留下了我們需要執(zhí)行的命令。
呼叫popen()創(chuàng)建一個管道。我們將要執(zhí)行的命令傳遞給popen(),我們得到一個指向FILE流,我們將從中讀取命令的輸出。
呼叫fread()從管道讀取命令的輸出。將讀取的字符串存儲在緩沖區(qū)中。
刪除所有尾隨換行符。
關閉管道,并使用命令輸出返回緩沖區(qū)。
算術擴展
使用算術擴展,我們可以讓外殼執(zhí)行不同的算術運算,并將結(jié)果用于執(zhí)行其他命令。盡管POSIX要求外殼程序僅支持帶符號的長整數(shù)算法,但許多外殼程序都支持浮點算法。
此外,盡管大多數(shù)外殼程序都不需要外殼程序來支持任何數(shù)學函數(shù)。對于簡單的shell,我們將僅支持帶符號的長整數(shù)算法,而沒有數(shù)學函數(shù)支持。
算術擴展寫為$((expression))。
要執(zhí)行擴展,我們將定義arithm_expand()函數(shù),具有以下原型:
char*arithm_expand(char*expr);
的arithm_expand()函數(shù)接收包含算術表達式的字符串,執(zhí)行必要的計算,然后以malloc'd字符串的形式返回結(jié)果。該函數(shù)及其相關的幫助器函數(shù)既復雜又冗長,但這是主要亮點:
算術表達式轉(zhuǎn)換為反向波蘭表示法,更易于分析和計算。RPN由一系列算術運算組成,其中運算符遵循其操作數(shù)。例如,RPNx-y是xy-,以及3+4×(2?1)是3421?×+。
在轉(zhuǎn)換過程中,算術運算符被壓入一個運算符堆棧,我們將從中彈出每個運算符并稍后執(zhí)行其運算。同樣,操作數(shù)被添加到它們自己的堆棧中。
一次將操作員從堆棧中彈出,然后對操作員進行檢查。根據(jù)運算符的類型,一個或兩個操作數(shù)從堆棧中彈出。控制此過程的規(guī)則是調(diào)車場算法的規(guī)則,您可以在此處閱讀。
結(jié)果將轉(zhuǎn)換為字符串,然后將其返回給調(diào)用方。
場分裂
在字段拆分期間,shell會獲取參數(shù)擴展,命令替換和算術擴展的結(jié)果,并將它們拆分為一個或多個部分,我們將其稱為字段。該過程取決于$IFS外殼變量。IFS是一個歷史術語,代表內(nèi)部字段分隔符,它起源于Unixshell沒有內(nèi)置數(shù)組類型的時間。
作為解決方法,早期的Unixshell必須找到另一種表示多成員數(shù)組的方式。外殼程序?qū)⒁詥蝹€字符串將數(shù)組成員連接在一起,并以空格分隔。當外殼程序需要檢索數(shù)組成員時,它將字符串分成一個或多個字段。的$IFS變量告訴外殼程序在何處確切地中斷該字符串。
外殼解釋$IFS如下字符:
如果值$IFS是空格,制表符和換行符,或者如果未設置變量,則在輸入的開頭或結(jié)尾處的空格,制表符或換行符的任何序列都將被忽略,并且輸入中這些字符的任何序列應定界領域。
如果值$IFS為null,不得執(zhí)行任何字段拆分。
否則,應依次適用以下規(guī)則:(a)$IFS在輸入的開頭和結(jié)尾應忽略空格。(b)輸入中的每次出現(xiàn)$IFS不是的字符$IFS空白,以及任何相鄰的空白$IFS如前所述,空白應界定一個字段。(c)非零長度$IFS空白應界定一個字段。
要執(zhí)行擴展,我們將定義field_split()函數(shù),具有以下原型:
structword_s*field_split(char*str);
路徑名擴展
在擴展路徑名期間,shell將以給定的模式匹配一個或多個文件名。除特殊字符外,該模式還可以包含普通字符*,?和[],也稱為“通配符”。
星號*匹配任意長度的字符,匹配一個字符,并且方括號引入正則表達式(RE)括號表達式。擴展的結(jié)果是名稱與模式匹配的文件列表。
要執(zhí)行擴展,我們將定義pathnames_expand()函數(shù),具有以下原型:
structword_s*pathnames_expand(structword_s*words);
此函數(shù)接受一個參數(shù):指向我們要路徑名擴展的單詞的鏈接列表中第一個單詞的指針。對于每個單詞,該函數(shù)執(zhí)行以下操作:
檢查單詞是否包含任何通配符*,?和[],通過調(diào)用輔助函數(shù)has_glob_chars(),我們將在源文件中定義pattern.c。如果單詞包含通配符,我們將其視為需要匹配的模式;否則,我們移至下一個單詞。
獲取名稱與模式匹配的文件列表,不包括特殊名稱.和..。我們將模式匹配委托給另一個幫助函數(shù),get_filename_matches(),我們將在同一源文件中定義pattern.c。
將匹配的文件名添加到最終列表。
移至下一個單詞并循環(huán)。
返回與所有給定單詞匹配的文件名列表。
刪除報價
單詞擴展過程的最后一步是刪除引號。引用用于刪除某些字符到shell的特殊含義。外殼會以特殊方式處理某些字符,例如反斜杠和引號。要禁止這種行為,我們需要引用那些字符以強制外殼將它們視為普通字符。
我們可以使用以下三種方式之一對字符進行引用:使用反斜杠,單引號或雙引號。反斜杠字符用于保留反斜杠后面的字符的字面意思。這類似于我們用C語言轉(zhuǎn)義字符的方式。
單引號保留引號內(nèi)所有字符的字面含義,即外殼程序不嘗試對單引號字符串進行單詞擴展。
雙引號與單引號類似,不同之處在于外殼可以識別反引號,反斜杠和$標志。也就是說,外殼程序可以在雙引號字符串內(nèi)執(zhí)行單詞擴展。
要執(zhí)行報價刪除,我們將定義remove_quotes()函數(shù),具有以下原型:
voidremove_quotes(structword_s*wordlist)。
放在一起
現(xiàn)在我們有了詞擴展功能,是時候?qū)⑵浣Y(jié)合在一起了。在本節(jié)中,我們將編寫主要的單詞擴展功能,我們將調(diào)用該功能來執(zhí)行單詞擴展。反過來,此函數(shù)將調(diào)用其他函數(shù)來執(zhí)行單詞擴展的各個步驟。
我們的主要功能是word_expand(),我們將在源文件中定義wordexp.c:
structword_s*word_expand(char*orig_word);
這是為了對作為唯一參數(shù)傳遞的單詞執(zhí)行單詞擴展的功能:
創(chuàng)建原始單詞的副本。我們將在此副本上執(zhí)行單詞擴展,以便在出現(xiàn)任何問題時將原始單詞保留完整。逐字掃描單詞,尋找特殊字符~,",',`,=,和$。如果找到上述字符之一,請致電substitute_word(),這將調(diào)用相應的單詞擴展功能。
跳過任何沒有特殊含義的字符。完成單詞擴展后,通過調(diào)用執(zhí)行字段拆分field_split()。通過調(diào)用執(zhí)行路徑名擴展pathnames_expand()。通過調(diào)用執(zhí)行報價刪除remove_quotes()。返回擴展單詞的列表。
更新掃描儀
在本教程的第二部分中,我們編寫了tokenize()函數(shù),我們用來獲取輸入令牌。到目前為止,我們的tokenize()函數(shù)不知道如何處理帶引號的字符串和轉(zhuǎn)義字符。要添加此功能,我們需要更新代碼。打開scanner.c文件,并將以下代碼添加到tokenize()功能之后switch聲明的開頭括號:
case'"':
case''':
case'`':
add_to_buf(nc);
i=find_closing_quote(src->buffer+src->curpos);
if(!i)
{
src->curpos=src->bufsize;
fprintf(stderr,"error:missingclosingquote'%c' ",nc);
return&eof_token;
}
while(i--)
{
add_to_buf(next_char(src));
}
break;
case'\':
nc2=next_char(src);
if(nc2==' ')
{
break;
}
add_to_buf(nc);
if(nc2>0)
{
add_to_buf(nc2);
}
break;
case'$':
add_to_buf(nc);
nc=peek_char(src);
if(nc=='{'||nc=='(')
{
i=find_closing_brace(src->buffer+src->curpos+1);
if(!i)
{
src->curpos=src->bufsize;
fprintf(stderr,"error:missingclosingbrace'%c' ",nc);
return&eof_token;
}
while(i--)
{
add_to_buf(next_char(src));
}
}
elseif(isalnum(nc)||nc=='*'||nc=='@'||nc=='#'||
nc=='!'||nc=='?'||nc=='$')
{
add_to_buf(next_char(src));
}
break;
現(xiàn)在,我們的詞法掃描器知道如何識別和跳過帶引號的字符串,轉(zhuǎn)義字符和其他單詞擴展構(gòu)造。在此鏈接中查看更新的詞法掃描程序代碼。
更新執(zhí)行器最后,我們需要更新后端執(zhí)行程序,以便可以:
在執(zhí)行命令之前,對命令的參數(shù)執(zhí)行單詞擴展。每個命令支持超過255個參數(shù)。打開executor.c文件,導航到do_simple_command()函數(shù)并找到以下幾行:
intargc=0;
longmax_args=255;
char*argv[max_args+1];
char*str;
while(child)
{
...
}
argv[argc]=NULL;
并用以下代碼替換它們:
intargc=0;
inttargc=0;
char**argv=NULL;
char*str;
while(child)
{
str=child->val.str;
structword_s*w=word_expand(str);
if(!w)
{
child=child->next_sibling;
continue;
}
structword_s*w2=w;
while(w2)
{
if(check_buffer_bounds(&argc,&targc,&argv))
{
str=malloc(strlen(w2->data)+1);
if(str)
{
strcpy(str,w2->data);
argv[argc++]=str;
}
}
w2=w2->next;
}
free_all_words(w);
child=child->next_sibling;
}
if(check_buffer_bounds(&argc,&targc,&argv))
{
argv[argc]=NULL;
}
使用此代碼,執(zhí)行程序調(diào)用word_expand()在每個命令自變量上,并將擴展的單詞添加到自變量列表,我們最終將其傳遞給命令。該列表可以根據(jù)需要增長,這要歸功于我們check_buffer_bounds()函數(shù),根據(jù)需要將內(nèi)存分配給緩沖區(qū)。
現(xiàn)在剩下的就是在執(zhí)行完命令后釋放參數(shù)列表,然后返回調(diào)用者。為此我們致電:
free_buffer(argc,argv);
在三個不同的位置:執(zhí)行內(nèi)置實用程序后,如果fork()返回錯誤狀態(tài),然后waitpid()已經(jīng)回來了。在此鏈接中查看更新的執(zhí)行程序代碼。
編譯外殼
讓我們編譯一下shell。打開您喜歡的終端模擬器,導航到源目錄,并確保其中有19個文件和2個子目錄:
現(xiàn)在,使用以下命令編譯外殼程序:
make
如果一切順利gcc不應輸出任何內(nèi)容,并且應該有一個名為shell在當前目錄中:
現(xiàn)在通過運行來調(diào)用shell./shell,然后嘗試使用我們的單詞擴展功能并檢查結(jié)果:$echo*Makefilebuildbuiltinsexecutor.cexecutor.hinitsh.cmain.cnode.cnode.hparser.cparser.hpattern.cprompt.cscanner.cscanner.hshellshell.hshunt.csource.csource.hstrings.csymtabwordexp.c
$echo'*'
*
$echo~
/home/user
$echo~/Downloads
/home/user/Downloads
$echo${A=value}
value
$echo$A
value
$echo$((2+7))
9
就是今天。我們的外殼現(xiàn)在可以處理各種單詞擴展。玩弄外殼,看看從不同類型的單詞擴展中可以得到什么結(jié)果。將結(jié)果與從默認Shell獲得的結(jié)果進行比較。
在此,我們已經(jīng)取得了長足的進步,提供了許多代碼,其中大多數(shù)代碼沒有時間或空間來詳細研究。您可能需要花一些時間閱讀我們存儲庫中的代碼,以使自己熟悉單詞擴展過程。想了解更多關于Linux Shell 的信息,請繼續(xù)關注中培偉業(yè)。