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