最佳實踐

部分內容由 LLM 生成,尚未經過人工驗證。

Shell 腳本開發最佳實踐與程式碼風格指南。

Shebang 選擇

腳本第一行應指定解釋器:

#!/bin/bash        # Bash 專用特性
#!/bin/sh          # POSIX 相容(可移植性高)
#!/usr/bin/env bash # 使用 PATH 中的 bash(跨平台)

選擇建議:

  • 使用 Bash 特性(陣列、[[ ]])→ #!/bin/bash
  • 需要最大可移植性 → #!/bin/sh
  • 跨平台開發(macOS, Linux)→ #!/usr/bin/env bash

嚴格模式 (Strict Mode)

在腳本開頭啟用嚴格模式,提早發現錯誤:

#!/bin/bash
set -euo pipefail
IFS=$'\n\t'

選項說明

選項說明
set -e命令失敗時立即退出
set -u使用未定義變數時報錯
set -o pipefail管線中任一命令失敗即返回失敗
IFS=$'\n\t'設定欄位分隔符(避免空格問題)
# 範例:沒有 set -e
#!/bin/bash
rm non-existent-file    # 失敗但繼續執行
echo "Still running"    # 會執行

# 範例:有 set -e
#!/bin/bash
set -e
rm non-existent-file    # 失敗後立即退出
echo "Won't reach here" # 不會執行

選擇性禁用

set -e

# 允許特定命令失敗
command_that_may_fail || true

# 暫時禁用 set -e
set +e
risky_command
set -e

程式碼風格

命名慣例

# 常數 - 大寫加底線
readonly MAX_RETRIES=3
readonly CONFIG_FILE="/etc/app.conf"

# 環境變數 - 大寫
export PATH="/usr/local/bin:$PATH"
export DATABASE_URL="postgres://..."

# 局部變數 - 小寫加底線
local user_name="alice"
local file_count=0

# 函式 - 小寫加底線
get_user_info() {
    # ...
}

# 私有函式 - 前綴底線(慣例)
_internal_helper() {
    # ...
}

縮排與格式

# 使用 2 或 4 空格縮排(不使用 tab)
if [[ -f "$file" ]]; then
    echo "File exists"
    process_file "$file"
fi

# 長命令使用反斜線換行
command --option1 value1 \
        --option2 value2 \
        --option3 value3

# 管線換行對齊
cat file.txt \
    | grep "pattern" \
    | sort \
    | uniq -c

引號使用

# 變數使用雙引號
echo "Hello, $name"

# 避免詞分割(word splitting)
for file in "$@"; do  # 正確
    echo "$file"
done

for file in $@; do    # 錯誤:會被空格分割
    echo "$file"
done

# 單引號用於字面字串(不擴展)
echo 'Value is $var'  # 輸出: Value is $var

註解與文件

函式文件

#######################################
# 計算兩數之和
# Arguments:
#   $1 - 第一個數字
#   $2 - 第二個數字
# Returns:
#   0 - 成功
#   1 - 參數錯誤
# Outputs:
#   計算結果到 stdout
#######################################
add() {
    if [ $# -ne 2 ]; then
        echo "Usage: add num1 num2" >&2
        return 1
    fi
    echo $(($1 + $2))
}

腳本標頭

#!/bin/bash
#
# backup.sh - 自動備份腳本
#
# Description:
#   備份指定目錄到遠端伺服器
#
# Usage:
#   ./backup.sh <source_dir> <dest_server>
#
# Author: Your Name
# Date: 2024-01-15
#

set -euo pipefail

使用 ShellCheck 進行靜態分析

ShellCheck 是 Shell 腳本靜態分析工具,可發現常見錯誤與風格問題。

# 安裝
sudo apt install shellcheck  # Debian/Ubuntu
brew install shellcheck      # macOS

# 檢查腳本
shellcheck script.sh

# 指定 shell
shellcheck --shell=bash script.sh

# 忽略特定警告
shellcheck --exclude=SC2034 script.sh

常見警告範例

# SC2086: 變數應加引號
echo $var        # 警告
echo "$var"      # 正確

# SC2046: 命令替換應加引號
for f in $(ls); do  # 警告
for f in *; do      # 正確(使用 glob)

# SC2164: cd 應檢查失敗
cd /some/path      # 警告
cd /some/path || exit  # 正確

詳細資訊請參考 ShellCheck 工具說明

效能考量

避免過度 fork

# 不佳:每次迭代都 fork 新程序
for file in *.txt; do
    cat "$file" | grep "pattern"
done

# 較佳:使用 Shell 內建功能
for file in *.txt; do
    grep "pattern" "$file"
done

# 最佳:單次呼叫處理所有檔案
grep "pattern" *.txt

使用內建命令

# 外部命令(較慢)
dirname=$(dirname "$path")
basename=$(basename "$path")

# Shell 內建(較快)
dirname=${path%/*}
basename=${path##*/}

批次操作

# 不佳:逐一處理
for file in *.jpg; do
    convert "$file" "${file%.jpg}.png"
done

# 較佳:使用 xargs 並行
find . -name "*.jpg" -print0 | xargs -0 -P 4 -I {} convert {} {}.png

可移植性

POSIX 相容寫法

# Bash 專用
[[ $var == "value" ]]
array=("a" "b" "c")

# POSIX 相容
[ "$var" = "value" ]
# 使用空格分隔的字串代替陣列

檢測系統差異

# 檢測作業系統
case "$(uname -s)" in
    Linux*)     machine=Linux;;
    Darwin*)    machine=Mac;;
    CYGWIN*)    machine=Cygwin;;
    *)          machine="UNKNOWN"
esac

echo "Running on $machine"

安全考量

避免注入攻擊

# 危險:直接使用使用者輸入
rm -rf $user_input  # 若 user_input=/* 會刪除整個系統

# 安全:驗證輸入
if [[ $user_input =~ ^[a-zA-Z0-9_-]+$ ]]; then
    rm -rf "$user_input"
else
    echo "Invalid input" >&2
    exit 1
fi

臨時檔案安全

# 不安全
tmpfile="/tmp/myapp.$$"

# 安全:使用 mktemp
tmpfile=$(mktemp) || exit 1
trap "rm -f '$tmpfile'" EXIT

# 臨時目錄
tmpdir=$(mktemp -d) || exit 1
trap "rm -rf '$tmpdir'" EXIT

版本管理

# 腳本版本號
readonly VERSION="1.2.3"

# 顯示版本
show_version() {
    echo "$(basename "$0") version $VERSION"
}

# 命令列參數處理
while getopts "v" opt; do
    case $opt in
        v) show_version; exit 0 ;;
        *) echo "Usage: $0 [-v]" >&2; exit 1 ;;
    esac
done

相關主題