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。雖然在這個簡單例子中看起來沒問題,但如果 result 或 counter 是腳本其他部分依賴的重要狀態,這種隱形的修改將導致難以預期的行為。
正確實踐:使用 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
可以看到,儘管函數內部 result 和 counter 被修改了,但外部的全域變數完全不受影響。這就是局部變數帶來的安全性與可預測性。
進階技巧:遞迴函數中的局部變數
局部變數在遞迴函數中尤為重要。如果遞迴呼叫中不使用 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)"
這段程式碼能正確輸出 120 和 3628800,因為每一層遞迴呼叫都擁有獨立的 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 變數作用域是提升腳本品質的關鍵。請記住以下黃金法則:
- 預設全域:未宣告的變數皆為全域,易產生副作用。
- 函數內必用 local:在函數內部使用的所有變數,除非刻意要修改全域狀態,否則一律使用
local宣告。 - 遞迴必用 local:避免遞迴層級間的變數衝突。
透過養成良好的命名與宣告習慣,你的 Shell 腳本將更加健壯、易於除錯,也更容易被他人理解與維護。