概述
偶然間發現 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 <<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編程的一些高級用法。還是那句話,建議有一定基礎的同學學習,畢竟在跑之前要先學會走路不是?