天天看點

快速學習Bash

作者:Vamei 出處:http://www.cnblogs.com/vamei 嚴禁轉載。

Shell是Linux下經典的文本互動方式,而Bash是現在最常用的一種Shell。我在這裡總結了Bash的要點知識。

Linux圖形化桌面算不上精美。幸好,Linux提供了更好的與樹莓派互動的方式:Shell。打開終端(Terminal),桌面上就會出現一個黑色背景的視窗,裡面就運作着一個Shell。如果你敲擊鍵盤,會發現字元會顯示在$提示符的後面,形成一串文本形式的指令。所謂的Shell,就是運作在終端中的文本互動程式。Shell分析你的文本輸入,然後把文本轉換成相應的計算機動作。

在後面的内容中,我将用$來表示Linux系統Shell的指令提示符。比如說輸入date指令:

date用于日期時間的相關功能。敲擊Enter鍵Enter後,Shell會顯示出系統目前的時間。

Shell看起來簡陋,但實際上比圖形化桌面強大得多。它是Unix體系下的文本互動界面。你隻需要用鍵盤來輸入文本,就可以和作業系統互動。但這還是不夠具體。說到底,Shell其實是一個運作着的程式。這個程式接收到你按下Enter鍵之間的輸入,就會對輸入的文本進行分析。比如下面這個指令:

包括空格在内總共7個字元。Shell程式會通過空格,區分出指令的不同部分。第一個部分是指令名。剩下的部分是選項和參數。在這個例子中,Shell會進一步分析第二個部分,發現這一部分的開頭是"-"字元,進而知道它是一個選項。

有了指令名,Shell下一步就要執行該指令名對應的動作。這聽起來就像是在戲劇舞台上,演員按照腳本演戲。Shell指令可以分為如下三類:

Shell内建函數(built-in function)

可執行檔案(executable file)

别名(alias)

Shell的内建函數是Shell自帶的功能,而可執行檔案是儲存在Shell之外的腳本,提供了額外的功能。Shell必須在系統中找到對應指令名的可執行檔案,才能正确執行。我們可以用絕對路徑來告訴Shell可執行檔案所在的位置。如果使用者隻是給出了指令名,而沒有給出準确的位置,那麼Shell必須自行搜尋一些特殊的位置,也就是所謂的預設路徑。Shell會執行第一個名字和指令名相同的可執行檔案。這就相當于,Shell幫我們自動補齊了可執行檔案的位置資訊。我們可以通過which指令,來确定指令名對應的是哪個可執行檔案:

别名是給某個指令一個簡稱,以後在Shell中就可以通過這個簡稱來調用對應的指令。在Shell中,我們可以用alias來定義别名:

Shell會記住我們的别名定義。以後我在這個Shell中輸入指令freak時,都将等價于輸入free -h。

在Shell中,我們可以通過type指令來了解指令的類型。如果一個指令是可執行檔案,那麼type将列印出檔案的路徑。

總的來說,Shell就是根據空格和其他特殊符号,來讓電腦了解并執行使用者要求的動作。到了後面,我們還将看到Shell中其他的特殊符号。

Shell是文本解釋器程式的統稱,是以包括了不止一種Shell。常見的Shell有sh、bash、ksh、rsh、csh等。在樹莓派中,就安裝了sh和bash兩個Shell解釋器。sh的全名是Bourne Shell。名字中的玻恩就是這個Shell的作者。而bash的全名是Bourne Again Shell。最開始在Unix系統中流行的是sh,而bash作為sh的改進版本,提供了更加豐富的功能。一般來說,都推薦使用bash作為預設的Shell。樹莓派,以及其他Linux系統中廣泛安裝sh,都是出于相容曆史程式的目的。

我們可以通過下面的指令來檢視目前的Shell類型:

echo用于在終端列印出文本。而$是一個新的Shell特殊符号。它提示Shell,後面跟随的不是一般的文本,而是用于存儲資料的變量。Shell會根據變量名找到真正的文本,替換到變量所在的位置。SHELL變量存儲了目前使用的Shell的資訊你可以在bash中用sh指令啟動sh,并可以用exit指令從中退出。

