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 的擴展將賦予 parameterparameter 的值將被替換。位置參數和特殊參數不能用這種方式賦值。

${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為空時,顯示錯誤信息wordshell 如果不是交互的,則將退出。

${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 <<EOF
> fist line
> second line
> third line
> EOF
fist line
second line
third line

上述的做法,把兩個EOF之間的內容作為一個文件,傳遞給cat命令。甚至,我們還有更高級的用法,實現動態創建文件。

$ kernel=linux
$ cat > ./readme.txt <<EOF
> You are using kernel ${kernel}
> EOF
$ cat ./readme.txt
You are using kernel linux

Here Strings

here-document 的變種,形式是

<<<word

word 被擴展,提供給命令作為標准輸入,例如,我希望檢索變量的值,有以下兩種做法:

$ echo ${var} | grep "test"
$ grep "test" <<< ${var}

Opening File Descriptors for Reading and Writing

重定向操作符,[n]<>word,使得以 word 擴展結果為名的文件被打開,通過文件描述符 n 進行讀寫。如果沒有指定 n 那么就使用文件描述符 0。如果文件不存在,它將被創建。

這操作暫時沒用過,待補充示例。

總結

本文結合man bash以及自己的一些經驗,總結了Shell編程的一些高級用法。還是那句話,建議有一定基礎的同學學習,畢竟在跑之前要先學會走路不是?


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM