bash命令的執行分爲四大步驟:輸入
、解析
、擴展
和執行
。
本文將詳述bash命令的通常處理過程:
如圖所示shell
在交互模式下,輸入來自終端。bash使用GNU Readline庫處理用戶命令輸入,Readline提供相似於vi或emacs的行編輯功能(如Ctrl+a
、Ctrl+e
等等)。
當敲擊鍵盤時,字符會存入Readline的編輯緩衝區,Readline會處理輸入的變化並及時地將結果顯示到終端上。
Readline還要保持命令提示符(prompt
)的穩定(好比提示符的顏色)。
在將編輯緩衝區的內容交給bash以前,Readline會執行歷史擴展
(見這裏),以後由bash負責將本條命令存儲到歷史列表並進入下一步驟。segmentfault
在非交互模式下,輸入通常來自文件。此時,bash使用C語言標準庫的stdio來得到輸入。
不像Readline那樣須要實現各類功能,stdio的工做較爲簡單:緩衝文件內容並逐行提供輸入給bash處理。centos
解析階段的主要工做爲:詞法分析
和語法解析
詞法分析
指分析器從Readline或其餘輸入獲取字符行,根據元字符
將它們分割成word
,並根據上下文環境標記這些word
(肯定單詞的類型)。元字符
包括:數組
| & ; ( ) < > space tab
語法解析
指解析器和分析器合做,根據各個單詞的類型以及它們的位置,判斷命令是否合法以及肯定命令類型。
單詞(word
)有不少種,bash從左到右依次分析它們的類型。下面對一些狀況作一下簡介:
一、重定向
分析器分析每一個單詞,若是單詞表示一個重定向,則保持至執行階段再處理。
二、賦值語句
對於非重定向的首個單詞進行分析,若是該單詞是一個賦值語句,則保持至擴展階段處理。
而後繼續分析下一個單詞,對於連續的賦值語句
或重定向
都作如上處理。
三、關鍵字
對於非重定向或賦值語句的第一個單詞進行斷定,若是是保留關鍵字
,則根據語法定義斷定該種命令類型的語法和結尾(結尾通常爲某種控制操做符)。
四、別名
若是非重定向或賦值語句的第一個單詞是一個普通單詞,bash會根據別名記錄斷定該單詞是否是一個命令別名,若是是,則使用對應的文本替換該別名(注意此文本能夠是shell可以接受的任意字符)。
而後繼續分割並斷定替換後的文本,重複上述一樣過程,若是替換後仍有別名
(不一樣於前面曾擴展過的別名),則遞歸地展開並斷定。
另外,默認時只有在交互式shell環境下才容許別名擴展。若是須要在腳本中使用命令別名,則需開啓選項shopt -s expand_aliases
。因爲別名的功能均可以用函數實現,建議在腳本中使用函數來代替命令別名。
五、其餘
若是非重定向或賦值語句的第一個單詞不是別名或複合命令的起始單詞,解析器將標記它爲命令名,並賦值給位置變量0
,其他單詞(控制操做符以前的)爲此命令的參數($一、$2...$n)。緩存
而後分析器繼續分析下一條命令(控制操做符以後的),直到整行都分析完畢。bash
注意,在同一命令內,賦值語句
後面必須是一個簡單命令
。若是是複合命令,將會報錯。函數
還要注意,引用
(見這裏)會使元字符
失去其特殊意義,其內部的多個單詞可能會被bash看作是一個word
。centos7
最終解析器返回一個C結構體來表達一個命令(對於複合命令,這個結構體中可能還包含有其餘命令),而後將其傳遞給shell的下一階段:單詞展開。spa
擴展階段對應於單詞的各類變換,最終獲得可用於執行的命令。
以以下腳本爲例解釋此階段依次進行的擴展(各類擴展的方法請看以前的文章):操作系統
#!/bin/bash TMP='temp/tmp' num=2 cat ~/"${TMP:0:$((num+2))}"/test_{[0-9],[a-z]}.txt
腳本第三行是一條簡單命令(只爲舉例說明)。
首先進行的是大括號擴展
,此擴展會致使單詞數量的變化。
擴展後的命令形如:
cat ~/"${TMP:0:$((num+2))}"/test_[0-9].txt ~/"${TMP:0:$((num+2))}"/test_[a-z].txt
而後進行的是波浪號擴展,~
被$HOME
的值所代替。
擴展後的命令形如:
cat /root/"${TMP:0:$((num+2))}"/test_[0-9].txt /root/"${TMP:0:$((num+2))}"/test_[a-z].txt
在波浪號擴展後進行變量擴展
、命令替換
、進程替換
和數學擴展
,它們按其出現的位置依次擴展。對於嵌套的狀況,先進行內部擴展。
擴展後的命令形如:
cat /root/"temp"/test_[0-9].txt /root/"temp"/test_[a-z].txt
單詞分割
只做用於前一種擴展(變量、命令、進程、數學擴展)的結果,若是擴展處於雙引號中,則不會分割(變量或數組使用@
的狀況例外)。
bash利用環境變量IFS
的值進行單詞分割,若是擴展的結果單詞中包含IFS中的任意字符,則被分割爲多個單詞。若是擴展的結果爲空,則此單詞被移除(引號中的空值會被保留)。
咱們的例子中擴展的結果單詞temp
不包含IFS中字符,因此沒有進行單詞分割
。
注意若是沒有上述擴展發生,也不會進行本階段的單詞分割。
單詞分割結束後,bash掃描每一個單詞中的字符*
、?
和[
,若是包含這些字符,此單詞就做爲一個模式對文件名進行通配符匹配
。
匹配到的全部結果將成爲命令的新單詞。
咱們的例子中,路徑擴展後的命令形如:
cat /root/"temp"/test_1.txt /root/"temp"/test_4.txt /root/"temp"/test_x.txt
路徑擴展完畢後,將移除全部的非擴展結果的引用字符(包括'' "" \
)。
咱們的例子中,做用於單詞temp的雙引號,並非擴展後的結果,因此會被移除:
cat /root/temp/test_1.txt /root/temp/test_4.txt /root/temp/test_x.txt
腳本執行:
[root@centos7 temp]# ./test.sh 我是文件 test_1.txt 我是文件 test_4.txt 我是文件 test_x.txt [root@centos7 temp]#
拋開咱們的例子,若是一條簡單命令有前置的賦值語句,等號右邊的單詞會通過:波浪號括展
、變量|命令|進程|數學擴展
和移除引用
。大括號擴展、單詞分割和路徑擴展不會發生。
不一樣類型的命令,bash的執行方式有所差別。
bash中每種複合命令
都使用一個C函數來實現,功能包括執行恰當的展開(如for循環中關鍵詞in後面的單詞),執行特定的命令,根據命令的返回值來變動執行流程等等。
對於管道命令
,管道兩側的命令會在不一樣的兩個子進程中執行。
此時命令要前後經歷
一、fork()系統調用
建立子進程。
二、鏈接管道
而後命令的執行步驟以下述簡單命令
的執行。
不管是什麼類型的命令,最終都將歸結到簡單命令的執行。
一條簡單命令的執行過程以下:
命令搜索
一、若是命令名中包含字符/
(目錄分隔符),則直接執行該路徑指定的文件。
二、若是命令名中無斜線,則搜索當前環境中定義的函數
,若是找到,則執行該函數。
三、若是未找到函數,則搜索內置命令
,若是找到,則執行該內置命令(注意內置命令eval
會使其後的全部單詞再次通過解析、擴展和執行)。
四、若是沒有對應的內置命令,則搜索hash
緩存中記錄的對象,若是有該命令的緩存,則直接執行該絕對路徑對應的文件。
五、若是hash表中無緩存記錄,則搜索環境變量PATH
值中全部目錄內的文件,若是找到該名稱的文件,則執行(並緩存至hash表);若是未找到,則返回錯誤信息,設置返回值爲127並exit。
命令執行
對於命令的執行,咱們介紹更通常的狀況(命令位於磁盤文件系統之上的狀況):
一、bash執行fork()系統調用
建立子進程(若是命令已經處於子shell內,則不會再次fork(),例如上述管道命令)
二、執行重定向
三、執行execve()系統調用
,控制權移交給操做系統。
四、內核判斷該文件是不是操做系統可以處理的可執行格式(如ELF格式的可執行二進制文件或開頭頂格寫#!
的可執行文本文件)
五、若是操做系統可以處理該文件,則調用相應的函數(二進制文件)或解釋器(腳本文件)進行執行。
六、若是文件不具有操做系統的可執行格式(如文本文件但沒有頂格寫的#!
),execve()
失敗,此時,bash會判斷該文件,若是該文件有可執行權限而且不是一個目錄,則認爲該文件是一個腳本,因而調用默認解釋器解釋執行該文件的內容。
七、執行完畢後,bash收集命令的返回值。
這些,就是bash執行命令的整個流程。