我們已經看到,一行指令裡還可以包含着選項和參數。總的來說,選項用于控制指令的行為,而參數說明了指令的作用對象。比如說:

在上面的指令中,選項-m影響了指令uname的行為,導緻uname輸出了樹莓派的CPU型号。如果不是該選項的影響,uname輸出的将是"Linux"。我們不妨把每個指令看做多功能的瑞士軍刀,而選項讓指令在不同的功能間切換。由一個"-"引領一個英文字母,這成為短選項。多個短選項的字母可以合在一起,跟在同一個"-"後面。比如,下面的兩個指令就等價:

此外還有一種長選項,是用"--"引領一整個英文單詞,比如:

上面的指令将輸出date程式的版本資訊。

如果說選項控制了瑞士軍刀的行為,那麼參數就提供了瑞士軍刀發揮用場的原材料。就拿echo這個指令來說,它能把字元列印到終端。它選擇列印的對象,正是它的參數:

有的時候,選項也會攜帶變量,以便來說明選項行為的原材料。比如:

選項"--set"用于設定時間,用等号連接配接的,就是它的參數。date會把日期設定成這一變量所代表的日期。如果用短選項,那麼就要用空格取代等号了:

值得注意的是,Shell對空格敏感。當一整個參數資訊中包含了空格時,我們需要用引号把參數包裹起來,以便Shell能識别出這是一個整體。

所謂的選項和參數提供給指令的附加資訊。是以,指令最終會拿這些字元串做什麼,是由指令自己決定的。是以,有時會發現一些特異的選項或參數用法。這個時候,你就要從文檔中尋找答案。

我們可以在Bash中輸入一行的指令。Bash會把輸入的指令轉化為特定的動作。從這一節起,我們将看到Bash的可程式設計性。Bash提供了某些類似于C語言那樣的程式設計文法,進而允許你用程式設計的方式,來組合使用Linux系統。我們首先看Bash用變量存儲資料的能力。正如我們在C語言中看到的,變量是記憶體中的一塊兒空間,可以用于存儲資料。我們可以通過變量名來引用變量中保持的資料。借助變量,程式員可以複用出現過的資料。Bash中也有變量,但Bash的變量隻能存儲文本。

1)變量指派

Bash和C類似,同樣用“=”來表示指派。比如:

就是把文本World存入名為var的變量,即指派。根據Bash的文法,指派符号“=”的前後不留白格。指派号右邊的文本内容會存入指派号左邊的變量。

如果文本中包含空格,那麼你可以用單引号或雙引号來包裹文本。比如:

或者:

在Bash中,我們可以把一個指令輸出的文本直接賦予給一個變量:

借助``符号,date指令的輸出存入了變量now。

我們還可以把一個變量中的資料指派給另一個變量:

2)引用變量

我們可以用$var的方式來引用變量。在Bash中,所謂的引用變量就是把變量翻譯成變量中存儲的文本。比如:

 就會列印出World,即變量中儲存的文本。

在Bash中,你還可以在一段文本中嵌入變量。Bash也會把變量替換成變量中儲存的文本。比如: 

 文本将列印出HelloWorld。

為了避免變量名和尾随的普通文本混淆,我們也可以換用${}的方式來辨別變量。比如說:

 由于Bash中并沒有varIsGood這個變量,是以Bash将列印空白行。但如果将指令改為: 

 Bash通過${}識别出變量var,并把它替換成資料。最終echo指令列印出WorldIsGood。

在Bash中,為了把一段包含空格的文本當做單一參數,就需要用到單引号或雙引号。你可以在雙引号中使用變量。比如:

 将列印Hello World。與此相對,Bash會忽視單引号中的變量引用,是以單引号中的變量名隻會被當做普通文本,比如:

 将列印Hello $var。

在Bash中,數字和運算符都被當做普通文本。是以你無法像C語言一樣便捷地進行數學運算。比如執行下面的指令:

Bash并不會進行任何運算。它隻會列印文本“1+2”。

在Bash中,你還可以通過$(())文法來進行數值運算。在雙括号中你可以放入整數的加減乘除表達式。Bash會對其中的内容進行數值運算。比如

 将列印運算結果12。此外,在$(())中,你也可以使用變量。比如: 

 将列印運算結果11。

你可以用Bash實作多種整數運算:

 加法:$(( 1 + 6 ))。結果為7。

 減法:$(( 5 – 3 ))。結果為2。

 乘法:$(( 2*2 ))。結果為4。

 除法:$(( 9/3 ))。結果為3。

 求餘:$(( 5%3 ))。結果為2。

 乘方:$(( 2**3 ))。結果為8。

現在,你就可以把數學運算結果存入變量:

在Linux中,每個可執行程式會有一個整數的傳回代碼。按照Linux慣例,當程式正常運作完畢并傳回時,将傳回整數0。是以,C程式中傳回0的語句,都出現在C程式中main函數的最後一句。例如下面的foo.c程式:

 這段程式可以正常運作。是以,它将在最後一句執行return語句,程式的傳回代碼是0。在Shell中,我們運作了程式後,可以通過$?變量來獲知傳回碼。比如:

如果一個程式運作異常,那麼這個程式将傳回非0的傳回代碼。比如删除一個不存在的檔案: 

 在Linux中,可以在一個行指令中執行多個程式。比如:

在執行多個程式時,我們可以讓後一個程式的運作參考前一個程式的傳回代碼。比如說,隻有前一個程式傳回成功代碼0,才讓後一個程式運作: 

如果rm指令順利運作,那麼第二個echo指令将執行。

還有一種情況,是等到前一個程式失敗了,才運作後一個程式,比如:

如果rm指令失敗,第二個echo指令才會執行。

你還可以把多行的Bash指令寫入一個檔案,成為所謂的Bash腳本。當Bash腳本執行時,Shell将逐行執行腳本中的指令。編寫Bash腳本,是我們開始實作Bash代碼複用的第一步。

1)腳本的例子

用文本編輯器編寫一個Bash腳本hello_world.bash: 

腳本的第一行說明了該腳本使用的Shell,即/bin/bash路徑的Bash程式。腳本正文是兩行echo指令。運作腳本的方式和運作可執行程式的方式類似,都是: 

需要注意的是,如果使用者不具有執行Bash腳本檔案的權限,那麼他将無法執行Bash腳本。此時,使用者必須更換檔案權限,或者以其他身份登入,才能執行腳本。當腳本運作時,兩行指令将按照由上至下的順序依次執行。Shell将列印兩行文本:

Hello World

Bash腳本是一種複用代碼的方式。我們可以用Bash腳本實作特定的功能。由于該功能記錄在腳本中,是以我可以反複地運作同一個檔案來實作相同的功能,而不是每次想用的時候都要重新敲一遍指令。我們看一個簡單的Bash腳本hw_info.bash,它将計算機的資訊存入到名為log的檔案中:

2)腳本參數

和可執行程式類似,Bash腳本運作時,也可以攜帶參數。這些參數可以在Bash腳本中以變量的形式使用。比如test_arg.bash:

在Bash中,你可以用$0、$1、$2……的方式,來獲得Bash腳本運作時的參數。我們用下面的方式運作Bash腳本:

$0是指令的第一部分,也就是./test_arg.bash。$1代表了參數hello,而$2代表了參數world。是以,上面程式将列印:

./test_arg.bash hello world

如果變更參數,同一段腳本将有不同的行為。這大大提高了Bash腳本的靈活性。上面的hw_info.bash腳本中,我們把輸出檔案名寫死成log。我們也可以修改腳本,用參數作為輸出檔案的檔案名:

借助參數,我們就可以自由地設定輸出檔案的名字:

3)腳本的傳回代碼

和可執行程式類似,腳本也可以有傳回代碼。還是按照慣例,腳本正常退出時傳回代碼0。在腳本的末尾,我們可以用exit指令來設定腳本的傳回代碼。我們修改hello_world.bash:

其實在腳本的末尾加一句exit 0并不必要。一個腳本如果正常運作完最後一句,會自動的傳回代碼0。在腳本運作後,我們可以通過$?變量查詢腳本的傳回代碼: 

如果在腳本中部出現exit指令,腳本會直接在這一行停止,并傳回該exit指令給出的傳回代碼。比如下面的demo_exit.bash:

你可以運作該腳本,檢查其輸出結果,并檢視其傳回代碼。

在Bash中,腳本和函數有很多相似的地方。腳本實作了一整個腳本檔案的程式複用,而函數複用了腳本内部的部分程式。一個函數可以像腳本一個包含多個指令,用于說明該函數如果被調用會執行哪些活動。在定義函數時,我們需要花括号來辨別函數包括的部分: 

腳本一開始定義了函數my_info,my_info是函數名。關鍵字function和花括号都提示了該部分是函數定義。是以,function關鍵字并不是必須的。上面的腳本等效于:

花括号中的三行指令,就說明了函數執行時需要執行的指令。需要強調的是,函數定義隻是食譜,并沒有轉化成具體的動作。腳本的最後一行是在調用函數。隻有通過函數調用,函數内包含的指令才能真正執行。調用函數時,隻需要一個函數名就可以了。

像腳本一樣,函數調用時還可以攜帶參數。在函數内部,我們同樣可以用$1、$2這種形式的變量來使用參數:

在上面的腳本中,進行了兩次函數調用。函數調用時,分别攜帶了參數output.file和another_output.file。

在Bash中使用source指令,可以實作函數的跨腳本調用。指令source的作用是在同一個程序中執行另一個檔案中的Bash腳本。比如說,有兩個腳本,my_info.bash和app.bash。腳本my_info.sh中的内容是: 

腳本app.bash中的内容是: 

運作app.bash時,執行到source指令那一行時,就會執行my_info.bash腳本。在app.bash的後續部分,就可以使用my_info.bash中的my_info函數。

我們已經介紹了函數和腳本兩種組合指令的方式。這兩種方式都可以把多行指令合并起來,組成一個功能單元。函數和腳本都實作了一定程度的代碼複用。從這一節起,我們将看到選擇和循環兩種文法結構,這兩種文法結構可以改變腳本的運作順序,進而編寫出更加靈活的程式。Bash除了可以進行數值運算,還可以進行邏輯判斷。邏輯判斷是決定某個說法的真假。我們在生活中很自然地進行各種各樣的邏輯判斷。比如“3大于2”這個說法,我們會說它是真的。邏輯判斷就是對一個說法判斷真假。在Bash中,我們可以用test指令來進行邏輯判斷:

指令test後面跟有一個判斷表達式,其中的-gt表示大于,即greater than。由于“3大于2”這一表達式為真,是以指令的傳回代碼将是0。如果表達式為1,那麼指令的傳回代碼是1:

表達式中的-lt表示小于,即less than。

數值大小和相等關系的判斷,是最常見的邏輯判斷。除了上面的大于和小于判斷,我們還可以進行以下的數值判斷:

等于: $test 3 -eq 3; echo $? 

不等于: $test 3 -ne 1; echo $? 

大于等于: $test 5 -ge 2; echo $? 

小于等于: $test 3 -le 1; echo $? 

Bash中最常見的資料形式是文本,是以也提供了很多關于文本的判斷:

文本相同: $test abc = abx; echo $? 

文本不同: $test abc != abx; echo $? 

按照詞典順序,一個文本在另一個文本之前: $test apple > tea; echo $? 

按照詞典順序,一個文本在另一個文本之後: $test apple < tea; echo $? 

Bash還可以對檔案的狀态進行邏輯判斷:

檢查一個檔案是否存在: $test –e a.out; echo $? 

檢查一個檔案是否存在,而且是普通檔案: $test –f file.txt; echo $? 

檢查一個檔案是否存在,而且是目錄檔案: $test –d myfiles; echo $? 

檢查一個檔案是否存在,而且是軟連接配接: $test –L a.out; echo $? 

檢查一個檔案是否可讀: $test –r file.txt; echo $? 

檢查一個檔案是否可寫: $test –w file.txt; echo $? 

檢查一個檔案是否可執行: $test –x file.txt; echo $? 

在做邏輯判斷時,可以把多個邏輯判斷條件用“與、或、非”的關系組合起來,形成複合的邏輯判斷。 

