本文說明的是一條linux命令在執行時大致要經過哪些過程?以及這些過程的大致順序。 1.1 shell解析命令行 shell讀取和執行命令時的大致操作過程如下圖: 以執行以下命令為例: echo -e "some files:" ~/i* "\nThe date:$(date +%F)\n$name ...
本文說明的是一條linux命令在執行時大致要經過哪些過程?以及這些過程的大致順序。
1.1 shell解析命令行
shell讀取和執行命令時的大致操作過程如下圖:
以執行以下命令為例:
echo -e "some files:" ~/i* "\nThe date:$(date +%F)\n$name's age is $((a+4))" >/tmp/a.log
假設在執行該命令前,已賦值變數"name=longshuai"和"a=24",於是重定向到/tmp/a.log中的結果為:
some files: /root/inotify.sh /root/inotify.sh.ori The date:2017-08-14 longshuai's age is 28
(1).讀取輸入的命令行。
(2).解析引用並分割命令行為各個單詞,各單詞稱為token。其中重定向所在的token會被保存下來,直到擴展步驟(5)結束後才進行相關處理,如進行擴展、截斷文件等。
shell中有3種引用方式:反斜線引用、單引號引用和雙引號引用。
◇ 反斜線轉義:使得元字元變為普通的字面字元。但這隻能對反斜線後一個字元進行轉義。
◇ 單引號引用:單引號內的所有字元全部變為字面符號符號。但註意:單引號內不能再使用單引號,即使使用了反斜線轉義也不允許。
◇ 雙引號引用:使雙引號內所有字元變為字面符號,但"\"、"$"、"`"(反引號)除外,如果開啟了"!"引用歷史命令時,則感嘆號也除外。
解析引用後,於是就可以將命令行進行單詞分割,分割後的每一部分都稱為一個token。分隔時,不僅分割單個命令,還分割命令列表,所以分隔符包括:空格、tab、分號、管道符號、&、&&、||、重定向符號、圓括弧等。
於是上述命令分割為以下幾個token:
如果分割時發現了管道符號,或者是命令列表等組合了多個命令的情況,則每個命令都的token都相互獨立。
(3).檢查命令行結構。主要檢查是否有命令列表、是否有shell編程結構的命令,如if判斷命令、迴圈結構的for/while/select/until,這些命令屬於保留關鍵字,需要特殊處理。
(4).對第一個token進行別名擴展。如果檢查出它是別名,則擴展後回到(2)再次進行token分解過程。
(5).進行各種擴展。擴展順序為:大括弧擴展;波浪號擴展;參數、變數和命令替換、算術擴展(如果系統支持,此步還進行進程替換);單詞拆分;文件名擴展。
不同引號的引用方式,將改變擴展的起始步驟,正如上圖所畫,沒有任何引號時將從頭到尾全部擴展,使用單引號時將完全不會進行任何擴展,使用雙引號時將從變數替換開始繼續擴展。
①大括弧擴展:如/tmp/{a,b}.log擴展為/tmp/a.log和/tmp/b.log。
②波浪號擴展:擴展為家目錄。如root用戶下的~/.ssh擴展為/root/.ssh。
③變數擴展:即操作和替換變數值。如$a替換為它的值24,${name:-longshuai}替換為longshuai。
④命令替換:此過程將執行命令替換中的命令,並將結果替換到token的對應位置處。
⑤進程替換:將進程的執行結果替換到對應位置。類似於命令替換。替換格式為"<(cmd_list)"和">(cmd_list)",例如"cat <(cat /etc/hosts)"。redhat系列應該都支持進程替換。
⑥算術擴展:計算算術值,並將計算結果替換到對應位置處。例如$((a+4))替換為28。
經過以上幾種擴展後,得到如下結果:
⑦單詞拆分:掃描變數擴展、命令替換和算術擴展的結果,對非引號內的結果按照$IFS的值對這些結果進行單詞分割。
註意,如果沒有進行擴展,或者擴展結果使用引號包圍了,則不會進行此步的單詞拆分。
預設情況下,$IFS值為" \t\n",所以擴展結果中每遇到空格、製表符、換行符都將被分割為兩個單詞。
這一步其實很容易犯錯,典型的是test命令。例如變數name="Ma longshuai",則test $name == "longshuai"將報錯,因為變數擴展後該語句變為test Ma longshuai == "longshuai",由於是變數替換,所以隨後進行單詞拆分,使得Ma和longshuai被拆分為兩個單詞,但實際上它們共同組成變數name的值。
所以,為了正確操作變數替換和命令替換,儘量將它們使用引號包圍。例如test "$name" == "longshuai",這時將不會進行單詞拆分。
⑧文件名擴展:對每個token進行搜索,將搜索"*"、"?"和"["符號,搜索到了將進行文件名擴展。例如將上面的"/root/i*"擴展為"/root/inotify.sh /root/inotify.sh.ori"。
(6).引號去除。經過上面的過程,該擴展的都擴展了,不需要的引號在此步就可以去掉了。
所以得到如下結果。
(7).搜索和執行命令。
單詞分割後,複雜的命令行將由各個簡單命令結構組成。於是可以搜索每個簡單命令結構的第一個token中的命令,同時還帶有一系列命令選項。例如上面的"echo"和"-e"。
如果命令中不含任何斜杠:
①則先判斷是否有此名稱的shell function存在,如果有則調用它,否則進行下一步搜索。
②判斷該命令是否為bash內置命令,如果是則執行它,否則進行下一步搜索。
③從$PATH的路徑下搜索該命令,如果搜索到了,則執行,否則報錯。
如果命令中包含一個或多個斜杠,則進行相對路徑擴展、絕對路徑查找,找到了則執行,否則報錯。
(8).返回退出狀態碼。
1.2 eval命令
正常情況下,當搜索到命令時將會執行命令,但如果搜索到的命令為eval時,則處理方式有所不同。
它的語法格式為:
eval command arguments
按照前文所述shell解析過程,將最終得到eval command和一系列擴展後的選項、參數,當搜索命令時,搜索到的結果為eval命令,於是eval命令將除了eval命令(以及eval的選項)的所有token再次傳遞給shell進行二次解析。但重定向所在token除外,因為重定向token早已被shell保存下來,所以不會再次截斷文件。
也就是說,"command arguments"被當作eval命令的參數,被傳遞給shell進行解析、執行。
執行過程如下圖所示:
使用示例來說明:
[root@xuexi ~]# a=24;name='long$a' # 註意,使用的是單引號,禁止$a被擴展
如果直接執行echo $name,則結果為"long$a",但如果執行eval echo $name,結果將是"long24"。
[root@xuexi ~]# eval echo $name long24
首先shell按照正常過程解析,在變數替換時由於使用了單引號,所以$name第一次變數替換的結果為"long$a",直到命令搜索時發現搜索到的命令是eval命令,執行eval命令,該命令將其參數"echo long$a"再次傳遞給shell,相當於在標準輸入中輸入了"echo long$a",於是shell進行二次解析,這次的變數替換將$a替換為24,最後搜索命令發現是echo命令,於是最終得到"long24"。
關於eval,更多的用法是間接變數$$var的用法,在bash shell中需要在第一個$前加上反引號,即\$$var,這麼做的原因是顯然的:防止第一次shell解析時被當作特殊變數"$$"被擴展。
[root@xuexi ~]# a=b [root@xuexi ~]# b=haha [root@xuexi ~]# eval echo \$$a haha
註:本文並非一定准確,只是根據man bash總結而來。如有錯誤,請明確指出。多謝
回到系列文章大綱:http://www.cnblogs.com/f-ck-need-u/p/7048359.html