這裡的技術技巧最初是來自谷歌的“Testing on the Toilet” (TOTT)。這裡是一個修訂和擴增版本。
指令碼安全
我的所有bash指令碼都以下麵幾句為開場白:
#!/bin/bash
set -o nounset
set -o errexit
這樣做會避免兩種常見的問題:
-
取用未定義的變數(預設值為“”)
-
執行失敗的命令被忽略
需要註意的是,有些Linux命令的某些引數可以強制忽略發生的錯誤,例如“mkdir -p” 和 “rm -f”。
還要註意的是,在“errexit”樣式下,雖然能有效的捕捉錯誤,但並不能捕捉全部失敗的命令,在某些情況下,一些失敗的命令是無法檢測到的。(更多細節請參考這個帖子。)
指令碼函式
在bash裡你可以定義函式,它們就跟其它命令一樣,可以隨意的使用;它們能讓你的指令碼更具可讀性:
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程式碼移入到函式裡,僅把全域性變數、常量和對“main”呼叫的陳述句放在最外層。
變數註解
Bash裡可以對變數進行有限的註解。最重要的兩個註解是:
-
local(函式內部變數)
-
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進行註解。
用$()代替反單引號(`)
反單引號很難看,在有些字型裡跟正單引號很相似。$()能夠內嵌使用,而且避免了轉義符的麻煩。
# 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)))”
用[[]](雙層中括號)替代[]
使用[[]]能避免像異常的檔案副檔名之類的問題,而且能帶來很多語法上的改進,而且還增加了很多新功能:
運運算元 功能說明
|| 邏輯or(僅雙中括號裡使用)
&& 邏輯and(僅雙中括號裡使用)
< 字串比較(雙中括號裡不需要轉移)
-lt 數字比較
= 字串相等
== 以Globbing方式進行字串比較(僅雙中括號裡使用,參考下文)
=~ 用正則運算式進行字串比較(僅雙中括號裡使用,參考下文)
-n 非空字串
-z 空字串
-eq 數字相等
-ne 數字不等
單中括號:
[ “${name}” \> “a” -o ${name} \< “m” ]
雙中括號
[[ “${name}” > “a” && “${name}” < “m” ]]
正則運算式/Globbing
使用雙中括號帶來的好處用下麵幾個例子最能表現:
t=”abc123″
[[ “$t” == abc* ]]
# true (globbing比較)
[[ “$t” == “abc*” ]]
# false (字面比較)
[[ “$t” =~ [abc]+[123]+ ]]
# true (正則運算式比較)
[[ “$t” =~ “abc*” ]]
# false (字面比較)
註意,從bash 3.2版開始,正則運算式和globbing運算式都不能用引號包裹。如果你的運算式裡有空格,你可以把它儲存到一個變數裡:
r=”a b+”
[[ “a bbb” =~ $r ]]
# true
按Globbing方式的字串比較也可以用到case陳述句中:
case $t in
abc*) ;;
esac
字串操作
Bash裡有各種各樣操作字串的方式,很多都是不可取的。
基本使用者
f=”path1/path2/file.ext”
len=”${#f}” # = 20 (字串長度)
# 切片操作: ${:} or ${::}
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”
避免使用臨時檔案
有些命令需要以檔案名為引數,這樣一來就不能使用管道。這個時候?
# 下載並比較兩個網頁
diff
還有一個非常有用處的是”here documents”,它能讓你在標準輸入上輸入多行字串。下麵的’MARKER’可以替換成任何字詞。
# 任何字詞都可以當作分界符
command << MARKER
…
${var}
$(cmd)
…
MARKER
如果文本里沒有內嵌變數替換操作,你可以把第一個MARKER用單引號包起來:
command << ‘MARKER’
…
no substitution is happening here.
$ (dollar sign) is passed through verbatim.
…
MARKER
內建變數
變數 說明
$0 指令碼名稱
$n 傳給指令碼/函式的第n個引數
$$ 指令碼的PID
$! 上一個被執行的命令的PID(後臺執行的行程)
$? 上一個命令的退出狀態(管道命令使用${PIPESTATUS})
$# 傳遞給指令碼/函式的引數個數
$@ 傳遞給指令碼/函式的所有引數(識別每個引數)
$* 傳遞給指令碼/函式的所有引數(把所有引數當成一個字串)
提示
使用$*很少是正確的選擇。
$@能夠處理空格引數,而且引數間的空格也能正確的處理。
使用$@時應該用雙引號括起來,像”$@”這樣。
除錯
對指令碼進行語法檢查:
bash -n myscript.sh
跟蹤腳本里每個命令的執行:
bash -v myscripts.sh
跟蹤腳本里每個命令的執行並附加擴充資訊:
bash -x myscript.sh
你可以在指令碼頭部使用set -o verbose和set -o xtrace來永久指定-v和-o。當在遠端機器上執行指令碼時,這樣做非常有用,用它來輸出遠端資訊。
什麼時候不應該使用bash指令碼
-
你的指令碼太長,多達幾百行
-
你需要比陣列更複雜的資料結構
-
出現了複雜的轉義問題
-
有太多的字串操作
-
不太需要呼叫其它程式和跟其它程式管道互動
-
擔心效能
這個時候,你應該考慮一種指令碼語言,比如Python或Ruby。
參考
-
Advanced Bash-Scripting Guide:?http://tldp.org/LDP/abs/html/
-
Bash Reference Manual
來自:外刊IT評論
連結:http://www.vaikan.com/bash-scripting/