天天看點

Linux應用環境實戰10:Bash腳本程式設計語言中的美學與哲學(轉)

閱讀目錄

  • ​​一、一切皆是字元串​​
  • ​​二、引用和元字元​​
  • ​​三、字元串從哪裡來、到哪裡去​​
  • ​​四、再加上一點點的定義,就可以推導出整個Bash腳本語言的文法了​​
  • ​​五、輸入輸出重定向​​
  • ​​六、Bash腳本語言的美學:大道至簡​​
  • ​​總結:​​

  我承認,我再一次地當了标題黨。但是不可否認,這一定是一篇精華随筆。在這一篇中,我将探讨Bash腳本語言中的美學與哲學。 這不是一篇Bash腳本程式設計的教程,但是卻能讓人更加深入地了解Bash腳本程式設計,更加快速地學習Bash腳本程式設計。 閱讀這篇随筆,不需要你有Bash程式設計的經驗,但一定要和我一樣熱衷于探索各種程式設計語言的本質,感悟它們的魅力。

  其實早就想寫關于Bash的東西了。 我們平時喜歡對程式設計語言進行分類,比如面向過程的程式設計語言、面向對象的程式設計語言、函數式程式設計語言等等。在我心中,我認為Bash就是一個面向字元串的程式設計語言。Bash腳本語言的本質:一切皆是字元串。 Bash腳本語言的一切哲學都圍繞着字元串:它們從哪裡來?到哪裡去?使命是什麼? Bash腳本語言的一切美學都源自字元串: 由鍵盤上幾乎所有的符号 “$ ~ ! # & ( ) [ ] { } | > < - . , ; * @ ' " ` \ ^” 排列組合而成的極富視覺沖擊力的、功能極其複雜的字元串。

​​回到頂部​​

一、一切皆是字元串

  Bash是一個Shell,Shell出現的初衷是為了将系統中的各種工具粘合在一起,是以它最根本的功能是調用各種指令。 但是Bash又提供了豐富的程式設計功能。 我們經常對程式設計語言進行分類,比如面向過程的語言、面向對象的語言、面向函數的語言等等。 可以把Bash腳本語言看成是一個面向字元串的語言。 Bash語言的本質就是:一切都是字元串。 看看下圖中的這些變量:

Linux應用環境實戰10:Bash腳本程式設計語言中的美學與哲學(轉)

  上圖是我在互動式的Bash指令行中做的一些示範。在上圖中,我對變量分别指派,不管等号右邊是一個沒有引号的字元串,還是帶有引号的字元串,甚至數字,或者數學表達式,最終的結果,變量裡面存儲的都是字元串。我使用一個for循環顯示所有的變量,可以看到數學表達式也隻是以字元串的形式儲存,沒有被求值。

​​回到頂部​​

二、引用和元字元

  如果一切都是沒有特殊功能的平凡的字元串,那就無法構成一門程式設計語言。在Bash中,有很多符号具有特殊含義,比如“$”符号被用于字元串展開,“&”符号用于讓指令在背景執行, “|”用作管道, “>” “<”用于輸入輸出重定向等等。是以在Bash中,雖然同樣是字元串,但是被引号包圍的字元串和不被引号包圍的字元串使用起來是不一樣的,被單引号包圍的字元串和被雙引号包圍起來的字元串也是不一樣的。

  究竟帶引号的字元串和不帶引号的字元串使用起來有什麼不一樣呢?下圖是我建構的一些比較典型的例子:

Linux應用環境實戰10:Bash腳本程式設計語言中的美學與哲學(轉)

  在上圖中,我展示了Bash中生成字元串的7種方法:大括号展開、波浪符展開、參數展開、指令替換、算術展開、單詞分割和檔案路徑展開。還有兩種生成字元串的方式沒有講(Process substitution和曆史指令展開)。在使用Bash腳本程式設計的時候,了解以上7種字元串生成的方式就夠了。在互動式使用Bash指令行的時候,還需要了解曆史指令展開,熟練使用曆史指令展開可以讓人事半功倍。

  在上面的圖檔中可以看到,有一些展開方式在被雙引号包圍的字元串中是不起作用的,比如大括号展開、波浪符展開、單詞分割、檔案路徑展開,而隻有參數展開、指令替換和算術展開是起作用的。從圖檔中還可以看出,字元串中的參數展開、指令替換和算術展開都是由“$”符号引導,指令替換還可以由“`”引導。是以,可以進一步總結為,在雙引号包圍的字元串中,隻有“$、`、\”這三個字元具有特殊含義。