邏輯判斷可以獲得計算機和程序的狀态。進一步,Bash可以根據邏輯判斷,讓程式有條件地運作,這也就是所謂的選擇結構。選擇結構是一種文法結構,可以讓程式根據條件決定執行哪一部分的指令。最早的程式都是按照指令順序依次執行。選擇結構打破了這一順序,給程式帶來更高的靈活性。最簡單的,我們可以根據條件來決定是否執行某一部分程式,比如下面的demo_if.bash腳本:

這個腳本中使用了最簡單的if結構。關鍵字if後面跟着[],裡面是一個邏輯表達式。這個邏輯表達式就是if結構的條件。如果條件成立,那麼if将執行then到fi之間包含的語句,我們稱之為隸屬于then的代碼塊。如果條件不成立,那麼then的代碼塊不執行。這個例子的條件是判斷使用者是否為root。是以,如果是非root使用者執行該腳本,那麼Shell不會列印任何内容。

我們還可以通過if...then...else...結構,讓Bash腳本從兩個代碼塊中選擇一個執行。該選擇結構同樣有一個條件。如果條件成立,那麼将執行then附屬的代碼塊,否則執行else附屬的代碼塊。下面的demo_if_else.bash腳本是一個小例子:

if後面的“-e $filename”作為判斷條件。如果條件成立,即檔案存在,那麼執行then部分的代碼塊。如果檔案不存在,那麼腳本将執行else語句中的echo指令。末尾的fi結束整個文法結構。腳本繼續以順序的方式執行剩餘内容。運作腳本: 

腳本會根據a.out是否存在,列印出不同的内容。

我們看到,在使用if...then...else...結構時,我們可以實作兩部分代碼塊的選擇執行。而在then代碼塊和else代碼塊内部,我們可以繼續嵌套選擇結構,進而實作更多個代碼塊的選擇執行。比如腳本demo_nest.bash:

在Bash下,我們還可以用case文法來實作多程式塊的選擇執行。比如下面的腳本demo_case.bash: 

這個腳本和上面的demo_nest.bash功能完全相同。可以看到case結構與if結構的差別。關鍵字case後面不再是邏輯表達式,而是一個作為條件的文本。後面的代碼塊分為三個部分,都以文本标簽)的形式開始,以;;結束。在case結構運作時,會逐個檢查文本标簽。當條件文本和文本标簽可以對應上時,Bash就會執行隸屬于該文本标簽的代碼塊。如果是使用者vamei執行該Bash腳本,那麼條件文本和vamei标簽對應上,腳本就會列印:

You are a happy user.

文本标簽除了是一串具體的文本,還可以包含文本通配符。結構case中常用的通配符包括:

通配符 含義 文本标簽例子 通過的條件文本

* 任意文本 *) Xyz, 123, …

? 任意一個字元 a?c) abc, axc, …

[] 範圍内一個字元 [1-5][b-d]) 2b, 3d, …

上面的程式中最後一個文本标簽是通配符*,即表示任意條件文本都可以觸發此段代碼塊的運作。當然,前提是前面的幾個文本标簽都沒有“截胡”。

循環結構是程式設計語言中另一種常見的文法結構。循環結構的功能是重複執行某一段代碼,直到計算機的狀态符合某一條件。在while文法中,Bash會循環執行隸屬于while的代碼塊,直到邏輯表達式不成立。比如下面的demo_while.bash:

關鍵字do和done之間的代碼是隸屬于該循環結構的代碼塊。在while後面跟着條件,該條件決定了代碼塊是否重複執行下去。這個條件是用目前的時間與目标時間對比。如果目前時間小于目标時間,那麼代碼塊就會重複執行下去。否則,Bash将跳出循環,繼續執行後面的語句。

如果while的條件始終是真,那麼循環會一直進行下去。下面的程式就是以無限循環的形式,不斷播報時間: 

文法while的終止條件是一個邏輯判斷。如果在循環過程中改變邏輯判斷的内容,那麼我們很難在程式執行之前預判循環進行的次數。正如我們之前在demo_while.bash中看到的,我們在循環進行過程中改變着作為條件的邏輯表達式,不斷地更新參與邏輯判斷的目前時間。與while文法對應的是for循環。這種文法會在程式進行前确定好循環進行的次數,比如demo_for.bash: 

