Bash腳本15分鐘進(jìn)階教程
這里的技術(shù)技巧最初是來自谷歌的“Testing on the Toilet” (TOTT)。這里是一個修訂和擴(kuò)增版本。
腳本安全
我的所有bash腳本都以下面幾句為開場白:
- #!/bin/bash
 - set -o nounset
 - set -o errexit
 
這樣做會避免兩種常見的問題:
- 引用未定義的變量(缺省值為“”)
 - 執(zhí)行失敗的命令被忽略
 
需要注意的是,有些Linux命令的某些參數(shù)可以強(qiáng)制忽略發(fā)生的錯誤,例如“mkdir -p” 和 “rm -f”。
還要注意的是,在“errexit”模式下,雖然能有效的捕捉錯誤,但并不能捕捉全部失敗的命令,在某些情況下,一些失敗的命令是無法檢測到的。(更多細(xì)節(jié)請參考這個帖子。)
腳本函數(shù)
在bash里你可以定義函數(shù),它們就跟其它命令一樣,可以隨意的使用;它們能讓你的腳本更具可讀性:
- ExtractBashComments() {
 - egrep "^#"
 - }
 - cat myscript.sh | ExtractBashComments | wc
 - comments=$(ExtractBashComments < myscript.sh)
 
還有一些例子:
- SumLines() { # iterating over stdin - similar to awk
 - local sum=0
 - local line=””
 - while read line ; do
 - sum=$((${sum} + ${line}))
 - done
 - echo ${sum}
 - }
 - SumLines < data_one_number_per_line.txt
 - log() { # classic logger
 - local prefix="[$(date +%Y/%m/%d\ %H:%M:%S)]: "
 - echo "${prefix} $@" >&2
 - }
 - log "INFO" "a message"
 
盡可能的把你的bash代碼移入到函數(shù)里,僅把全局變量、常量和對“main”調(diào)用的語句放在最外層。
變量注解
Bash里可以對變量進(jìn)行有限的注解。最重要的兩個注解是:
local(函數(shù)內(nèi)部變量)readonly(只讀變量)
- # a useful idiom: DEFAULT_VAL can be overwritten
 - # with an environment variable of the same name
 - readonly DEFAULT_VAL=${DEFAULT_VAL:-7}
 - myfunc() {
 - # initialize a local variable with the global default
 - local some_var=${DEFAULT_VAL}
 - ...
 - }
 
這樣,你可以將一個以前不是只讀變量的變量聲明成只讀變量:
- x=5
 - x=6
 - readonly x
 - x=7 # failure
 
盡量對你bash腳本里的所有變量使用local或readonly進(jìn)行注解。
用$()代替反單引號(`)
反單引號很難看,在有些字體里跟正單引號很相似。$()能夠內(nèi)嵌使用,而且避免了轉(zhuǎn)義符的麻煩。
- # both commands below print out: A-B-C-D
 - echo "A-`echo B-\`echo C-\\\`echo D\\\`\``"
 - echo "A-$(echo B-$(echo C-$(echo D)))"
 
用[[]](雙層中括號)替代[]
使用[[]]能避免像異常的文件擴(kuò)展名之類的問題,而且能帶來很多語法上的改進(jìn),而且還增加了很多新功能:
| 操作符 | 功能說明 | 
|---|---|
| || | 邏輯or(僅雙中括號里使用) | 
| && | 邏輯and(僅雙中括號里使用) | 
| < | 字符串比較(雙中括號里不需要轉(zhuǎn)移) | 
| -lt | 數(shù)字比較 | 
| = | 字符串相等 | 
| == | 以Globbing方式進(jìn)行字符串比較(僅雙中括號里使用,參考下文) | 
| =~ | 用正則表達(dá)式進(jìn)行字符串比較(僅雙中括號里使用,參考下文) | 
| -n | 非空字符串 | 
| -z | 空字符串 | 
| -eq | 數(shù)字相等 | 
| -ne | 數(shù)字不等 | 
單中括號:
- [ "${name}" \> "a" -o ${name} \< "m" ]
 
雙中括號
- [[ "${name}" > "a" && "${name}" < "m" ]]
 
正則表達(dá)式/Globbing
使用雙中括號帶來的好處用下面幾個例子最能表現(xiàn):
- t="abc123"
 - [[ "$t" == abc* ]] # true (globbing比較)
 - [[ "$t" == "abc*" ]] # false (字面比較)
 - [[ "$t" =~ [abc]+[123]+ ]] # true (正則表達(dá)式比較)
 - [[ "$t" =~ "abc*" ]] # false (字面比較)
 
注意,從bash 3.2版開始,正則表達(dá)式和globbing表達(dá)式都不能用引號包裹。如果你的表達(dá)式里有空格,你可以把它存儲到一個變量里:
- r="a b+"
 - [[ "a bbb" =~ $r ]] # true
 
按Globbing方式的字符串比較也可以用到case語句中:
- case $t in
 - abc*) <action> ;;
 - esac
 