awk -e '{print $1,$5}' ,其中,傳遞給awk的指令用單引号包圍,說明bash不執行其中的任何替換或展開。

  另外一個特殊的字元是“\”,它也是引用的一種。它可以解除緊跟在它後面的一個特殊字元的特殊含義(引用)。之是以需要“\”的存在,是因為在Bash中,有些字元稱為元字元,這些字元一旦出現,就會将一個字元串分割為多個子串。如果需要在一個字元串中包含這些元字元本身,就必須對它們進行引用。如下圖:

Linux應用環境實戰10:Bash腳本程式設計語言中的美學與哲學(轉)

  最常見的元字元就是空格。 從上面幾張圖檔可以看出,如果要将一個含有空格的字元串指派給一個變量,要麼把這個字元串用雙引号包圍,要麼使用“\”對空格進行引用。 從上圖中可以看出,Bash中隻有9個元字元,它們分别是“| & ( ) ; < > space tab”,而在其它程式設計語言中經常出現的元字元“. { } [ ]”以及作為數學運算的加減乘除,在Bash中都不是元字元。

​​回到頂部​​

三、字元串從哪裡來、到哪裡去

  介紹完字元串、介紹完引用和元字元,下一個目标就是來探讨這一個哲學問題:字元串從哪裡來、到哪裡去?通過該哲學問題的探讨,可以推導出Bash腳本語言的整個文法。字元串從哪裡來?很顯然,其中一個很直接的來源就是我們從鍵盤上敲上去的。除此之外,就是我前面提到的七八九種字元串展開的方法了。

  字元串展開的流程如下:

    1.先用元字元将一個字元串分割為多個子串;

    2.如果字元串是用來給變量指派,則不管它是否被雙引号包圍,都認為它被雙引号包圍;

    3.如果字元串不被單引号和雙引号包圍,則進行大括号展開,即将{a,b}c展開為ab ac;

以上三個流程可以通過下圖證明:

Linux應用環境實戰10:Bash腳本程式設計語言中的美學與哲學(轉)

    4.如果字元串不被單引号或雙引号包圍,則進行波浪符展開,即将~/展開為使用者的主目錄,将~+/展開為目前工作目錄(PWD),将~-/展開為上一個工作目錄(OLDPWD);

    5.如果字元串不被單引号包圍,則進行參數和變量展開;這一類的展開全都以“$”開頭,這是整個Bash字元串展開中最複雜的,其中包括使用者定義的變量,包括所有的環境變量,以上兩種展開方式都是“$”後跟變量名,還包括位置變量“$1、 $2、 ...、 $9、 ... ”,其它特殊變量:“$@、 $*、 $#、 $-、 $!、 $0、 $?、 $_ ”,甚至還有數組:“${var[i]}”, 還可以在展開的過程中對字元串進行各種複雜的操作,如:“ ${parameter:-word}、 ${parameter:=word}、 ${parameter:+word}、 ;${parameter:?word}、 ${parameter:offset}、 ${parameter:offset:length}、 ${!prefix*}、 ${!prefix@}、 ${name[@]}、 ${!name[*]}、 ${#parameter}、 ${parameter#word}、 ${parameter##word}、 ${parameter%word}、 ${parameter%%word}、 ${parameter/pattern/string}、 ${parameter^pattern}、 ${parameter^^pattern}、 ${parameter,pattern}、 ${parameter,,pattern}”;

    6.如果字元串不被單引号包圍,則進行指令替換;指令替換有兩種格式,一種是$(...),一種是`...`;也就是将指令的輸出作為字元串的内容;

    7.如果字元串不被單引号包圍,則進行算術展開;算術展開的格式為$((...));

    8.如果字元串不被單引号或雙引号包圍,則進行單詞分割;

    9.如果字元串不被單引号或雙引号包圍,則進行檔案路徑展開;

    10.以上流程全部完成後,最後去掉字元串外面的引号(如果有的話)。以上流程隻按以上順序進行一遍。比如不會在變量展開後再進行大括号展開,更不會在第10步去除引用後執行前面的任何一步。如果需要将流程再走一遍,請使用eval。

  探讨完了字元串從哪裡來,下面來看看字元串到哪裡去。也就是怎麼使用這些字元串。使用字元串有以下幾種方式:

    1.把它當指令執行;這是Bash中的最根本的用法,畢竟Shell的存在就是為了粘合各種指令。如果一個字元串出現在本該指令出現的地方(比如一行的開頭,或者關鍵字then、do等的後面),它将會被當成指令執行,如果它不是個合法的指令,就會報錯;

    2.把它當成表達式;Bash中本沒有表達式,但是有了((...))和[[...]],就有了表達式;((...))可以把它裡面的字元串當成算術表達式,而[[...]]會把它裡面的字元串當邏輯表達式,僅此兩個特例;

    3.給變量指派;這也是一個特例,有點破壞Bash程式設計語言文法哲學的完整性。為什麼這麼說呢?因為“=”即不是一個元字元,也不允許兩邊有空格,而且隻有第1個等号會被當成指派運算符。

  下面圖檔為以上觀點給出證據:

