Shell腳本最佳實踐
設置編碼、縮進、文件命名和執行權限
使用utf-8編碼;
統一使用tab縮進或空格縮進,不要混用;
文件名以.sh
結尾,並且統一風格;
添加可執行權限:
chmod +x [bash_script.sh]
最后,在所有輸出完畢后,添加一個空行。
指定默認解釋器
也就是不要省略腳本第一行的shebang,一般默認是bash:
#!/bin/bash
或者更為通用一些:
#!/usr/bin/env bash
本機可用的shell解釋器,可以通過以下命令查看:
cat /etc/shells
設置Shell環境
設置命令回顯:
set -x
shell默認設置不夠友好,我們希望予以加強。
# 遇到未聲明的變量則報錯停止
set -u
# 遇到執行錯誤則停止
set -e
由於set -e
對管道命令無效,管道命令其中一步失敗則中止,需要使用:
set -o pipefail
我們將這三條合並,構成 bash strict mode,添加在bash腳本的開始位置:
set -euo pipefail
因為這里都是shell環境設置,所以也可以在執行腳本的時候來使用:
bash -euo pipefail [bash_sctipt.sh]
總是使用main函數包裹執行體
main() {
func1 param1 param2
func2 param
}
main "$@"
與python類似,shell不需要函數入口,可以從第一條指令開始執行。但是為了可讀性和方便調試,我們總是寫一個命名為main的函數來作為全局入口。
變量
1)環境變量的設置和取消:
# 設置環境變量
export SKIP_BFS=1
# 取消環境變量
unset SKIP_BFS
注意,由於前文啟用了strict mode,受set -u
影響,腳本中使用未設置的環境變量,會報unbound variable
錯誤。
可以通過-v
來檢測是否設置了環境變量:
if [[ -v SKIP_BFS ]]; then
echo 'environment variable SKIP_BFS is set'
fi
2)局部變量
shell變量默認全局作用域,這一點與JavaScript類似,函數內聲明局部變量,應該添加local
關鍵字。
3)使用變量時,總是用花括號和雙引號把變量包起來,例如:
# 帶空格的路徑
cp -r "${src_dir}" "${dest_dir}"
不適用雙引號包裹變量的話,路徑有空格會被作為兩個參數來處理,從而導致很嚴重的bug,用"$var"
這種寫法,避免了這個問題。
花括號則是避免避免變量名和下划線的拼接處出現歧義的問題。
條件判斷
字符比較和文件測試使用雙方括號 [[ ]]
,並在每個變量和運算符以及和括號之間加入一個空格,例如:
if [[ $# > 1 ]] || [[ $# == 1 && $1 != 'PC' && $1 != 'server' ]]; then
echo 'Invalid commandline arguments, you should use `./run.sh` or `./run.sh PC` or `./run.sh server`'
exit 1
fi
其中,$#
用於獲取命令行參數個數,$N
用於獲取第N個命令行參數,參數$0
指的是腳本文件名。
相比單方括號,雙方括號的優勢在於可以直接使用比較運算符>
<
==
!=
等,而不是必須使用-gt
-lt
-eq
-ne
;此外雙方括號可以使用&&
||
來表達與和或,而不用必須寫-a
-o
這種難以記憶的寫法,並且擁有邏輯短路的功能。因此,強烈建議使用雙方括號取代單方括號作為作為條件判斷語句。
數字的比較應該使用雙小括號 (( ))
,並且不需要空格分隔各值和運算符。例如判斷正在運行的進程個數:
running=$(ps -aux -r | wc -l)
if (( ${running} > 5 )); then
echo "${running} processes running, please handle this problem. exit."
exit 1;
fi
在雙方括號中進行數字的比較也是可以的,但是直接使用比較運算符>
<
==
!=
等得到的常常是錯誤的結果,使用-gt
-lt
-eq
-ne
得到的總是正確的但難以記憶。使用雙小括號則可以直接使用比較運算符進行判斷。
使用文件前做好異常處理
# 判斷文件夾存在
if [[ ! -d 'src' ]]; then
echo 'src dir not found'
exit 1
fi
# 判斷普通文件存在
if [[ ! -f 'a.txt' ]]; then
touch 'a.txt'
fi
# 判斷可執行文件存在並且可執行
if [[ ! -x "$(command -v java)" ]]; then
echo 'java is not installed, or not execuatable'
fi
注意cp -r
命令,在文件夾不存在時回創建文件夾並復制,而當文件夾存在時,會復制到子文件夾內。
循環語句
提倡使用for-in循環
# C風格
for (( i=0; i<10; i++)); do
// echo $i
done
# for-in
for i in $(seq 0 9); do
// echo $i
和 if 語句的 then 一樣,for 語句的 do 也緊跟在語句后面,不單獨占一行,這樣顯得比較緊湊。同樣不要忘記加分號。
這里補充說明一下seq
語句用法,注意與python做好區分:
# 單參數,輸出 1 2 3 4
$(seq 4)
# 雙參數,輸出 2 3 4 5
$(seq 2 5)
# 三參數,輸出 8 6 4 2
$(seq 8 -2 1)
這里三參數情況時的增量參數,可以正可以負,也可以是小數。
更多用法可參考Bash Range: How to iterate over sequences generated on the shell
用 ${arr[@]}
和 ${arr[*]}
進行列表循環
$*
與 $@
的相同點都是引用所有參數;不同點則只有在雙引號中體現出來。假設在腳本運行時寫了三個參數 1、2、3,,則 $*
等價於 "1 2 3"(傳遞了一個參數),而 $@
等價於 "1" "2" "3"(傳遞了三個參數)
單個列表元素迭代:
arr=(1 3 5 a)
for s in ${arr[@]}; do
echo $s
done
多個列表合並迭代:
arr1=(1 3 5 a)
arr2=(2 4 6 b)
for s in ${arr1[@]} ${arr2[@]}; do
echo $s
done
注意,花括號不可省略。
如果需要進行函數傳參,則需要使用使用 $*
,並且在傳參時使用雙引號 "
把列表參數包起來作為一個參數整體傳入。示例如下:
deltas="0.1 0.2 0.3"
run() {
params=$1
for x in ${params[*]}; do
echo $x
done
}
run "${deltas[*]}"
注意,函數內參數列表不能用引號,函數調用處引號不可少。原因在於,函數外是要把列表作為一個參數整體傳入(而不是分成多個參數傳入),函數內是把列表拆成多個元素依次遍歷。
有時候,我們希望對列表中的元素整體加前綴或者加后綴,在Makefile里可以很方便地調用addprefix
和addsuffix
兩個內置函數來完成,在bash里則需要使用Bash parameter expansion:
# addprefix
for s in ${arr[@]/#/PREFIX}; do
echo $s
done
# addsuffix
arr_suffix=${arr[@]/%/SUFFIX}
echo ${arr_suffix}
使用$()
而不是反引號獲取表達式的值
如for-in:
# 建議使用 $(seq lb ub) 而不是 `seq lb ub` 獲取范圍
for i in $(seq 0 10) do
echo $i
done
使用(())
和bc
進行數學運算
shell默認的都是文本操作,所以a=$b+$c
並不能把兩個數進行求和,需要數學運算的話,應該明確標明。
分兩種情況,整數運算和浮點運算:
整數運算建議使用(())
,不建議適用[]
、let
、expr
:
(( a = $b + $c ))
# 或者
a=$( b + c ))
浮點運算可以用bc:
echo "$b + $c" | bc
# 或者
bc <<< "$b + $c"
使用 /dev/null
過濾輸出信息
[expr] > /dev/null 2>&1
命令解釋:重定向到空設備,並把標准錯誤輸出stderr也重定向為stdout。
注意,2>&1
應該總是放在命令的末尾。
獲取腳本所在目錄
有時候,需要適用腳本對同一份代碼倉庫下其他文件夾內的文件進行操作,如codegen、format、validate等工作。此時需要的是相對本腳本的路徑,與調用腳本時的路徑無關,所以需要先行獲取腳本所在路徑(絕對路徑):
# 在 `$()` 里面執行 cd 命令不會改變當前工作路徑
readonly __DIR__=$(cd $(dirname $0) && pwd)
echo $__DIR__
參考https://john-yuan.org/blog/how-to-get-the-dir-of-the-current-shell-script.html
case語句等
TBD