1. 程式人生 > >Bash 指令碼程式設計的一些高階用法

Bash 指令碼程式設計的一些高階用法

## 概述 偶然間發現 ```man bash``` 上其實詳細講解了 ```shell``` 程式設計的語法,包括一些很少用卻很實用的高階語法。就像發現了寶藏的孩子,興奮莫名。於是參考```man bash```,結合自己的理解,整理出了這篇文章。 本文並不包含```man bash```所有的內容,也不會詳細講解```shell```程式設計,只會分享一些平時很少用,實際很實用的高階語法,或者是一些平時沒太注意和總結的經驗,建議有一定```shell```基礎的同學進階時可以看一看。 當然,這只是 ```Bash``` 上適用的語法,不確定是否所有的```Shell```都能用,請慎用。 ## shell語法 ### 管道 有一點```shell```程式設計基礎的應該都知道管道。這是一個或多個命令的序列,用字元```|```分隔。實際上,一個完整的管道格式是這樣的: ``` [time [-p]] [ ! ] command [ | command2 ... ] ``` ```time```單獨執行某一條命令非常容易理解,統計這個命令執行的時間,但管道這種多個命令的組合,他統計的是某一個命令的時間還是管道所有命令的時間呢?**如果保留字 time 作為管道字首,管道中止後將給出執行管道耗費的使用者和系統時間**。 如果保留字 ```!``` 作為管道字首,管道的退出狀態將是最後一個命令的退出狀態的邏輯非值。 否則,管道的退出狀態就是**最後一個命令的**。 shell 在返回退出狀態值之前,等待管道中的所有命令返回。 ### 複合命令 我們常見的```case ... in ... esac```語句,```if ... elif ... else```語句,```while .... do ... done```語句,```for ... in ...; do ... done```,甚至函式```function name() {....}```都屬於複合命令。 #### for 語句 ```for```迴圈常見的完整格式是: ``` for name [ in word ] ; do list ; done ``` 除此之外,其實還支援類似與C語言的for迴圈, ``` for (( expr1 ; expr2 ; expr3 )) ; do list ; done ``` 返回值是序列 list 中被執行的最後一個命令的返回值;或者是 false,如果任何表示式非法的話。 #### case 語句 ```man bash```上顯示,```case```語句的完整格式是```case word in [ [(] pattern [ | pattern ] ... ) list ;; ] ... esac```。 展開後應該是這樣的: ``` case word in [(] pattern [ | pattern ]) list ;; ... esac ``` 每一個```case```的分支,都是```pattern```,使用**與路徑擴充套件相同的匹配規則**來匹配,見下面的 *路徑擴充套件* 章節,且通過```|```支援多種匹配走同一分支。例如: ``` case ${val} in *linux* | *uboot* ) ... ;; ... esac ``` 如果找到一個匹配,相應的序列將被執行。找到一個匹配之後,不會再嘗試其後的匹配。 如果沒有模式可以匹配,返回值是 0。否則,返回序列中最後執行的命令的返回值。 #### select 語句 ```select```語句可以說用得很少,但其實在需要互動選擇的場景下非常實用。它的完整格式是: ``` select name [ in word ] do list done ``` 它可以顯示出帶編號的選單,使用者輸入不同的編號就可以選擇不同的選單,並執行不同的功能。我們看一個例子: ``` #!/bin/bash echo "What is your favourite OS?" select name in "Linux" "Windows" "Mac OS" "UNIX" "Android" do echo "You have selected $name" done ``` 執行結果是這樣的: ``` What is your favourite OS? 1) Linux 2) Windows 3) Mac OS 4) UNIX 5) Android #? 4↙ You have selected UNIX #? 1↙ You have selected Linux #? 9↙ You have selected #? 2↙ You have selected Windows #?^D ``` ```#?```用來提示使用者輸入選單編號,這實際是環境變數```PS3```的值,可以通過改這變數來改使用者提示資訊。```^D```表示按下 ```Ctrl+D``` 組合鍵,它的作用是結束 ```select``` 迴圈。 如果使用者輸入的選單編號不在範圍之內,例如上面我們輸入的 9,那麼就會給 name 賦一個空值;如果使用者輸入一個空值(什麼也不輸入,直接回車),會重新顯示一遍選單。 注意,select 是無限迴圈(死迴圈),輸入空值,或者輸入的值無效,都不會結束迴圈,只有遇到 break 語句,或者按下 Ctrl+D 組合鍵才能結束迴圈。通常和 case in 一起使用,在使用者輸入不同的編號時可以做出不同的反應。例如 ``` echo "What is your favourite OS?" select name in "Linux" "Windows" "Mac OS" "UNIX" "Android" do case $name in "Linux") echo "Linux是一個類UNIX作業系統,它開源免費,執行在各種伺服器裝置和嵌入式裝置。" break ;; "Windows") echo "Windows是微軟開發的個人電腦作業系統,它是閉源收費的。" break ;; ...... *) echo "輸入錯誤,請重新輸入" esac done ``` #### ( list ) 語句 ```( list )```會讓 ```list``` 序列將**在一個子 shell 中執行**。變數賦值和影響 shell 環境變數的內建命令在命令結束後不會再起作用。返回值是序列的返回值。 這個在需要臨時切換目錄或者改變環境變數的情況下非常使用。例如封裝編譯核心的命令,實現任何目錄下都可以直接編譯,我們總需要先```cd```到核心根目錄,再```make```編譯,最後再```cd```回原目錄。例如: ``` alias mkernel='cd ~/linux ; make -j4 ; cd -' ``` 這樣會導致,在編譯過程如果```Ctrl + C```取消返回時,你所處在的目錄就變成了```~/linux```。這種情況下,使用```( list )```就能解決這問題,甚至都不需要```cd -```返回原目錄,直接退出即可。 ``` alias mkernel='(cd ~/linux ; make -j4)' ``` 也例如,有某個程式比較挫,只能在程式目錄執行,在其他目錄,甚至上一級目錄執行,都會找不到資原始檔導致退出,我們可以這樣解決: ``` alias xmind='(cd ~/軟體/xmind/XMind_amd64 &>/dev/null && nohup ./XMind &>/dev/null) &' ``` #### (( expression)) 語句 表示式 ```expression``` 將被求值。如果表示式的值非零,返回值就是 0;否則返回值是 1。這種做法和 let "expression" 等價。 #### [[ expression ]] 語句 在 ```if``` 語句中,我們喜歡用 ```if [ expression ]; then ... fi```的*單括號*的形式,但看大神們的指令碼,他們更常用```if [[ expression ]]; then ... fi```的**雙括號**形式。 ```[ ... ]```等效於```test```命令,而```[[ ... ]]```是另一種命令語法,相似功能卻更高階,它除了傳統的條件表示式(Eg. [ ${val} -eq 0 ])外,還支援表示式的轉義,就是說可以**像在其他語言中一樣使用出現的比較符號**,例如```>```,```<=```,```&&```,```||```等。 舉個例子,要判斷變數```val```有值且大於4,用單括號需要這麼寫: ``` [ -n ${val} -a ${val} -gt 4 ] ``` 用雙括號可以這麼寫: ``` [[ -n ${val} && ${val} > 4 ]] ``` 當使用```==```和```!=```操作符時,操作符右邊的字串被認為是一個模式,根據下面 模式匹配 章節中的規則進行匹配。如果匹配則返回值是 0,否則返回1。模式的任何部分可以被引用,強制使它作為一個字串而被匹配。 ## 引用 這裡主要講的是```$'string'```特殊格式,注意的是,必須是單引號。它被擴充套件為```string```,其中的反斜槓轉義字元被替換為 ANSI C 標準中規定的字元。反斜槓轉義序列,如果存在的話,將做如下轉換: | 轉義 | 含義 | | :---: | :--- | | ```\a``` | alert (bell) 響鈴 | | ```\b``` | backspace 回退 | | ```\e``` | an escape character 字元 Esc | | ```\f``` | form feed 進紙 | | ```\n``` | new line 新行符 | | ```\r``` | carriage return 回車 | | ```\t``` | horizontal tab 水平跳格 | | ```\v``` | vertical tab 豎直跳格 | | ```\\``` | backslash 反斜槓 | | ```\'``` | single quote 單引號 | | ```\nnn``` | 一個八位元字元,它的值是八進位制值 nnn (一到三個數字) | | ```\xHH``` | 一個八位元字元,它的值是十六進位制值 HH (一到兩個十六進位制數字) | | ```\cx``` | 一個 ctrl-x 字元 | 例如,我希望把有換行的一段話暫存到某個變數: ``` $ var="第一行"$'\n'"第二行" $ echo "${var}" 第一行 第二行 ``` ## 引數 ### 陣列 Bash 提供了一維陣列變數。任何變數都可以作為一個數組;內建命令```declare```可以顯式地定義陣列。陣列的大小沒有上限,也沒有限制在連續對成員引用和 賦值時有什麼要求。陣列以整數為下標,從 0 開始。 除了```declare``定義陣列外,更常用的是以下兩種方式定義陣列變數: ``` $ array_var=( "mem1" 3 str ) $ array_var[4]="mem4" $ echo ${array_var[@]} mem1 3 str mem4 $ echo ${array_var[1]} 3 ``` 陣列的使用跟C語言很像,```[] + 下標數字```可以訪問特定某一個數組成員。花括號是必須的,以避免和路徑擴充套件衝突。 如果下標是 ```@``` 或是 ```*```,它擴充套件為陣列的所有成員。 這兩種下標只有在雙引號中才不同。在雙引號中,```${name[*]}```,把所有成員當成一個詞,用特殊變數 IFS 的第一個字元分隔;```${name[@]}``` 將陣列的每個成員擴充套件為一個詞。 如果陣列沒有成員,```${name[@]}``` 擴充套件為空串。這種不同類似於特殊引數 ```*``` 和 ```@``` 的擴充套件。在作為函式引數傳遞的時候能很明顯感受到他們的差別。 ``` #定義陣列 $ array=(a b c) # 定義函式 $ function func() { > echo first para is $1 > echo second para is $2 > echo third para is $3 > } # 雙引號+'*' $ func "${array[*]}" first para is a b c second para is third para is # 雙引號+‘@’ $ func "${array[@]}" first para is a second para is b third para is c ``` 內建命令 ```unset``` 用於銷燬陣列。```unset name[subscript]``` 將銷燬下標是 ```subscript``` 的元素。 ```unset name```, 這裡```name``` 是一個數組,或者 ```unset name[subscript]```, 這裡```subscript``` 是 ```*```或者是```@```,將銷燬整個陣列。 ## 擴充套件 ### 花括號擴充套件 什麼是花括號擴充套件,舉個例子就好理解了 ``` mkdir /usr/local/src/bash/{old,new,dist} ``` 等效於 ``` mkdir /usr/local/src/bash/old /usr/local/src/bash/new /usr/local/src/bash/dist ``` 除此之外,還支援模式匹配來批量選擇,例如: ``` chown root /usr/{ucb/{ex,edit},lib/{ex?.?*,how_ex}} ``` ### 變數擴充套件 我們知道,```${var}```的形式可以獲取變數```var```的值,但其實還可以有更多花式玩法。其中```~```表示使用者根目錄其實屬於 **波浪線擴充套件**,這比較常見,不展開介紹了。 下面的每種情況中,word 都要經過波浪線擴充套件,引數擴充套件,命令替換和 算術擴充套件。如果不進行子字串擴充套件,bash 測試一個沒有定義或值為空的 引數;忽略冒號的結果是隻測試未定義的引數。 大致描述下變數擴充套件的功能: | 擴充套件 | 功能 | | :---: | :--- | | ```${var}``` | 獲取變數值 | | ```${!var}``` | 取變數var的值做新的變數名,再次獲取新變數名的值 | | ```${!prefix*``` | 獲取prefix開頭的變數名 | | ```${#parameter}``` |獲取變數長度 | | ```${parameter:-word}``` | parameter為空時,使用wrod返回 | | ```${parameter:+word}``` | parameter非空時,使用word返回 | | ```${parameter:=word}``` | parameter為空時,使用word返回,同時把word賦值給parameter變數 | | ```${parameter:?word}``` | parameter為空時,列印錯誤資訊word | | ```${parameter:offset}``` | 從offset位置擷取字串 | | ```${parameter:offset:length``` | 從offset位置擷取length長度的字串 | | ```${parameter#word}``` | 從頭開始刪除最短匹配word模式的內容後返回 | | ```${parameter##word}``` | 從頭開始刪除最長匹配word模式的內容後返回 | | ```${parameter%word}``` | 從尾開始刪除最短匹配word模式的內容後返回 | | ```${parameter%%word}``` | 從尾開始刪除最長匹配word模式的內容後返回 | | ```${parameter/pattern/string}``` | 最長匹配pattern的內容替換為string | | ```${parameter//pattern/string}``` | 所有匹配pattern的內容替換為string | #### ${!var} ```${!var}```是間接擴充套件。```bash``` 使用以 ```var``` 的其餘部分為名的變數的值作為變數的名稱; 接下來新的變數被擴充套件,它的值用在隨後的替換當中,而不是使用```var```自身的值。 有點拗口,舉個例子就懂了 ``` $ var_name=val $ val="Bash expansion" $ echo ${!var_name} Bash expansion ``` 所以,```${!var_name}```等效於```${val}```,就是取```val_name```的值作為變數名,再獲取新變數名的值。 ```!```有一種例外情況,那就是```${!prefix*}```,下面再介紹。 #### ${!prefix*} ```${!prefix*}```實現擴充套件為名稱以 prefix 開始的變數名,以特殊變數 IFS 的第一個字元分隔。換句話說,這種用法就是用於**獲取變數名**的。例如: ``` # 建立3個以VAR開頭的變數 $ VAR_A=a $ VAR_B=b $ VAR_C=c # 尋找以VAR開頭的變數名 $ echo ${!VAR*} VAR_A VAR_B VAR_C ``` #### ${#parameter} ```${#parameter}```用於獲取變數的長度。如果 ```parameter``` 是```*``` 或者是 ```@```, 替換的值是位置引數的個數。如果 ```parameter``` 是一個數組名,下標是 ```*``` 或者是 ```@```, 替換的值是陣列中元素的個數。 #### ${parameter:-word} ```${parameter:-word}```表示使用預設值。如果 ```parameter``` 未定義或值為空,將替換為 ```word``` 的擴充套件。否則,將替換為 parameter 的值。 #### ${parameter:=word} ```${parameter:=word}```賦預設值。如果 ```parameter``` 未定義或值為空, ```word``` 的擴充套件將賦予 ```parameter```。```parameter``` 的值將被替換。位置引數和特殊引數不能用這種方式賦值。 ```${parameter:=word}```和```${parameter:-word}```有什麼差別?還是舉個例子: ``` # 刪除var變數 $ unset var # 確認var變數為空 $ echo ${var} # 當var為空時,把test賦值給var,同時返回test $ echo ${var:=test} test # 可以看到,此時var已經被賦值 $ echo ${var} test # 再次刪除var變數,繼續實驗 $ unset var # 當var為空時,返回test $ echo ${var:-test} test # 對比驗證,此時var並沒有賦值 $ echo ${var} ``` 所以,差別在於,當```parameter```為空時,```${parameter:=word}```會比```${parameter:-word}```多做一步,就是把```word```的值賦給```parameter```。 #### ${parameter:?word} ```${parameter:?word}```主要用於當```parameter```為空時,顯示錯誤資訊```word```。```shell``` 如果不是互動的,則將退出。 #### ${parameter:+word} 如果 parameter 未定義或非空,不會進行替換;否則將替換為 word 擴充套件後的值。這與```${parameter:-word}```完全相反。簡單來說,就是**當```parameter```非空時,才使用```word```**。 #### ${parameter:offset} 同 ```${parameter:offset:length}``` #### ${parameter:offset:length} ```${parameter:offset:length}```可以實現字串的擷取,從```offset```開始,擷取```length```個字元。如果 offset 求值結果小於 0, 值將當作從 ```parameter``` 的值的末尾算起的偏移量。如果```parameter``` 是 ```@```,結果是 ```length``` 個位置引數,從 ```offset``` 開始。 如果 ```parameter``` 是一個數組名,以 ```@``` 或 ```*``` 索引,結果是陣列的 ```length``` 個成員,從 ```${parameter[offset]}``` 開始。 子字串的下標是從 0 開始的,除非使用位置引數時,下標從 1 開始。 #### ${parameter#word} 參考 ```${parameter##word}``` #### ${parameter##word} ```word```支援模式匹配,從```parameter```的開始位置尋找匹配,一個```#```的是尋找最短匹配,兩個```#```的是尋找最長匹配,把匹配的內容刪除後,把剩下的返回。例如: ``` $ str="we are testing, we are testing" $ echo ${str#*are} testing, we are testing $ echo ${str##*are} testing ``` 這必須是從頭開始刪的,如果要刪除中間的某一些字串,可以用```${parameter/pattern/string}```。 如果 ```parameter```是一個數組變數,下標是```@```或者是```*```,模式刪除將依次施用於陣列中的每個成員,最後擴充套件為結果的列表。 #### ${parameter%word} 參考```${parameter%%word}``` #### ${parameter%%word} 這也是在```parameter```中刪除匹配的內容後返回。```%```與```#```非常類似,前者是從頭開始匹配,後者是從尾部開始匹配。同樣的,一個```%```是尋找最短匹配,兩個```%%```是尋找最長匹配。例如: ``` $ str="we are testing, we are testing" $ echo ${str%are*} we are testing, we $ echo ${str%%are*} we ``` 這必須是從末端開始刪的,如果要刪除中間的某一些字串,可以用```${parameter/pattern/string}```。 如果 ```parameter```是一個數組變數,下標是```@```或者是```*```,模式刪除將依次施用於陣列中的每個成員,最後擴充套件為結果的列表。 #### ${parameter/pattern/string} 參考```${parameter//pattern/string}``` #### ${parameter//pattern/string} ```${parameter//pattern/string}```和```${parameter/pattern/string}```,主要實現了字串替換,當然,如果要替換的結果是空,就等效於刪除。一個```/```,表示只有第一個匹配的被替換,兩個```/```表示所有匹配的都替換。例如: ``` $ str="we are testing, we are testing" # 替換首次匹配 $ echo ${str/we are/I am} I am testing, we are testing # 替換所有匹配 $ echo ${str//we are/I am} I am testing, I am testing # 刪除首次匹配 $ echo ${str/are/} we testing, we are testing # 刪除所有匹配 $ echo ${str//are/} we testing, we testing ``` 如果```patten```以```#```開始,例如```${str/#we are/}```,則必須從頭開始就匹配;以```%```表示,例如```${str/%are testing/}```,必須從末端就要完全匹配。 如果 ```parameter```是一個數組變數,下標是```@```或者是```*```,模式刪除將依次施用於陣列中的每個成員,最後擴充套件為結果的列表。 ### 路徑擴充套件 我們經常會這樣使用路徑擴充套件,```ls ~/work*```,這裡的```*```就是路徑匹配的一種,表示匹配包含空串的任何字串。除了```*```之外,還有```?```和```[```。路徑擴充套件其實運用了**模式匹配**,所以匹配規則不妨直接看**模式匹配**。 #### 模式匹配 任何模式中出現的字元,除了下面描述的特殊模式字元外,都匹配它本身。 模式中不能出現 NUL 字元。如果要匹配字面上的特殊模式字元,它必須被引用。 特殊模式字元有下述意義: * ```*```: 匹配任何字串包含空串。 * ```?```: 匹配任何單個字元。 * ```[...]```: 匹配括號內的任意一個字元,與正則匹配一致。 與正則的```[...]```一致,```[!...]```或者```[^...]```表示不匹配括號內的字元;```[a-zA-Z]```表示從a到z以及從A到Z的所有字元;也支援```[:alinum:]```這類的特殊字元。 如果使用內建命令 shopt 啟用了 shell 選項 extglob, 將識別另外幾種模式匹配操作符。 * ```?(pattern-list)```:匹配所給模式零次或一次出現 * ```*(pattern-list)```:匹配所給模式零次或多次出現 * ```+(pattern-list)```:匹配所給模式一次或多次出現 * ```@(pattern-list)```:準確匹配所給模式之一 * ```!(pattern-list)```:任何除了匹配所給模式之一的字串 ## 重定向 簡單的重定向不累述了,講一些高階用法。 ### Here Documents here-document 的格式是: ``` <<[-]word here-document delimiter ``` 這種重定向使得 shell 從當前原始檔讀取輸入,直到遇到僅包含 ```word``` 的一行 (並且沒有尾部空白,trailing blanks) 為止。直到這一點的所有行被用作 命令的標準輸入。 還是聽拗口,咱們看例子: ``` $ cat < fist line > second line > third line > EOF fist line second line third line ``` 上述的做法,把兩個```EOF```之間的內容作為一個檔案,傳遞給```cat```命令。甚至,我們還有更高階的用法,實現動態建立檔案。 ``` $ kernel=linux $ cat > ./readme.txt < You are using kernel ${kernel} > EOF $ cat ./readme.txt You are using kernel linux ``` ### Here Strings here-document 的變種,形式是 ``` <<word```,使得以 ```word``` 擴充套件結果為名的檔案被開啟,通過檔案描述符 ```n``` 進行讀寫。如果沒有指定 ```n``` 那麼就使用檔案描述符 ```0```。如果檔案不存在,它將被建立。 這操作暫時沒用過,待補充示例。 ## 總結 本文結合```man bash```以及自己的一些經驗,總結了Shell程式設計的一些高階用法。還是那句話,建議有一定基礎的同學學習,畢竟在跑之前要先學會走路