Linux應用環境實戰10:Bash腳本程式設計語言中的美學與哲學(轉)

​​回到頂部​​

四、再加上一點點的定義,就可以推導出整個Bash腳本語言的文法了

  前面我已經展示了我對字元串從哪裡來、到哪裡去這個問題的了解。關于字元串的去向,除了兩個表達式和一個為變量指派這三個特例,剩下的就隻有當指令來執行了。在前面,我提到了元字元和引用的概念,這裡,還得再增加一點點定義:

cat /etc/passwd 就是一個指令,第一個單詞cat是指令,第2個單詞/etc/passwd是指令的參數,而字元串 cat /etc/passwd | grep youxia 就是兩個指令,這兩個指令分别是cat和grep,它們之間通過“|”分割,是以這裡的“|”是控制操作符。熟悉Shell的朋友肯定知道“|”代表的是管道,是以它的作用是1.把一個字元串分割為兩個指令,2.将第一個指令的輸出作為第二個指令的輸入。在Bash中,總共隻有10個控制操作符,它們分别是“|| & && | ; ;; ( ) |& <newline>”。隻要看到這些控制操作符,就可以認為它前面的字元串是一個完整的指令。

    定義2:關鍵字(Reserved Words) 我沒有将其翻譯成保留字,很顯然,作為程式設計語言來說,它們應該叫做關鍵字。一門程式設計語言肯定必須得提供選擇、循環等流程控制語句,還得提供定義函數的功能。這些功能隻能通過關鍵字實作。在Bash中,隻有22個關鍵字,它們是“! case coproc do done elif else esac fi for function if in select then until while { } time [[ ]]”。這其中有不少的特别之處,比如“! { } [[ ]]”等符号都是關鍵字,也就是說它們當關鍵字使用時相當于一個單詞,也就是說它們和别的單詞必須以元字元分開(否則無法成為獨立的單詞)。這也是為什麼在Bash中使用“! { } [[ ]]”時經常要在它們周圍留白格的原因。(再一次證明“=”是一個很變态的特例,因為它既不是元字元,也不是控制操作符,更加不是關鍵字,它到底是什麼?)

  下面開始推導Bash腳本語言的文法:

uname -r 指令(單獨放在一行的指令其實是以<newline>結尾,<newline>是控制操作符),或者雖然不單獨放在一行,但是以“;”或“&”結尾,比如 uname -r; who; pwd; gvim& 其中每一個指令都是一個簡單指令(當然,這四個指令放在一起的這行代碼不叫簡單指令),“;”就是簡單地分割指令,而“&”還有讓指令在背景執行的功能。這裡比較特殊的是雙分号“;;”,它隻用在case語句中。

[time [-p]] [ ! ] command1 | command2 或這樣 [time [-p]] [ ! ] command1 |& command2 的。其中time關鍵字和!關鍵字都是可選的(使用[...]指出哪些部分是可選的),time關鍵字可以計算指令運作的時間,而!關鍵字是将指令的傳回狀态取反。看清楚!關鍵字周圍的空格哦。如果使用“|”,就是把第一個指令的标準輸出作為第二個指令的标準輸入,如果使用“|&”,則将第一個指令的标準輸出和标準錯誤輸出都當成第二個指令的輸入。

command1 || command2 隻有當command1執行不成功的時候才執行command2,而 command1 && command2 隻有當command1執行成功的時候才執行command2。

(list) 、  { list; } 、  ((expression)) 、  [[ expression ]] 。請注意第2種形式和第4種形式大括号和中括号周圍的空格,也請注意第2種形式中list後面的“;”,不過如果“}”另起一行,則不需要“;”,因為<newline>和“;”是起同樣作用的。在以上4種複合指令中, (list) 是在一個新的Shell中執行指令序列,這些指令的執行不會影響目前Shell的環境變量,而 { list; } 隻是簡單地将指令序列分組。後面兩種表達式求值前面已經講過,這裡就不講了。後面可能會詳細列出邏輯表達式求值的選項。

  上面的4步推導是一步更進一步的,是由簡單逐漸到複雜的,最簡單的指令可以組合成稍複雜的管道,再組合成更複雜的指令序列,最後組成最複雜的複合指令。

  下面是Bash腳本語言的流程控制語句,如下:

