官方資料:Shell Functions (Bash Reference Manual) 簡介 正如我們在《Bash腳本編程學習筆記06:條件結構體》中最後所說的,我們應該把一些可能反覆執行的代碼塊整合起來,避免反覆編寫使得代碼過於臃腫。 函數正是為瞭解決這個問題而存在的。函數在定義時,可以將常用的 ...
簡介
正如我們在《Bash腳本編程學習筆記06:條件結構體》中最後所說的,我們應該把一些可能反覆執行的代碼塊整合起來,避免反覆編寫使得代碼過於臃腫。
函數正是為瞭解決這個問題而存在的。函數在定義時,可以將常用的代碼整合為一個整體,當我們需要執行的時候,只需要調用這個函數即可。
Bash是過程式編程語言,從上至下順序執行代碼,因此函數定義必須在函數調用之前完成。
函數屬於shell的基礎特性,即不僅僅是針對於bash,包括csh、sh、ksh和zsh等都具有這裡說明的函數特性。
函數會在當前的shell環境下執行,不會創建新的進程(子shell)來解釋函數。
函數可以通過“unset -f”命令刪除。
函數的定義和調用
函數的定義有兩種方式。
方式一。
FUNC_NAME () {
BODY
}
方式二。
function FUNC_NAME [()] { BODY }
[()]:表示小括弧可以省略。
函數的調用只需要鍵入函數名即可,就像鍵入命令名一樣。
[root@c7-server ~]# cat function.sh #!/bin/bash
# 函數定義 test_func() { echo "This is just for test." }
# 函數調用 test_func [root@c7-server ~]# bash function.sh This is just for test.
示例:編寫一個腳本,接收一個用戶名參數,輸出用戶名、UID和預設shell。(要求以函數的形式)
#!/bin/bash userinfo() { if ! id $username &> /dev/null; then echo "The user $username is not exists." else grep "^$username\>" /etc/passwd | cut -d : -f 1,3,7 fi } username=$1 userinfo
示例:將《Bash腳本編程學習筆記06:條件結構體》中最後的服務腳本中冗餘的代碼改寫為函數,即函數式編程。
#!/bin/bash # # chkconfig: - 50 50 # Description: test service script # prog=$(basename $0) lockfile="/var/lock/subsys/$prog" start() { if [ -e $lockfile ]; then echo "The service $prog has already started." else touch $lockfile echo "The service $prog starts finished." fi } stop() { if [ ! -e $lockfile ]; then echo "The service $prog has already stopped." else rm -f $lockfile echo "The service $prog stops finished." fi } restart() { if [ -e $lockfile ]; then rm -f $lockfile touch $lockfile echo "The service $prog restart finished." else touch $lockfile echo "The service $prog starts finished." fi } status() { if [ -e $lockfile ]; then echo "The service $prog is running." else echo "The service $prog is not running." fi } case $1 in start) start ;; stop) stop ;; restart) restart ;; status) status ;; *) echo "Usage: $prog {start|stop|restart|status}" exit 1 ;; esac
函數的輸出和退出狀態碼
函數的輸出指的是函數體中執行的命令的輸出,STDOUT和STDERR都會輸出。其中也包括了我們使用echo或者printf的顯式的STDOUT。
如果函數中沒有return語句的話,那麼函數的退出狀態碼是函數體中最後一條命令的退出狀態碼。
如果函數中有return語句的話,那麼當函數體自上而下執行到return時,函數會立即停止並返回,函數的退出狀態碼就是return指定的退出狀態碼。
return [n]
如果return沒有指定退出狀態碼的話,那麼就是return的上一條命令的退出狀態碼。
註意不要和exit語句混淆。return用戶終止函數的執行並返回,而exit是終止了整個腳本的執行並退出。後者的作用域更廣。
函數的位置參數
函數和腳本一樣,都可以接收參數作為位置參數,然後在函數中引用這些參數。也支持引用與位置參數有關的特殊變數($#, $*, $@)。
向函數傳遞位置參數和向腳本傳遞位置參數的方式是一樣的。
my_func() { echo "$1 $2" } my_func arg1 arg2
記住,傳參時,不要寫成類似C語言的風格,會報錯的。
my_func(arg1, arg2)
練習
1、編寫一個腳本,批量添加用戶,用戶傳參給腳本,參數是欲添加的用戶名首碼,用戶名其餘部分使用數字補全。
#!/bin/bash userAdd() { if id $1 &> /dev/null; then return 1 else useradd $1 return 0 fi } for i in {01..03}; do username="$1$i" userAdd $username retval=$? if [ $retval -eq 0 ]; then echo "The user $username has been added." elif [ $retval -eq 1 ]; then echo "The user $username was existed." fi done
這裡有一點需要註意,在for迴圈體中,一定要在函數調用後立即將函數的退出狀態碼(return)獲取並存入變數中(如該示例中的retval),而後在對該狀態碼做判斷。
如果直接多次判斷$?的值的話,那麼第一次的$?的值是函數的退出狀態碼,到了第二次,就會變成了上一句echo語句了。
2、編寫一個腳本,通過函數檢測(ping)某一主機的線上狀態。需檢測192.168.152.1~192.168.152.254這個範圍。
#!/bin/bash ping_test() { ip=$1 if ping -c 1 -q $ip &> /dev/null; then echo "The host $ip is online." return 0 else return 1 fi } for i in 192.168.152.{1..254}; do ping_test $i done
Linux中的ping命令,預設是發送無限的請求數據包持續ping的,需要使用-c指定包的數量。這個腳本不太好,因為ping遇到不通的情況會等待一段時間,導致腳本比較耗時。
3、編寫一個腳本,通過函數實現乘法口訣表,註意,不是九九乘法口訣表,而是NN乘法口訣表,N作為用戶參數傳入腳本。
#!/bin/bash multi() { N=$1 for ((i=1;i<=N;i++)); do for ((j=1;j<=i;j++)); do echo -ne "$j*$i=$((i*j))\t" done echo done } multi $1
註意:這個腳本有瑕疵,在輸出時,當N數值過大的時候,使用“\t”製表符會使顯示較不人性化。
函數中的變數作用域
在《Bash腳本編程學習筆記01:變數與多命令執行》中我們說變數有三種類型並說明瞭其作用域。
- 本地變數:僅當前shell有效(即當前bash進程)。
- 環境變數:當前shell及其子shell(即當前bash進程即其子bash進程)。
- 局部變數:在某部分代碼片段中有效(例如函數)。
結合作用域的概念,我們來看下麵這個示例。
#!/bin/bash name=tom set_name() { name=jerry echo $name } set_name echo $name
我們應該會認為這樣吧?
set_name --輸出--> jerry echo $name --輸出--> tom
其實,真實的結果是:
[root@c7-server ~]# bash func_scope.sh jerry jerry
造成這種結果的原因是,局部變數是需要手動定義的,而不是其出現在函數體中就是局部變數了。
可以通過內置命令local來定義局部變數,local僅可以在函數內部使用!
local [option] name[=value] …
因此我們對腳本進行改寫,函數體中的變數明確使用local命令定義,就可以驗證我們此前說的變數作用域的理論了。
[root@c7-server ~]# cat func_scope.sh #!/bin/bash name=tom set_name() { local name=jerry echo $name } set_name echo $name [root@c7-server ~]# bash func_scope.sh jerry tom
函數中的變數作用域,是一個難點,我其實沒搞太明白。上面的示例其實是一個非常簡單的示例,在複雜的情況下,例如函數A調用函數B時,local會變得很有幫助。具體遇到的時候,建議大家再翻閱一下官方的手冊。
函數的遞歸
一個函數可以調用另一個函數,而當一個函數調用自身時,就叫做函數的遞歸。
遞歸不會無限遞歸,一般會有一個邊界,在邊界處會返回,而後根據遞歸的順序逐一按照相反的順序返回。
函數的遞歸很好地解決了計算階乘(factorial)和斐波那契(fibonacci)數列。
階乘
階乘是由基斯頓·卡曼發明的數學運算。一個正整數的階乘是所有小於等於該數的正整數的積,記作“n!”。0和1的階乘都是1。
n!=1*2*3*4*...*n
階乘存在一個規律。
5!=1*2*3*4*5 4!=1*2*3*4 3!=1*2*3 2!=1*2 1!=1
因此。
5!=4!*5 4!=3!*5 3!=2!*3 ... n!=(n-1)!*n=n*(n-1)!
我們就可以把階乘定義為一個函數並通過遞歸的方式來實現階乘的計算。
#!/bin/bash fact() { if [ $1 -eq 1 -o $1 -eq 0 ]; then echo 1 else echo $(($1*$(fact $(($1-1))))) fi } fact $1
斐波那契數列
斐波那契數列是數學家萊昂納多·斐波那契以兔子繁殖為例引入的,又作兔子數列。
該數列F,第一項和第二項都為1,從第二項開始,每一項的值都是前兩項之和。
數列F F(1)=1 F(2)=1 F(3)=F(1)+F(2) ... F(n)=F(n-2)+F(n-1)
1 1 2 3 5 8 13 21 34 ...
因此我們可以把求斐波那契數列的第N項的值寫為一個函數。
[root@c7-server ~]# cat func_factorial.sh #!/bin/bash fact() { if [ $1 -eq 1 -o $1 -eq 0 ]; then echo 1 else echo $(($1*$(fact $(($1-1))))) fi } fact $1 [root@c7-server ~]# vim func_fibo.sh [root@c7-server ~]# bash func_fibo.sh 1 1 [root@c7-server ~]# bash func_fibo.sh 2 1 [root@c7-server ~]# bash func_fibo.sh 3 2 [root@c7-server ~]# bash func_fibo.sh 4 3 [root@c7-server ~]# bash func_fibo.sh 5 5 [root@c7-server ~]# bash func_fibo.sh 6 8 [root@c7-server ~]# bash func_fibo.sh 7 13 [root@c7-server ~]# bash func_fibo.sh 8 21
不過這種方式僅僅是求得第N項的值,我們可以改為列印這個斐波那契數列。
[root@c7-server ~]# cat func_fibo.sh #!/bin/bash fibo() { if [ $1 -eq 1 -o $1 -eq 2 ]; then echo -n "1 " else echo -n "$(($(fibo $(($1-2)))+$(fibo $(($1-1))))) " fi } for i in $(seq $1); do fibo $i done echo [root@c7-server ~]# bash func_fibo.sh 10 1 1 2 3 5 8 13 21 34 55