#p#
字符串操作
Bash里有各種各樣操作字符串的方式,很多都是不可取的。
基本用戶
- f="path1/path2/file.ext"
 - len="${#f}" # = 20 (字符串長度)
 - # 切片操作: ${<var>:<start>} or ${<var>:<start>:<length>}
 - slice1="${f:6}" # = "path2/file.ext"
 - slice2="${f:6:5}" # = "path2"
 - slice3="${f: -8}" # = "file.ext"(注意:"-"前有空格)
 - pos=6
 - len=5
 - slice4="${f:${pos}:${len}}" # = "path2"
 
替換操作(使用globbing)
- f="path1/path2/file.ext"
 - single_subst="${f/path?/x}" # = "x/path2/file.ext"
 - global_subst="${f//path?/x}" # = "x/x/file.ext"
 - # 字符串拆分
 - readonly DIR_SEP="/"
 - array=(${f//${DIR_SEP}/ })
 - second_dir="${arrray[1]}" # = path2
 
刪除頭部或尾部(使用globbing)
- f="path1/path2/file.ext"
 - # 刪除字符串頭部
 - extension="${f#*.}" # = "ext"
 - # 以貪婪匹配方式刪除字符串頭部
 - filename="${f##*/}" # = "file.ext"
 - # 刪除字符串尾部
 - dirname="${f%/*}" # = "path1/path2"
 - # 以貪婪匹配方式刪除字符串尾部
 - root="${f%%/*}" # = "path1"
 
避免使用臨時文件
有些命令需要以文件名為參數(shù),這樣一來就不能使用管道。這個時候 <() 就顯出用處了,它可以接受一個命令,并把它轉(zhuǎn)換成可以當(dāng)成文件名之類的什么東西:
- # 下載并比較兩個網(wǎng)頁
 - diff <(wget -O - url1) <(wget -O - url2)
 
還有一個非常有用處的是”here documents”,它能讓你在標(biāo)準(zhǔn)輸入上輸入多行字符串。下面的’MARKER’可以替換成任何字詞。
- # 任何字詞都可以當(dāng)作分界符
 - command << MARKER
 - ...
 - ${var}
 - $(cmd)
 - ...
 - MARKER
 
如果文本里沒有內(nèi)嵌變量替換操作,你可以把***個MARKER用單引號包起來:
- command << 'MARKER'
 - ...
 - no substitution is happening here.
 - $ (dollar sign) is passed through verbatim.
 - ...
 - MARKER
 
內(nèi)置變量
| 變量 | 說明 | 
|---|---|
| $0 | 腳本名稱 | 
| $n | 傳給腳本/函數(shù)的第n個參數(shù) | 
| $$ | 腳本的PID | 
| $! | 上一個被執(zhí)行的命令的PID(后臺運行的進(jìn)程) | 
| $? | 上一個命令的退出狀態(tài)(管道命令使用${PIPESTATUS}) | 
| $# | 傳遞給腳本/函數(shù)的參數(shù)個數(shù) | 
| $@ | 傳遞給腳本/函數(shù)的所有參數(shù)(識別每個參數(shù)) | 
| $* | 傳遞給腳本/函數(shù)的所有參數(shù)(把所有參數(shù)當(dāng)成一個字符串) | 
$*很少是正確的選擇。$@能夠處理空格參數(shù),而且參數(shù)間的空格也能正確的處理。$@時應(yīng)該用雙引號括起來,像”$@”這樣。調(diào)試
對腳本進(jìn)行語法檢查:
- bash -n myscript.sh
 
跟蹤腳本里每個命令的執(zhí)行:
- bash -v myscripts.sh
 
跟蹤腳本里每個命令的執(zhí)行并附加擴(kuò)充信息:
- bash -x myscript.sh
 
你可以在腳本頭部使用set -o verbose和set -o xtrace來***指定-v和-o。當(dāng)在遠(yuǎn)程機(jī)器上執(zhí)行腳本時,這樣做非常有用,用它來輸出遠(yuǎn)程信息。
什么時候不應(yīng)該使用bash腳本
- 你的腳本太長,多達(dá)幾百行
 - 你需要比數(shù)組更復(fù)雜的數(shù)據(jù)結(jié)構(gòu)
 - 出現(xiàn)了復(fù)雜的轉(zhuǎn)義問題
 - 有太多的字符串操作
 - 不太需要調(diào)用其它程序和跟其它程序管道交互
 - 擔(dān)心性能
 
這個時候,你應(yīng)該考慮一種腳本語言,比如Python或Ruby。
參考
- Advanced Bash-Scripting Guide: http://tldp.org/LDP/abs/html/
 - Bash Reference Manual
 















 
 
 





 
 
 
 