for name [ [ in [ word ... ] ] ; ] do list ; done ;

for (( expr1 ; expr2 ; expr3 )) ; do list ; done ;

select name [ in word ] ; do list ; done ;

case word in [ [(] pattern [ | pattern ] ... ) list ;; ] ... esac ;

if list; then list; [ elif list; then list; ] ... [ else list; ] fi ;

while list-1; do list-2; done ;

until list-1; do list-2; done 。

  上面的公式大家看得懂吧,我相信大家肯定看得懂。其中的[...]代表的是可以有也可以真沒有的部分。在以上公式中,請注意第2個公式for循環中的雙括号,它執行的是其中的表達式的算術運算,這是和其它進階語言的for循環最像的,但是很遺憾,Bash中的算術表達式目前隻能計算整數。再請注意第3個公式,select文法,和for...in...循環的文法比較類似,但是它可以在螢幕上顯示一個菜單。如果我沒有記錯的話,Basic語言中應該有這個功能。其它的控制結構在别的進階語言中都很常見,就不需要我在這裡啰嗦了。

  最後,再來展示一下如何定義函數:

name () compound-command [redirection]

    或者

function name [()] compound-command [redirection]

  可以看出,如果有function關鍵字,則“()”是可選的,如果沒有function關鍵字,則“()”是必須的。這裡需要特别指出的是:函數體隻要求是compound-command,我前面總結過compound-command有四種形式,是以有時候定義一個函數并不會出現“{ }”哦。如下圖,這樣的函數也是合法的:

Linux應用環境實戰10:Bash腳本程式設計語言中的美學與哲學(轉)

  That's all。這就是Bash腳本語言的全部文法。就這麼簡單。

  好像忘了點什麼?對了,還有輸入輸出重定向沒有講。輸入輸出重定向是Shell中又一個偉大的發明,它的存在有着它獨特的哲學意義。這個請看下一節。

​​回到頂部​​

五、輸入輸出重定向

  Unix世界有一個偉大的哲學:一切皆是檔案。(這個扯得有點遠。)Unix世界還有一個偉大的哲學:建立程序比較友善。(這個扯得也有點遠。)而且,每一個程序一建立,就會自動打開三個檔案,它們分别是标準輸入、标準輸出、标準錯誤輸出,普通情況下,它們連接配接到使用者的控制台。在Shell中,使用數字來辨別一個打開的檔案,稱為檔案描述符,而且數字0、 1、 2分别代表标準輸入、标準輸出和标準錯誤輸出。在Shell中,可以通過“>”、“<”将指令的輸入、輸出進行重定向。結合exec指令,可以非常友善地打開和關閉檔案。需要注意的是,當檔案描述符出現在“>”、“<”右邊的時候,前面要使用“&”符号,這可能是為了和數學表達式中的大于和小于進行差別吧。使用“&-”可以關閉檔案描述符。

  “> < & 數字 exec -”,這就是輸入輸出重定向的全部。下面的公式中,我使用n代表數字,如果是兩個不同的數字,則使用n1、n2,使用[...]代表可選參數。輸入輸出重定向的文法如下:

1 [n]> file2 [n]>> file3 [n]< file4 [n1]>&n2         #重定向标準輸出(或 n1)到n2。
5 2> file >&26 |7 2>&1 | command   #将标準輸出和錯誤輸出一起通過管道傳遞給command,等同于|&。      

  請注意,數字和“>”、“<”符号之間是沒有空格的。結合exec,可以非常友善地使用一個檔案描述符來打開、關閉檔案,如下:

echo Hello >file1
3<file1 4>file2  #打開檔案
cat <&3 >&4           #重定向标準輸入到 3,标準輸出到 4,相當于讀取file1的内容然後寫入file2
3<&- 4>&-cat#顯示結果為 Hello
#還可以暫存和恢複檔案描述符,如下:
5>&210 exec 2> /tmp/$0.log  #重定向标準錯誤輸出
11 ...
12 exec 2>&513 exec 5>&-            #關閉檔案描述符5,因為不需要了      

  還可以将“<>”一起使用,表示打開一個檔案進行讀寫。

  除了exec,輸入輸出重定向和read指令配合也很好用,read指令每次讀取檔案的一行。但是要注意的是,輸入輸出重定向放到for、while等循環的循環體和循環外,效果是不一樣的。如下圖:

