Shell從標准輸入或腳本中讀取的每行稱為一個管道行,它包含一個或多個由0個或多個管道字符()分隔的命令。對每一個管道行,進行12個步驟的處理。一、bash命令處理的12個步驟;
+-------------+ 單引號 -------------------------> -------------------------- -----------------------> 1.分隔成記號---- --------------- -------------------> 雙引號 +-------------+ 讀取下一個命令 \/ +-------------------------------------------+ 2. ------ 檢驗第一個記號 開放的關鍵字 其他關鍵字 非關鍵字 +-------------------------------------------+ \/ +-----------------------------+ 擴展別名 3. 檢驗第一個記號 ------------ 別名 不是別名 +-----------------------------+ \/ +--------------+ 4.大括號擴展 +--------------+ \/ +--------------+ 5.~符號擴展 +--------------+ \/ +--------------+ 雙引號 6.參數擴展 <----------------- +--------------+ \/ +------------------------------+ 7.命令替換(嵌套命令行處理) +------------------------------+ \/ +--------------+ 雙引號 8.算術擴展 ------------------ +--------------+ \/ +--------------+ 9.單詞分割 +--------------+ \/ +--------------+ 10.路徑名擴展 +--------------+ \/ +----------------------------------------+ 11.命令查尋:函數,內置命令,可執行文件<-------- +----------------------------------------+ \/ 將參數帶入下一個命令 +-------------+ ----------eval-------------- 12.運行命令 +-------------+
結合上面的插圖,這裡給出命令行的12個步驟。1、將命令行分成由固定元字符集分隔的記號;
SPACE, TAB, NEWLINE, ; , (, ), <, >, , &記號類型包括單詞,關鍵字,I/O重定向符和分號。2、檢測每個命令的第一個記號,查看是否為不帶引號或反斜線的關鍵字。如果是一個開放的關鍵字,如if和其他控制結構起始字符串,function,{或(,則命令實際上為一復合命令。shell在內部對復合命令進行處理,讀取下一個命令,並重復這一過程。如果關鍵字不是復合命令起始字符串(如then等一個控制結構中間出現的關鍵字),則給出語法錯誤信號。3、依據別名列表檢查每個命令的第一個關鍵字;如果找到相應匹配,則替換其別名定義,並退回第一步;否則進入第4步。該策略允許遞歸別名,還允許定義關鍵字別名。如alias procedure=function4、執行大括號擴展,例如a{b,c}變成ab ac5、如果~位於單詞開頭,用$HOME替換~。使用usr的主目錄替換~user。6、對任何以符號$開頭的表達式執行參數(變量)替換;7、對形式$(string)的表達式進行命令替換;這裡是嵌套的命令行處理。8、計算形式為$((string))的算術表達式;9、把行的參數,命令和算術替換部分再次分成單詞,這次它使用$IFS中的字符做分割符而不是步驟1的元字符集;
10、對出現*, ?, [ / ]對執行路徑名擴展,也稱為通配符擴展;
11、按命令優先級表(跳過別名),進行命令查尋;
12、設置完I/O重定向和其他操作後執行該命令。
二、關於引用
1、單引號跳過了前10個步驟,不能在單引號裡放單引號2、雙引號跳過了步驟1~5,步驟9~10,也就是說,只處理6~8個步驟。
也就是說,雙引號忽略了管道字符,別名,~替換,通配符擴展,和通過分隔符分裂成單詞。雙引號裡的單引號沒有作用,但雙引號允許參數替換,命令替換和算術表達式求值。可以在雙引號裡包含雙引號,方式是加上轉義符"\",還必須轉義$, `, \。
三、eval的作用;
eval的作用是再次執行命令行處理,也就是說,對一個命令行,執行兩次命令行處理。這個命令要用好,就要費一定的功夫。我舉兩個例子,拋磚引玉。
1、例子1:用eval技巧實現shell的控制結構for
用eval技巧實現shell的控制結構for。
[root@home root]# cat myscript1#!/bin/shevalit(){ if [ $cnt = 1 ];then eval $@ return else let cnt=cnt-1 evalit $@ fi eval $@}cnt=$1echo $cnt egrep "^[1-9][0-9]*$" >/dev/nullif [ $? -eq 0 ]; then shift evalit $@else echo 'ERROR!!! Check your input!'fi[root@home root]# ./myscript1 3 hostnamehomehomehome[root@home root]# ./myscript1 5 id cut -f1 -d' 'uid=0(root)uid=0(root)uid=0(root)uid=0(root)uid=0(root)注意:bash裡有兩個很特殊的變量,它們保存了參數列表。
$*,保存了以$IFS指定的分割符所分割的字符串組。$@,原樣保存了參數列表,也就是"$1""$2"...
這裡我使用了函數遞歸以及eval實現了for結構。當執行eval $@時,它經歷了步驟如下:第1步,分割成eval $@第6步,擴展$@為hostname第11步,找到內置命令eval重復一次命令行處理,第11步,找到hostname命令,執行。
注意:也許有人想當然地認為,何必用eval呢?直接$@來執行命令就可以了嘛。
例子2:一個典型錯誤的例子
錯誤!這裡給個典型的例子大家看看。
[root@home root]# a="id cut -f1 -d' '"[root@home root]# $aid:無效選項 -- f請嘗試執行‘id --help’來獲取更多信息。[root@home root]# eval $auid=0(root)如果命令行復雜的話(包括管道或者其他字符),直接執行$a字符串的內容就會出錯。分析如下。$a的處理位於第6步──參數擴展,也就是說,跳過了管道分析,於是"", "cut", "-f1", "-d"都變成了id命令的參數,當然就出錯啦。但使用了eval,它把第一遍命令行處理所得的"id", "", "cut", "-f1", "-d"這些字符串再次進行命令行處理,這次就能正確分析其中的管道了。
總而言之:要保證你的命令或腳本設計能正確通過命令行處理,跳過任意一步,都可能造成意料外的錯誤!
例子3:設置系統的ls色彩顯示
eval $(dircolors -b /etc/dircolors)eval語句通知shell接受eval參數,並再次通過命令行處理的所有步驟運行它們。它使你可以編寫腳本隨意創建命令字符串,然後把它們傳遞給shell執行;$()是命令替換,返回命令的輸出字符串。其中dircolors命令根據/etc/dircolors配置文件生成設置環境變量LS_COLORS的bash代碼,內容如下 [root@localhost root]# dircolors -b > tmp[root@localhost root]# cat tmpLS_COLORS='no=00:fi=00:di=01;34:ln=01; ......eXPort LS_COLORS#這裡我沒有指定配置文件,所以dircolors按預置數據庫生成代碼。其輸出被eval命令傳遞給shell執行。eval是對Bash Shell命令行處理規則的靈活應用,進而構造"智能"命令實現復雜的功能。上面提及的命令是eval其中一個很普通的應用,它重復了1次命令行參數傳遞過程,純粹地執行命令的命令。其實它是bash的難點,是高級bash程序員的必修之技。
四、命令優先級表1、別名2、關鍵字3、函數4、內置命令5、腳本或可執行程序($PATH)五、鑒於一些學習中會遇到的困惑,我再給出一些有趣的命令。
1、command builtin enable
上面的命令行提及過,第11步會進行命令查找,那它的具體過程如何呢?它的默認查找次序為函數,內部命令,腳本和可執行代碼。我們往往要在實際編程中跳過一些查找項以滿足一定的功能需求。這時候就要用到這三個命令來施展魔法~~
2、command
跳過別名和函數的查找,換句話說,它只查找內部命令以及搜索路徑中找到的腳本或可執行程序。這裡舉個有趣的例子。
[root@home root]# type -all pwdpwd is a shell builtinpwd is /bin/pwd[root@home root]# cat myscript2#!/bin/shpwd(){ echo "This is the current Directory." command pwd}pwd[root@home root]# ./myscript2This is the current directory./root
我用pwd()函數取代了內置命令pwd以及外部命令/bin/pwd,然後在腳本裡執行內置命令pwd。在這裡我們為什麼要用command呢?是為了避免函數陷入遞歸循環,因為函數名與內置命令同名,而函數的優先級比內置命令高。
3、builtin
顧名思義,它只查找內置命令。這個命令很簡單,就不多說了。
4、enable
與builtin相反,它屏蔽一個內置命令,允許運行一個shell腳本或同名的可執行代碼而無須給出完全路徑名。舉個例子吧。
pwd命令有兩個,一個是shell內置的,一個是可執行程序。
當執行一些奇怪的路徑名後,shell內置的pwd會打印出"錯誤信息",但外部的pwd會打印出當前目錄的"原來面目"。請看下面:
[root@home root]# cd //[root@home //]# pwd//[root@home //]# type -all pwdpwd is a shell builtinpwd is /bin/pwd[root@home //]# /bin/pwd/[root@home //]# enable -n pwd[root@home //]# pwd/
這樣,用enable -n屏蔽內置pwd命令後,就可以用外部pwd打印出正確的路徑名了。
Bash博大精深,希望大家好好學習。:)