Bash 變數作用域與局部變數 文章首圖

Bash 變數作用域與局部變數

Bash 變數作用域與局部變數

在撰寫 Shell 腳本時,許多初學者常會遇到一個令人頭痛的現象:明明在函數內修改了變數,離開函數後卻發現值沒變,或者更糟糕的是,意外覆蓋了全域變數的值。這背後的核心概念就是「變數的作用域」(Variable Scope)。

對於有基礎 Linux 使用經驗的讀者來說,理解變數的生命週期與可見範圍,是撰寫穩定、可維護腳本的必要門檻。本文将深入探討 Bash 中的全域變數與局部變數,並透過實際範例展示如何正確管理變數空間。

全域變數 vs. 局部變數

在 Bash 中,預設情況下,所有在腳本頂層或函數內直接賦值的變數都是全域變數(Global Variables)。這意味著它們可以在腳本的任何地方被讀取或修改,包括其他函數中。這種特性雖然方便,但也容易導致命名衝突和難以追蹤的 Bug。

為了限制變數的可見範圍,我們可以使用 local 關鍵字來宣告局部變數(Local Variables)。局部變數僅在其被宣告的函數內部有效,函數執行完畢後,該變數即被銷毀,不會影響外部環境。

實際範例:未使用 local 的陷阱

讓我們先看一個典型的錯誤範例。假設我們寫了一個計算平方的函數,但忘記使用 local

#!/bin/bash

# 定義一個全域變數
result=10
counter=1

calculate_square() {
    local input=$1
    # 錯誤示範:這裡沒有使用 local,result 會變成全域變數
    result=$(( input * input ))
    counter=$(( counter + 1 ))
}

echo "呼叫函數前:"
echo "result: $result"
echo "counter: $counter"

# 呼叫函數
calculate_square 5

echo "呼叫函數後:"
echo "result: $result"
echo "counter: $counter"

執行這段腳本,你會發現 result 從 10 變成了 25,而 counter 也變成了 2。雖然在這個簡單例子中看起來沒問題,但如果 resultcounter 是腳本其他部分依賴的重要狀態,這種隱形的修改將導致難以預期的行為。

正確實踐:使用 local 宣告

現在,讓我們修正上面的範例,將內部變數宣告為 local

#!/bin/bash

# 定義一個全域變數
result=10
counter=1

calculate_square() {
    local input=$1
    # 正確示範:使用 local 宣告,僅在函數內有效
    local result=$(( input * input ))
    local counter=$(( counter + 1 ))

    echo "函數內部: result=$result, counter=$counter"
}

echo "呼叫函數前:"
echo "result: $result"
echo "counter: $counter"

# 呼叫函數
calculate_square 5

echo "呼叫函數後:"
echo "result: $result"
echo "counter: $counter"

執行修正後的腳本,你會看到:

呼叫函數前:
result: 10
counter: 1
函數內部: result=25, counter=2
呼叫函數後:
result: 10
counter: 1

可以看到,儘管函數內部 resultcounter 被修改了,但外部的全域變數完全不受影響。這就是局部變數帶來的安全性與可預測性。

進階技巧:遞迴函數中的局部變數

局部變數在遞迴函數中尤為重要。如果遞迴呼叫中不使用 local,所有層級的遞迴都會共享同一個變數空間,導致邏輯混亂。

以下是一個計算階乘的範例,展示如何在遞迴中安全地使用局部變數:

#!/bin/bash

factorial() {
    local n=$1

    # 基準情況
    if [ $n -le 1 ]; then
        echo 1
        return
    fi

    # 遞迴呼叫
    local sub_result
    sub_result=$(factorial $(( n - 1 )))

    # 計算當前層級結果
    echo $(( n * sub_result ))
}

# 測試
echo "5! = $(factorial 5)"
echo "10! = $(factorial 10)"

這段程式碼能正確輸出 1203628800,因為每一層遞迴呼叫都擁有獨立的 sub_result 變數空間。

常見問題

1. 在子殼層(Subshell)中修改變數無效嗎?

是的。當你使用 $(...)( ) 建立子殼層時,變數的修改僅限於子殼層內部,不會影響父殼層。這與 local 不同,local 是在同一個函數作用域內限制變數,而子殼層是完全隔離的進程。

#!/bin/bash
x=10

# 錯誤:這不會改變外部的 x
x=$(echo 20)

# 正確:使用 local 或在同一作用域修改
modify_var() {
    local x=20
    echo "函數內: $x"
}
modify_var
echo "函數外: $x"

2. 為什麼有時需要匯出變數(export)?

export 用於將變數傳遞給子進程(例如執行的外部命令),但它並不會改變變數的作用域規則。export 的變數仍然是全域的,只是子進程也能看到它。在腳本內部函數間傳遞數據,應優先使用 return(透過全域變數或標準輸出)或 local 變數,而非依賴 export

小結

掌握 Bash 變數作用域是提升腳本品質的關鍵。請記住以下黃金法則:

  1. 預設全域:未宣告的變數皆為全域,易產生副作用。
  2. 函數內必用 local:在函數內部使用的所有變數,除非刻意要修改全域狀態,否則一律使用 local 宣告。
  3. 遞迴必用 local:避免遞迴層級間的變數衝突。

透過養成良好的命名與宣告習慣,你的 Shell 腳本將更加健壯、易於除錯,也更容易被他人理解與維護。