Bash 同時是互動式命令語言與腳本執行環境。可靠的 Shell 程式碼會將字組、結束狀態與檔案描述元視為不同的資料流。需要 Bash 功能時,請以明確的直譯器啟動腳本:
#!/usr/bin/env bashbash -c 'commands' name arg1 會將 name 指派給 $0,將 arg1 指派給 $1。當命令字串使用位置參數時,這項細節很重要。
Shell 文法與執行 #
清單使用 ;、&、&& 或 || 連接管線。&& 與 || 是短路運算子;& 會啟動非同步清單。管線使用 | 連接命令;當左側命令的標準錯誤也傳送至管線時,使用 |&。
build && test && deploy
generate_report 2>&1 | tee build.log括號會建立子 Shell,因此其中的指派、目錄變更與選項不會影響呼叫端 Shell。大括號在目前 Shell 中執行,需要空格,以及結尾分號或換行。
( cd build && make )
{ cd build && make; }依預設,管線中的命令會在不同環境執行。因此,不要預期管線右側 while 迴圈所指派的變數在之後仍保持設定。請改用重新導向或程序替換將資料送入迴圈:
count=0
while IFS= read -r line; do
((count++))
done < <(printf '%s\n' one two)
printf '%s\n' "$count"shopt -s lastpipe 在特定非互動且停用工作控制的條件下,會變更最後一個管線命令的行為;程式碼不應依賴這種選用的執行模型。
條件、迴圈與函式 #
Shell 條件是命令狀態:零表示成功,非零表示失敗。if、while 與 until 接受清單,而不是布林運算式。
if [[ -d $directory && -r $directory/config ]]; then
printf 'configuration is readable\n'
elif [[ -e $directory ]]; then
printf 'configuration is unavailable\n' >&2
else
printf 'directory does not exist\n' >&2
fiBash 條件式使用 [[ ... ]],因為它避免未加引號運算元的字組分割與路徑名稱展開。算術使用 (( ... )),萬用字元模式分派使用 case。即使外圍條件不要求,也要在命令中為變數展開加上引號。
for file in "$@"; do
[[ -f $file ]] || continue
process "$file"
done
retry=0
until fetch; do
((++retry >= 3)) && exit 1
done函式在目前 Shell 中執行。local 將變數限制在目前函式;return 提供其狀態。引數是函式的位置參數,因此使用 "$@" 轉送。
run_checked() {
local output
output=$("$@") || return
printf '%s\n' "$output"
}展開與引號 #
命令執行前,Bash 會執行大括號展開、波浪號展開、參數/算術/命令/程序替換、字組分割、路徑名稱展開與移除引號。參數、算術、命令與程序替換由左至右發生。大括號展開早於參數展開,因此不能使用變數建立範圍。
引號會改變此流程:
- 單引號會保留每個字元,直到下一個單引號。
- 雙引號保留一個字組,但允許參數、算術與命令替換。
- 未加引號的展開會經歷字組分割與路徑名稱展開。
"$@"每個位置參數展開為一個字組;"$*"展開為一個以IFS第一個字元連接的字組。
files=("$directory"/*.log)
printf '%s\n' "${files[@]}"
name=${path##*/} # Remove the longest matching prefix.
extension=${name##*.}
printf '%s\n' "${name%.*}" # Remove the shortest matching suffix.
printf '%s\n' "$((retries + 1))"未匹配的未加引號萬用字元依預設會保持原樣。只有在需要空結果時,才在嚴格限定範圍的情境設定 nullglob;failglob 會將未匹配模式轉為錯誤。絕不要使用 for file in $(command) 剖析任意檔名,因為分割與萬用字元展開會破壞檔名。
陣列、算術與正規表示式 #
只有以 "${array[@]}" 展開時,索引與關聯陣列才會保留元素邊界。指派字串鍵前,先宣告關聯陣列。
declare -a files=(one 'two words')
declare -A status=([build]=pending)
files+=("three words")
for file in "${files[@]}"; do
printf '<%s>\n' "$file"
done算術接受 Shell 變數,當評估值非零時回傳零狀態。如果輸入可能有前導零,請在十進位數字前加上 10#;否則 Bash 可能將前導零解讀為八進位。
number=008
(( value = 10#$number + 1 ))[[ value =~ regex ]] 使用 POSIX 擴充正規表示式。將非字面模式保留在變數中,並讓該展開保持未加引號;加引號會改變匹配語意。成功匹配後,BASH_REMATCH[0] 包含完整匹配,後續元素包含擷取群組。
version='release-1.2.3'
regex='^release-([0-9]+)\.([0-9]+)\.([0-9]+)$'
if [[ $version =~ $regex ]]; then
printf 'major=%s\n' "${BASH_REMATCH[1]}"
fi重新導向與輸入 #
管線連線建立後,重新導向由左至右套用。2>&1 會複製標準輸出的目前目的地,因此其位置會改變結果。
command >output.log 2>&1 # Both standard streams go to the file.
command 2>&1 >output.log # Standard error retains its prior destination.
command |& consumer # Equivalent to command 2>&1 | consumer.當命令使用標準輸入而迴圈也需要輸入時,開啟專用描述元:
while IFS= read -r line <&3; do
printf '%s\n' "$line"
done 3< input.txtread -r 防止反斜線解讀。沒有變數時,read 將資料指派給 REPLY,並移除輸入分隔符。mapfile(也稱為 readarray)將行讀入索引陣列,除非使用 -t,否則會保留分隔符。
mapfile -t lines < input.txt
printf '%s\n' "${lines[@]}"未加引號的 here-document 分隔符允許參數、命令與算術展開。加引號的分隔符會讓本文保持字面值。<<- 會移除前導 tab 字元,不會移除空格。
cat <<'EOF'
literal $HOME and $(command)
EOF啟動檔、提示與引數 #
互動式登入 Bash 會讀取 /etc/profile,接著讀取 ~/.bash_profile、~/.bash_login 與 ~/.profile 中第一個可讀檔案。互動式非登入 Bash 會讀取 ~/.bashrc。發行版檔案可能加入全系統啟動檔,因此請使用本機文件,而不要假設路徑。當設定 BASH_ENV 時,非互動 Bash 會讀取該變數命名的檔案。
將互動式別名、完成與提示設定放在 ~/.bashrc。當互動式登入 Shell 應共用該設定時,登入設定檔通常會載入它。只有子程序需要時,才在登入啟動檔匯出環境變數。
PS1 控制主要互動提示。將不列印的終端機序列包在 \[ 與 \] 中;否則 readline 會錯誤計算可見提示寬度。
PS1='\[\e[32m\]\u@\h \W\[\e[0m\]\$ '$0 命名 Shell 或腳本,$# 是引數數量,而 $1 到 ${10} 選取引數。使用 "$@" 保留引數邊界;只有刻意消耗目前引數後才使用 shift。
失敗語意與選項 #
除非啟用 set -o pipefail,否則管線狀態是最後一個命令的狀態;啟用後,只要任何命令失敗就會回報非零狀態。PIPESTATUS 會在管線後立即保存每個命令的狀態。
set -o pipefail
producer | consumer
printf 'statuses: %s\n' "${PIPESTATUS[*]}"set -e 不代表「每個錯誤都退出」。Bash 會在條件測試、&& 與 || 清單中除最後命令以外的命令、以 ! 反轉狀態的命令,以及數種複合命令情境中抑制其效果。因此,請在重要位置撰寫錯誤處理:
if ! deploy "$environment"; then
printf 'deployment failed\n' >&2
exit 1
fiset -u 會偵測未設定變數的展開,但選用輸入需要 ${optional:-} 等明確預設值。set -x 會暴露展開後的命令,可能洩漏祕密,因此只在受控診斷中啟用。只有當腳本理解每個選項的語意時,才使用 set -euo pipefail;它是明確驗證與清理的補充,而不是替代品。
評論
在 GitHub 上閱讀或評論(發表評論需要登入)