Shell腳本最佳實踐


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里可以很方便地調用addprefixaddsuffix兩個內置函數來完成,在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並不能把兩個數進行求和,需要數學運算的話,應該明確標明。
分兩種情況,整數運算和浮點運算:
整數運算建議使用(()),不建議適用[]letexpr:

(( 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

進一步閱讀

Google Bash風格指南
阮一峰 Bash 腳本教程


免責聲明!

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



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