Linux應用環境實戰10:Bash腳本程式設計語言中的美學與哲學(轉)
Linux應用環境實戰10:Bash腳本程式設計語言中的美學與哲學(轉)

  另外,輸入輸出重定向符号“>”、“<”還可以和“()”一起使用,表示程序替換(Process substitution),如“>(list)”、“<(list)”。結合前面提到的“<”、“>”、“(list)”的含義,程序替換的作用是很容易猜到的哦。

​​回到頂部​​

六、Bash腳本語言的美學:大道至簡

  如果你問我Bash腳本語言哪裡美?我會回答:簡潔就是美。請看下面逐條論述:

  1.使用了簡潔的抽象的符号。Bash腳本語言幾乎使用到了鍵盤上能夠找到的所有符号,比如“$”用作字元串展開,“|”用作管道,“<”、“>”用作輸入輸出重定向,一點都不浪費;

  2.隻使用了9個元字元、10個控制操作符和22個關鍵字,就建構了一個完整的、面向字元串程式設計的語言;

(list) 複合指令的功能是執行括号内的指令序列,而“$”用于引導字元串展開,是以 $(list) 用于指令替換(是以我前面說“$()”形式的指令替換比“``”形式的指令替換更加具有一緻性)。比如 ((expresion)) 用于數學表達式求值,是以 $((expression)) 代表算術展開。再比如“{}”和“,”配合使用,且中間沒有空格時,代表大括号展開,但是當需要使用“{ }”來定義複合指令時,必須把“{ }”當關鍵字,它們和它裡面的内容必須以空格隔開,而且“}”和它前面的一條指令之間必須有一個“;”或者“<newline>”。這些概念上的一緻性設計得非常精妙,使用起來自然而然可以讓人體會到一種美感;

  4.完美解決了一個指令執行時的輸出和運作狀态的分離。有其它程式設計語言經曆的人也經常會遇到這樣的問題:當我們調用一個函數的時候,函數可能會産生兩個結果,一個是函數的傳回值,一個是函數調用是否成功。在C#和Java等進階語言中,往往使用try...catch等捕獲異常的方式來判斷函數調用是否成功,但仍然有程式員讓函數傳回null代表失敗,而C語言這種沒有異常機制的語言,實在是難以判斷一個函數的傳回值究竟如何表示該函數調用是否成功(比如就有很多API讓函數傳回-1代表失敗,而有的函數運作失敗是會設定errno全局變量)。在Bash中,指令運作的狀态和指令的标準輸出區分很明确,如果你需要指令的标準輸出,使用指令替換來生成字元串,如果你隻需要指令的運作狀态,直接将指令寫在if語句之中即可,或者使用$?特殊變量來檢查上一條指令的運作狀态。如果不想在檢查指令運作狀态的時候讓指令的标準輸出影響使用者,可以把它重定向到/dev/null,比如

if cat /etc/passwd | grep youxia > /dev/null; then echo 'youxia is exist'; fi      

  5.使用管道和輸入輸出重定向讓檔案的讀寫變得簡單。想一想在C語言中怎麼讀檔案吧,除了麻煩的open、close不說,每讀一個字元串還得先準備一個buffer,準備長了怕浪費空間,準備短了怕緩沖區溢出,虐心啦。使用Bash,那真的是太友善了。

  6.它還是一門不折不扣的動态語言哦,eval指令實在是太強大了,請看下圖,模拟指針進行查表:

Linux應用環境實戰10:Bash腳本程式設計語言中的美學與哲學(轉)

  當然,自從Bash 3之後,Bash本身就提供了間接引用的功能(使用“${!var}”)。

  例外:

array=(a b c d e f) ,這和前面講的“()”用于在子Shell中執行指令序列還真的是不一緻。

​​回到頂部​​

總結:

  以上内容是我的胡言亂語,因為以上内容即無法教會大家完整的Bash文法,也無法教會大家用Bash做任何一點有意義的工作。

  如果想用Bash幹點實事,我送大家一本O'Reilly出的《Shell腳本學習指南》:​​Shell腳本學習指南.part1.rar​​​ ​​Shell腳本學習指南.part2.rar​​​ ​​Shell腳本學習指南.part3.rar​​​ ​​Shell腳本學習指南.part4.rar​​ (為什麼一次隻能上傳10M啊,好好的一本書被拆分成4部分了。)

man bash 。