函式

函式 在 Bash

{other: "%{count} 個練習"}

關於 函式

許多 Bash 腳本是用嚴格的命令式風格寫成的:先執行一個指令,再執行另一個指令,依此類推。有時候,你會需要把一連串概念上共同達成單一目的的指令群組在一起。這正是_函式_派上用場的地方。

定義函式

你可以像這樣定義一個函式:

my_function () {
    COMMANDS
}

空的括號只是表示你正在定義一個函式,裡面什麼都不用放。

函式參數

函式一旦定義好,就和任何其他指令(無論是不是內建)沒有兩樣。就像任何指令一樣,你可以把_引數_傳給函式。在函式內部,你可以用_位置參數_$1、$2等來存取這些引數。(還記得嗎?我們在變數概念裡學過位置參數。)

Advanced

特殊參數$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

在函式內部,你可以存取_呼叫者_作用域裡的變數。也就是說,你可以使用全域變數,也可以使用在呼叫者(或呼叫呼叫者的某個函式)裡宣告的區域變數。

Advanced

嚴格說來,「全域」這個詞用得並不精確。要在函式中展開一個變數,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

同樣地,把值_指定_給變數時,會_在該變數被宣告的作用域裡_完成指定。這種「遠距作用」可能寫出難以追蹤的程式碼,因為變數究竟在哪裡被指定了值,並不總是那麼明顯。

Advanced

你可以用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時,會回傳最後一個執行過的指令的狀態。

Note

注意,check_password函式可以簡化成:

check_password () { [[ $1 == "secret" ]]; }
  1. [[...]]條件建構有結束狀態:0代表「true」,1代表「false」。
  2. {...}群組建構在結尾大括號之前,必須有一個換行或一個分號。

函式輸出

函式的回傳狀態只是一個數字。那函式要怎麼產生輸出呢?

你的函式可以印到標準輸出。用大家熟悉的_指令替換_把它捕捉下來:

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
Advanced

遞迴深度可以用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)
透過 GitHub 編輯 連結會在新視窗或分頁中開啟