許多 Bash 腳本是用嚴格的命令式風格寫成的:先執行一個指令,再執行另一個指令,依此類推。有時候,你會需要把一連串概念上共同達成單一目的的指令群組在一起。這正是_函式_派上用場的地方。
你可以像這樣定義一個函式:
my_function () {
COMMANDS
}
空的括號只是表示你正在定義一個函式,裡面什麼都不用放。
函式一旦定義好,就和任何其他指令(無論是不是內建)沒有兩樣。就像任何指令一樣,你可以把_引數_傳給函式。在函式內部,你可以用_位置參數_$1、$2等來存取這些引數。(還記得嗎?我們在變數概念裡學過位置參數。)
特殊參數$0在函式內部不會改變,它仍然是正在執行的那個腳本的名稱。目前正在執行的函式可以用$FUNCNAME變數取得自己的名稱。
請參閱手冊中的 3.4.2 特殊參數。
你可以在函式內部定義變數。如果你用local指令宣告變數,這個變數的_作用域_就只限於目前的函式(以及它所呼叫的任何函式)。否則,這個變數會被放進_全域作用域_。
區域變數可以和全域變數同名。這種情況下,區域變數會「遮蔽」全域變數。舉例來說,在函式中宣告的區域變數會把同名的全域變數藏起來:參照和指定都指向區域變數,全域變數則保持不變。當函式返回後,全域變數又會再次現身。
x=5
myfunc () {
local x=100
echo "in my function, $x == 100"
}
echo "in the global scope, $x == 5"
myfunc
echo "back in the global scope, $x == 5"
輸出結果是
in the global scope, 5 == 5
in my function, 100 == 100
back in the global scope, 5 == 5
在函式內部,你可以存取_呼叫者_作用域裡的變數。也就是說,你可以使用全域變數,也可以使用在呼叫者(或呼叫呼叫者的某個函式)裡宣告的區域變數。
嚴格說來,「全域」這個詞用得並不精確。要在函式中展開一個變數,Bash 會沿著呼叫堆疊往上追溯,一路找到全域作用域,設法找出宣告過該變數名稱的函式。
這個範例改寫自手冊的 Shell 函式 一節:
func1() {
local var='func1 local'
func2
}
func2() {
echo "In func2, var = $var"
}
var=global
func1
func2
輸出結果是:
In func2, var = func1 local
In func2, var = global
同樣地,把值_指定_給變數時,會_在該變數被宣告的作用域裡_完成指定。這種「遠距作用」可能寫出難以追蹤的程式碼,因為變數究竟在哪裡被指定了值,並不總是那麼明顯。
你可以用FUNCNAME陣列變數來檢視呼叫堆疊。
函式和任何指令一樣,都有_結束狀態_。預設情況下,函式的狀態就是_最後一個執行過的指令_的結束狀態。
你可以用return指令,讓函式以特定的結束狀態返回。
check_password () {
if [[ $1 == "secret" ]]; then
return 0
else
return 1
fi
}
read -sp "Enter your password: " pass
if check_password "$pass"; then
echo "Correct!"
else
echo "Wrong password."
fi
不帶引數使用return時,會回傳最後一個執行過的指令的狀態。
注意,check_password函式可以簡化成:
check_password () { [[ $1 == "secret" ]]; }
[[...]]條件建構有結束狀態:0代表「true」,1代表「false」。{...}群組建構在結尾大括號之前,必須有一個換行或一個分號。函式的回傳狀態只是一個數字。那函式要怎麼產生輸出呢?
你的函式可以印到標準輸出。用大家熟悉的_指令替換_把它捕捉下來:
d6 () { echo "$(( 1 + RANDOM % 6 ))"; }
die=$( d6 )
echo "You rolled a $die."
即使你正在捕捉輸出,也一樣可以使用函式的結束狀態。
roll () {
local n=$1
if (( 4 <= n && n <= 20 )); then
echo "$(( 1 + RANDOM % n ))" # exit status is 0
else
return 1
fi
}
read -p "How many faces does your die have? " faces
if die=$( roll "$faces" ); then
echo "You rolled a $die."
else
echo "I can't roll a die with $faces faces."
fi
函式可以遞迴地呼叫自己。預設情況下,遞迴深度沒有限制。
舉個例子:
fibonacci() {
local n=$1
if (( n <= 1 )); then
echo "1"
else
local a=$(fibonacci "$(( n - 1 ))")
local b=$(fibonacci "$(( n - 2 ))")
echo "$(( a + b ))"
fi
}
for i in {1..10}; do fibonacci "$i"; done
# => 1
# => 2
# => 3
# => 5
# => 8
# => 13
# => 21
# => 34
# => 55
# => 89
遞迴深度可以用FUNCNEST變數來控制。
bash -c '
recur() {
echo $1
recur $(($1 + 1))
}
FUNCNEST=5
recur 1
'
1
2
3
4
5
environment: line 1: recur: maximum function nesting level exceeded (5)