錯誤處理

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

Shell 腳本錯誤處理、除錯技巧與最佳實踐。

Exit Codes 與 $?

每個命令執行後都會返回退出碼(exit code):

  • 0 - 成功
  • 1-255 - 失敗(不同數字代表不同錯誤)
# 檢查上一個命令的退出碼
ls /tmp
echo $?  # 0(成功)

ls /non-existent
echo $?  # 2(檔案不存在)

自訂退出碼

#!/bin/bash

# 定義退出碼常數
readonly E_SUCCESS=0
readonly E_NOFILE=1
readonly E_PERMISSION=2
readonly E_INVALID_ARG=3

check_file() {
    local file=$1

    if [ $# -eq 0 ]; then
        echo "Error: No file specified" >&2
        exit $E_INVALID_ARG
    fi

    if [ ! -e "$file" ]; then
        echo "Error: File not found: $file" >&2
        exit $E_NOFILE
    fi

    if [ ! -r "$file" ]; then
        echo "Error: Permission denied: $file" >&2
        exit $E_PERMISSION
    fi

    exit $E_SUCCESS
}

check_file "$1"

使用退出碼進行流程控制

# if 語句
if command; then
    echo "Success"
else
    echo "Failed"
fi

# && 和 ||
command1 && command2  # command2 只在 command1 成功時執行
command1 || command2  # command2 只在 command1 失敗時執行

# 鏈式操作
mkdir /tmp/test && cd /tmp/test && touch file.txt || echo "Failed"

set -e 與 set -o pipefail

set -e

命令失敗時立即退出腳本:

#!/bin/bash
set -e

echo "Step 1"
false           # 這會導致腳本退出
echo "Step 2"   # 不會執行

set -e 的例外情況

set -e

# 在條件判斷中不會退出
if false; then
    echo "Won't print"
fi

# 在 while/until 條件中不會退出
while false; do
    echo "Won't loop"
done

# 使用 || 或 && 時不會退出
false || echo "Handled"

# 在管線中只檢查最後一個命令
false | true  # 不會退出

set -o pipefail

檢查管線中所有命令的退出碼:

#!/bin/bash
set -e
set -o pipefail

# 沒有 pipefail
false | true  # 不會退出(只檢查 true)

# 有 pipefail
false | true  # 會退出(檢查 false)

組合使用

#!/bin/bash
set -euo pipefail

# 安全的管線處理
cat file.txt | grep "pattern" | sort | uniq
# 任一命令失敗都會導致腳本退出

trap 命令

trap 用於捕捉信號和錯誤,執行清理操作:

基本語法

trap 'commands' SIGNAL [SIGNAL...]

常見信號

信號說明
EXIT腳本退出時(正常或異常)
ERR命令失敗時(需 set -e
INTCtrl+C(SIGINT)
TERM終止信號(SIGTERM)
HUP終端關閉(SIGHUP)

清理臨時檔案

#!/bin/bash
set -e

tmpfile=$(mktemp)

# 設定 trap 確保臨時檔案被刪除
trap "rm -f '$tmpfile'" EXIT

# 使用臨時檔案
echo "data" > "$tmpfile"
# ... 處理 ...

# EXIT 時自動執行 rm -f '$tmpfile'

多個清理操作

#!/bin/bash

cleanup() {
    echo "Cleaning up..." >&2
    rm -f /tmp/lockfile
    kill $background_pid 2>/dev/null || true
    # 其他清理操作
}

trap cleanup EXIT

# 腳本內容
touch /tmp/lockfile
some_command &
background_pid=$!

# ... 執行任務 ...

捕捉錯誤

#!/bin/bash
set -e

error_handler() {
    local line=$1
    echo "Error occurred in script at line: $line" >&2
    # 記錄錯誤、發送通知等
    exit 1
}

trap 'error_handler $LINENO' ERR

# 腳本內容
command1
command2

忽略信號

# 忽略 Ctrl+C
trap '' INT

# 睡眠期間無法被 Ctrl+C 中斷
sleep 10

# 恢復預設行為
trap - INT

組合範例

#!/bin/bash
set -euo pipefail

# 全域變數
tmpdir=""
logfile="/var/log/script.log"

# 清理函式
cleanup() {
    local exit_code=$?
    if [ -n "$tmpdir" ] && [ -d "$tmpdir" ]; then
        rm -rf "$tmpdir"
    fi
    echo "Script exited with code: $exit_code" >> "$logfile"
    exit $exit_code
}

# 錯誤處理
error_handler() {
    echo "Error at line $1" >&2
    cleanup
}

# 設定 trap
trap cleanup EXIT
trap 'error_handler $LINENO' ERR
trap 'echo "Interrupted"; exit 130' INT

# 建立臨時目錄
tmpdir=$(mktemp -d)

# 腳本主要邏輯
echo "Working in $tmpdir"
# ...

錯誤訊息輸出

使用 stderr

# 錯誤訊息應輸出到 stderr(檔案描述符 2)
echo "Error: File not found" >&2

# 或使用函式
error() {
    echo "ERROR: $*" >&2
}

error "Something went wrong"

訊息等級

# 定義訊息函式
log() {
    echo "[$(date +'%Y-%m-%d %H:%M:%S')] $*"
}

info() {
    echo "[INFO] $*"
}

warn() {
    echo "[WARN] $*" >&2
}

error() {
    echo "[ERROR] $*" >&2
}

fatal() {
    echo "[FATAL] $*" >&2
    exit 1
}

# 使用
info "Starting process..."
warn "Configuration file not found, using defaults"
error "Failed to connect to database"
fatal "Critical error, cannot continue"

除錯技巧

set -x

輸出每個執行的命令(帶擴展):

#!/bin/bash
set -x  # 啟用除錯輸出

name="Alice"
echo "Hello, $name"
# 輸出: + echo 'Hello, Alice'
#       Hello, Alice

set +x  # 禁用除錯輸出

PS4 變數

自訂除錯輸出格式:

#!/bin/bash

# 預設 PS4='+ '
set -x
echo "test"
# 輸出: + echo test

# 自訂 PS4
export PS4='+(${BASH_SOURCE}:${LINENO}): ${FUNCNAME[0]:+${FUNCNAME[0]}(): }'
set -x
echo "test"
# 輸出: +(script.sh:10): echo test

條件除錯

#!/bin/bash

# 使用環境變數控制除錯
if [ "${DEBUG:-}" = "1" ]; then
    set -x
fi

# 使用
# DEBUG=1 ./script.sh

函式追蹤

#!/bin/bash

# 啟用函式追蹤
set -o functrace

trace() {
    echo "Entering ${FUNCNAME[1]} (called from ${FUNCNAME[2]})" >&2
}

func1() {
    trace
    echo "In func1"
}

func2() {
    trace
    func1
}

func2

使用 DEBUG trap

#!/bin/bash

# 每個命令執行前觸發
trap 'echo "Line $LINENO: $BASH_COMMAND"' DEBUG

x=1
y=2
z=$((x + y))
echo $z

最佳實踐

1. 輸入驗證

validate_input() {
    local input=$1

    if [ -z "$input" ]; then
        error "Input cannot be empty"
        return 1
    fi

    if [[ ! $input =~ ^[0-9]+$ ]]; then
        error "Input must be a number"
        return 1
    fi

    return 0
}

read -p "Enter a number: " num
if validate_input "$num"; then
    echo "Valid input: $num"
else
    exit 1
fi

2. 依賴檢查

check_dependencies() {
    local missing=()

    for cmd in "$@"; do
        if ! command -v "$cmd" &> /dev/null; then
            missing+=("$cmd")
        fi
    done

    if [ ${#missing[@]} -gt 0 ]; then
        error "Missing dependencies: ${missing[*]}"
        return 1
    fi
}

check_dependencies git curl jq || exit 1

3. 完整的錯誤處理範例

#!/bin/bash
set -euo pipefail

# 常數定義
readonly SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
readonly SCRIPT_NAME="$(basename "$0")"

# 錯誤處理
error_exit() {
    echo "${SCRIPT_NAME}: error: $*" >&2
    exit 1
}

# 清理
cleanup() {
    local exit_code=$?
    # 清理操作
    exit $exit_code
}

trap cleanup EXIT

# 主程式
main() {
    # 檢查參數
    if [ $# -eq 0 ]; then
        error_exit "No arguments provided"
    fi

    # 檢查依賴
    command -v jq &> /dev/null || error_exit "jq is not installed"

    # 執行任務
    # ...

    return 0
}

main "$@"

相關主題