Bash 可攜性
撰寫可攜式 POSIX 相容腳本,並了解何時善用 Bash 特定功能的指南。
Shebang 選擇
Bash 腳本使用 #!/usr/bin/env bash
#!/usr/bin/env bash
原因: 搜尋 PATH 中的 bash,可在 bash 位於不同位置的系統上運作。
POSIX 腳本使用 #!/bin/sh
#!/bin/sh
原因: 當不需要 Bash 功能時,達到最大可攜性。在許多系統上,/bin/sh 是 dash 或其他 POSIX Shell。
必要時使用絕對路徑
#!/bin/bash
僅在以下情況使用: 系統需求保證 bash 的位置,或安全政策要求絕對路徑。
POSIX 與 Bash 功能對照表
| 功能 | POSIX | Bash | 建議 |
|---|---|---|---|
[[ ]] | 否 | 是 | POSIX 請用 [ ] |
(( )) | 否 | 是 | POSIX 請用 [ ] 搭配 -eq 等 |
| 陣列 | 否 | 是 | 改用位置參數或檔案 |
local | 部分 | 是 | 通常安全 |
${var,,} | 否 | 4+ | POSIX 請用 tr |
<<< | 否 | 是 | 使用 `echo |
=~ 正則表達式 | 否 | 是 | 使用 grep 或 expr |
source | 否 | 是 | 使用 .(點)命令 |
function f() | 否 | 是 | 僅使用 f() |
$'...' | 否 | 是 | 使用 printf |
{1..10} | 否 | 是 | 使用 seq 或 while 迴圈 |
POSIX 相容模式
條件判斷
# POSIX - 使用 [ ] 並正確引號
if [ -f "$file" ]; then
echo "檔案存在"
fi
# 字串比較
if [ "$var" = "value" ]; then
echo "符合"
fi
# 數字比較
if [ "$num" -gt 10 ]; then
echo "更大"
fi
# 複合條件
if [ -f "$file" ] && [ -r "$file" ]; then
echo "可讀取的檔案"
fi
大小寫轉換(POSIX)
# 轉小寫
lower=$(echo "$string" | tr '[:upper:]' '[:lower:]')
# 轉大寫
upper=$(echo "$string" | tr '[:lower:]' '[:upper:]')
子字串操作(POSIX)
# 取得子字串 - 使用 expr 或 cut
substr=$(expr "$string" : '.\(.\)') # 第4-8個字元
substr=$(echo "$string" | cut -c4-8)
# 字串長度
length=$(expr length "$string")
length=${#string} # 這其實是 POSIX 的
讀取檔案(POSIX)
# 逐行讀取
while IFS= read -r line; do
echo "$line"
done < "$file"
# 讀取整個檔案(不使用 cat)
content=$(cat "$file") # cat 是 POSIX 的
命令替換
# 現代語法(即使在 POSIX 中也較佳)
result=$(command)
# 傳統語法(避免使用)
result=`command`
# 巢狀(為何現代語法更好)
result=$(echo $(date)) # 清晰
result=`echo \`date\`` # 跳脫噩夢
值得使用的 Bash 特有功能
當可攜性不是必要條件時,這些 Bash 功能可提升程式碼品質:
擴展測試 [[ ]]
# 模式比對
[[ "$file" == *.txt ]]
# 正則表達式比對
[[ "$input" =~ ^[0-9]+$ ]]
# 不用擔心單詞分割
[[ -f $file ]] # 引號可選(但仍建議使用)
# 內部邏輯運算子
[[ -f "$file" && -r "$file" ]]
陣列
# 索引陣列
declare -a files=()
files+=("one.txt")
files+=("two.txt")
for f in "${files[@]}"; do
process "$f"
done
# 關聯陣列(Bash 4+)
declare -A config
config[host]="localhost"
config[port]="8080"
參數展開
# 預設值
"${var:-default}"
# 大小寫轉換(Bash 4+)
"${var,,}" # 小寫
"${var^^}" # 大寫
# 子字串
"${var:0:10}" # 前10個字元
"${var: -5}" # 後5個字元
# 搜尋/取代
"${var//old/new}"
Here Strings
# Bash
read -r var <<< "input string"
# POSIX 等價寫法
var=$(echo "input string")
Process Substitution
# Bash - 比較兩個命令的輸出
diff <(sort file1) <(sort file2)
# POSIX 等價寫法(使用暫存檔)
sort file1 > /tmp/sorted1
sort file2 > /tmp/sorted2
diff /tmp/sorted1 /tmp/sorted2
偵測 Shell 類型
# 檢查是否在 Bash 中執行
if [ -n "${BASH_VERSION:-}" ]; then
echo "正在 Bash 中執行"
fi
# 檢查 Bash 版本以使用特定功能
if [ "${BASH_VERSINFO[0]:-0}" -ge 4 ]; then
echo "可使用 Bash 4+"
fi
# 通用 Shell 偵測
case "${SHELL##*/}" in
bash) echo "bash" ;;
zsh) echo "zsh" ;;
*) echo "其他" ;;
esac
可攜式工具函式
可攜性決策指南
使用 POSIX 當:
- 腳本在最小系統(容器、嵌入式)上執行
- 目標包含 dash、ash 或 busybox sh
- 需要最大相容性
- 腳本是系統初始化的一部分
使用 Bash 當:
- 目標系統保證有 bash
- 需要陣列、關聯陣列或正則表達式
- 需要複雜的字串操作
- 程式碼清晰度顯著提升
- 需要互動式功能
常見可攜性陷阱
echo 與 printf
# 有問題 - 行為因系統而異
echo -n "不換行"
echo -e "帶\t定位字元"
# 可攜式
printf '%s' "不換行"
printf '帶\t定位字元\n'
變數賦值
# 所有地方都可運作
var="value"
# 在某些 Shell 上可能失敗
var = "value" # 等號周圍的空格是錯誤的
Export 賦值
# POSIX - 分開的命令
var="value"
export var
# Bash/現代 - 合併(大部分地方可運作)
export var="value"
不使用陣列的類陣列操作
# 使用位置參數
set -- "item1" "item2" "item3"
for item in "$@"; do
echo "$item"
done
# 或使用 IFS 分割
items="item1:item2:item3"
IFS=':' read -r item1 item2 item3 <<EOF
$items
EOF