在這個例子中,指令ls log*将傳回所有以log開頭的檔案名。這些檔案名之間由空格分隔。循環進行時,Bash會依次取出一個檔案名,指派給變量var,并執行do和done之間隸屬于for結構的程式塊。由于ls指令傳回的内容在是确定的,是以for循環進行的次數也會在一開始确定下來。

在for文法中,我們也可以使用自己建構一個由空格分隔的文本。由空格區分出來的每個子文本會在循環中指派給變量。比如:

此外,for循環還可以和seq指令配合使用。指令seq用于生成一個等差的整數序列。指令後面可以跟3個參數,第一個參數表示整數序列的開始數字,第二個參數表示每次增加多少,最後一個參數表示序列的終點。是以,下面指令: 

将傳回:

1 3 5 7 9

可以看到,seq傳回的也是由空格分隔開的文本。是以,seq的傳回結果也可用于for循環。

結合for循環和seq指令,我們可以解一些有趣的數學問題。比如高斯求和,是要計算從1到100的所有整數的和。我們可以用Bash解決: 

 這個問題還可以用do while循環來求解:

這裡break語句的作用是在滿足條件時跳出循環。

如果想計算1到100所有不被3整數的和,則可以使用continue語句,跳過所有被3整數的數:

到了這裡,我們已經介紹完Bash語言的基本文法。Bash語言和C語言都是Linux下的常用語言。它們都能通過特定的文法來編寫程式,而程式運作後都能實作某些功能。盡管在文法細節上存在差異,但兩種語言都有以下文法:

變量:在記憶體中儲存資料

循環結構:重複執行代碼塊

選擇結構:根據條件執行代碼塊

函數:複用代碼塊

程式設計語言的作者在設計語言時,往往會借鑒已有程式設計語言的優點。這是程式設計語言之間相似性的一大原因。程式員往往要掌握不止一套程式設計語言。相似的文法特征,會讓程式員在學習新語言時感到親切,進而促進語言的推廣。

Bash和C的相似性,也來自于它們共同遵守的程式設計範式——面向過程程式設計。支援面向過程程式設計的語言,一般都會提供類似于函數的代碼封裝方式。函數把多行指令包裝成一個功能。隻要知道了函數名,程式可以通過調用函數來使用函數功能,最終實作代碼複用。除了面向過程程式設計,還有面向對象和函數式的程式設計範式。每種程式設計範式都提供了特定的代碼封裝方式,并達到代碼複用的目的。值得注意的是,近年來出現的新語言往往會支援不止一種程式設計範式。

除了相似性,我們還應該注意到Bash和C程式的差別。Bash的變量隻能是文本類型,C的變量卻可以有整數、浮點數、字元等類型。Bash的很多功能,如加減乘除運算,都是調用其他程式實作的。而C直接就可以進行加減乘除運算。可以說,C語言是一門真正的程式設計語言。C程式最終會編譯成二進制的可執行檔案。CPU可以直接了解這些檔案中的指令。

另一方面,Bash是一個Shell。它本質上是一個指令解釋器程式,而不是程式設計語言。使用者可以通過指令行的方式,來調用該程式的某些功能。所謂的Bash程式設計,隻是指令解釋器程式提供的一種互動方法。Bash腳本隻能和Bash程序互動。它不能像C語言一樣,直接調用CPU的功能。是以,Bash能實作的功能會受限,運作速度上也比不上可執行檔案。

但另一反面,Bash腳本也有它的好處。 C語言能接觸到很底層的東西,但使用起來也很複雜。有時候,即使你已經知道如何用C實作一個功能,寫代碼依然是一個很繁瑣的過程。Bash正相反。由于Bash可以便捷地調用已有的程式,是以很多工作可以用數行的腳本解決。此外,Bash腳本不需要編輯,就可以由Bash程序了解并執行。是以,開發Bash腳本比寫C程式要快很多。Linux的系統運維工作,如定期備份、檔案系統管理等,就經常使用到Bash腳本。總之,Bash程式設計知識是晉級為資深Linux使用者的必要條件。

歡迎閱讀“騎着企鵝采樹莓”系列文章

如果你喜歡這篇文章,歡迎推薦。

繼續閱讀