1. 程式人生 > >2018-7-10bash編程之case及函數

2018-7-10bash編程之case及函數

imp 打印nn乘法表 作用 遞歸 shell腳本 read 微軟雅黑 發現 elif

在上一章當中我們講述了bash循環,其中我們講述了for循環的特殊用法,以及while循環的特殊用法,而在此前我們講述了循環的控制語句,一個是break,另一個是continue,對於continue來說,它是結束本輪循環而後進入下一輪循環,而break是提前結束其循環本身。但如果是循環嵌套的話,break只能退出當前那一層的循環,如果想退出所有的循環,就要使用break後面加上一個數字用來跳出循環的層數。而後while循環的特殊用法可以遍歷文件的每一行,for以及while循環可以寫成:

for (()); do
    循環體
done

while read VARIABLE; do
    循環體
done < /PATH/TO/SOMEFILE

一、case語句

我們此前講到過,任何一個程序控制語言,在執行該程序時,都由順序、選擇及循環這三種組成,而對於判斷條件的語句來說,主要有ifcase,我們回顧一下多分支的if語句組成格式為:

    多分支if語句:
        if CONDITION1; then
            分支1
        elif CONDINTION2; then
            分支2
        ...
        else
            分支n
        fi

示例1:顯示一個菜單給用戶:

cpu) display cpu information
mem) display mem information
disk) display disks information
quit) quit

要求:(1) 提示用戶給出自己的選擇;
    (2) 正確的選擇則給出相應的信息;否則,重新提示讓其用戶選擇正確的選項

那麽以上我們使用的是if控制語句寫出的bash程序,接下來我們了解一下case語句的使用,其語法格式為:

case語句的語法格式:
    
    case $VARIABLE in
    PAT1)
        分支1
        ;;
    PAT2)
        分支2
        ;;
    ...
    *)
        分支n
        ;;
    esac

每一個分支都要使用雙分號結尾,這也是固定的語法格式,而這個雙分號可以單獨成行,也可以加載分支語句的後面,但是不能省略,因為省略的話,即便模式匹配,也會判斷下一個模式。不過雖然支持通配,但僅能支持

glob的通配符。

case支持glob風格的通配符:
    *:任意長度的任意字符;
    ?:任意單個字符;
    []:範圍內任意單個字符;
    a|b:a或b;

那麽現在我們更改一下上面的示例,更改為case語句使得來回作為比較。

#!/bin/bash
#

cat << EOF
cpu) display cpu information.
mem) display memory information.
disk) display disk information.
quit) quit
==============================================
EOF

read -p "Please input option: " option

while [ "$option" != "cpu" -a "$option" != "mem" -a "$option" != "disk" -a "$option" != "quit" ]; do
	echo "Error! Please input information."
	read -p "Enter your option: " option
done

case $option in
cpu)
	lscpu
	;;
mem)
	free -m
	;;
disk)
	fdisk -l /dev/[hs]d[a-z]
	;;
quit)
	echo "quit!"
	exit 0
	;;
esac

與多分支的if語句相比簡單很多,case語句在很多的場景當中,能夠替換多分支的if,寫為最簡潔的格式,但只能用於一個變量對於多個值做模式匹配才可以使用。

示例:寫出一個服務框架腳本;

    $lockfile, 值/var/lock/subsys/SCRIPT_NAME
    (1) 此腳本可接受start, stop, restart, status四個參數之一;
    (2) 如果參數非此四者,則提示幫助後退出;
    (3) start, 則創建lockfile,並顯示啟動,stop, 則刪除lockfile,並顯示停止;restart, 則先刪除文件在創建此文件,而後顯示重啟完成;status, 如果lockfile存在,則顯示running,否則,則顯示為stopped。

以上就是case語句的示例,看上去比多分支的語句要簡便了許多,但是有些代碼還是重復了使用,而重復使用的後果使得腳本會變得臃腫,所以我們必須使用代碼某種手段能夠將代碼重用,而函數就是將代碼能夠實現重用及模塊和結構化的編程。

二、函數

那麽函數對於過程式編程來說,實現的是代碼重用的一個功能組件,它能實現模塊和結構化編程,那麽對於函數來講,我們可以理解為:

把一段獨立功能的代碼當作一個整體(用大括號圈起來),並為之起一個名字;而命名的代碼片段,此即為函數;

我們需要註意的是:

註意:定義函數的代碼段不會自動執行,在調用時才能執行;所謂調用函數,是指在執行的代碼段中給定函數名即可;
    函數名出現的任何位置,在代碼執行時,都會被自動替換為函數代碼;

由於函數是一個獨立而又完整的功能,因此就會引進新的上下文,那麽函數就是將一段擁有獨立功能的代碼或命令使用{}進行封裝,等待被執行調用,而函數名即為就是調用該函數代碼時的名稱,將函數名引入到執行代碼時則會被調用,而在執行時則會自動替換為函數代碼執行,所以它是能夠完成模塊化與結構化編程的一個重要組件,而主要的實現就是代碼重用,將其一段代碼可調用n多次。

那麽如何定義其函數語法,其共有兩種方式:

語法一:
    function f_name {
        ...函數體...
    }
    
語法二:
    f_name() {
        ...函數體...
    }

那麽再次強調的是,函數只能被調用,才會執行,而調用的方式就是給定其函數名即可,函數名出現的地方則會自動替換其函數代碼。

函數是有其生命周期的,就是每一次被調用時才會創建該函數,而執行完成返回給執行代碼的時候則會終止;

其實在我們執行bash腳本時,都會有一個命令的狀態值或者為返回值,而函數也有返回值,共有兩個,一種是使用echo或者是printf的結果返回,還有一種就是狀態返回值,其狀態返回的就是函數體中的最後一條命令運行的狀態結果。

當然,對我們來講其實並不理想,所以我們可能需要自定義狀態返回值。

函數的生命周期:每次被調用時創建,返回時終止;
    其狀態返回值為函數體中運行的最後一條命令的狀態結果;
    自定義狀態返回值,需要使用:return
        0: 成功;
        1-255: 失敗;

不過需要註意的是,在函數中的任何位置但凡遇見return,則函數生命周期就執行結束,即使該函數代碼並不是最後一個語句,就像腳本語句,一旦遇見exit,則後續的腳本程序也無法運行。

示例:給定一個用戶名;取得用戶的id號和默認的shell。

#!/bin/bash
#

但是這段代碼還是不夠靈活,因為腳本的交互式限制了多個用戶查詢,即使可以允許多個用戶查詢,每次在read命令中還得需要添加變量,這是非常麻煩的,所以我們盡可能的避開,那麽另一個示例代碼為:

#!/bin/bash
#

需要註意的是,在函數中$1變量有特殊的意義。

以上我們了解到函數的作用及用法之後,我們將之前的服務框架腳本使用函數的方式來進行重寫。
示例:使用函數來重寫服務框架腳本:

#!/bin/bash
#
# chkconfig: - 44 55
#

prog=$(basename $0)
lockfile=/var/lock/subsys/$prog

start (){
if [ -f $lockfile ]; then
echo "$prog started..."
else
touch $lockfile
[ $? -eq 0 ] && echo "$prog start..."
fi
}

stop(){
if [ -f $lockfile ]; then
rm -rf $lockfile
[ $? -eq 0 ] && echo "$prog stop..."
else
echo "$prog stop yet..."
fi
}

status() {
if [ -f $lockfile ]; then
echo "$prog is running..."
else
echo "$prog is stopped..."
fi
}

usage(){
echo "Usage: { start | stop | restart | status }"
exit 3
}

case $1 in
"start")
start
;;
"stop")
stop
;;
"restart")
stop
start
;;
"status")
status
;;
*)
usage
;;
esac

接下來說一下函數返回值的問題,函數的返回值共有兩種,一種是執行結果,另一種是狀態結果,也就是結果狀態碼,那麽如果想要使用函數返回值給予調用者,在調用者內部則可以通過某些功能用來取得,所謂函數出現並替換的地方,能夠將其函數的代碼執行結果放在主程序中,那麽函數如何有執行結果共有以下幾種方式:

函數返回值:
    函數執行結果返回值:
        (1) 使用echo或printf命令進行輸出;
        (2) 函數體中調用的命令的執行結果函數退出狀態碼;

還有就是函數的退出狀態碼,我們之前說過:

    函數的退出狀態碼:
        (1) 默認取決於函數體中執行的最後一條命令的退出狀態碼;
        (2) 自定義:return

在以後寫函數時,有必要顯示給出return用來自定義其退出狀態碼。

在函數中,$1$2有特殊意義,因為函數也可以接受參數,而且傳遞參數給函數時,在函數內部中,$1$2調用的是函數內部的參數,而不是腳本的參數。那麽如何給函數傳遞參數是在函數名後面給定函數列表即可,例如:testfun arg1 arg2 arg3 ...

我們在函數體當中,如果使用的是$1,就表示調用的是arg1。同樣,使用的是$2,則調用的是arg2,以此類推。同時還可以在函數中使用$*或$@引用所有參數,而$#引用傳遞參數的個數。

函數可以接受參數:
    傳遞參數給函數:
        在函數體當中,可以使用$1, $2, ...引用傳遞給函數的參數;還可以在函數中使用$*或$@引用所有參數,$#引用傳遞參數的個數;
        在調用函數時,在函數名後面以空白給定參數列表即可,例如:testfun arg1 arg2 ...

所以說函數是可以很靈活的,在代碼段當中能夠實現某一完整的功能,但是到底執行什麽樣的操作,施加在那個對象身上,可取決於傳遞函數的參數來實現。對於函數能夠施加參數而言,我們以下有個示例,以便於能夠進行理解。

示例:添加10個用戶,添加用戶的功能使用函數實現,用戶名做為參數傳遞給函數;

#!/bin/bash
#
# 5 user exists
#
adduser() {
if id $1 &> /dev/null; then
return 5
else
useradd $1
retval=$?
return $retval
fi
}

for i in {1..10}; do
adduser ${1}${i}
retval=$?

if [ $retval -eq 0 ]; then
echo "add user ${1}${i} finish."
elif [ $retval -eq 5 ]; then
echo "user ${1}${i} exists."
else
echo "Unknown Error."
fi
done

$?多次調用時,最好將其$?的數值保存下來。不然的話,第一個條件測試完成之後,返回的是當時條件的測試值。

練習:

    1、使用函數實現ping主機時測試主機的在線狀態;主機地址通過參數傳遞給函數;
       主程序:測試192.168.1.1-192.168.10.1範圍內各個主機的在線狀態;
    
    2、打印NN乘法表;

三、變量作用域

我們此前說過變量共有三種類型,分別為環境變量、本地變量和局部變量,在這裏我們說的是局部變量,局部變量就是在函數內部所存在,其有效範圍就是函數內部,函數創建並調用時則局部變量創建便引用,當函數生命周期結束時,則局部變量也會其自動銷毀,需要註意的是,無需等待腳本運行結束,而是函數調用返回時,則變量就結束,而這種變量就叫做局部變量。

而定義局部變量的方法為:local VARIABLE=VALUE

變量作用域:
    局部變量:作用域是函數的生命周期;在函數結束時被自動銷毀;
    定義局部變量的方法:local VARIABLE=VALUE

我們回顧一下本地變量的作用域:

本地變量:作用域是運行腳本的shell進程的生命周期;因此,其作用範圍為當前shell腳本程序文件;

舉個栗子:

#!/bin/bash
#

name=tom

setname() {
name=jerry
echo "Function: $name"
}

setname
echo "Shell: $name"

運行之後會發現:

# bash local.sh 
Function: jerry
Shell: jerry

本來Shell應該是tom,結果為jerry,因為我們在函數中所調用的name,其實就是主程序的name,這表示當函數開始調用時,直接將主程序的變量的值改為了函數中變量的值,因此函數內部為jerry,在函數外部依然是jerry,第一行name變量為本地變量,而在函數中是可以調用本地變量的。

如果二者不互相受影響,在函數內部的name變量之前加上local

#!/bin/bash
#

name=tom

setname() {
local name=jerry
echo "Function: $name"
}

setname
echo "Shell: $name"

加上之後其運行結果為:

# bash local.sh 
Function: jerry
Shell: tom

需要註意的是,如果主程序變量和函數變量相同且互不影響時,在函數裏加local對變量進行修飾。所以在寫代碼時,在函數中使用局部變量,不然會有一些故障需要手動排除,這是一個很麻煩的過程,有時候你也不知道那個bug是出自在那裏。除非和主程序交互,交換值,否則在函數中一定要用local方式來使用局部變量。

四、函數遞歸

所謂遞歸就是自己不斷的調用自己,而函數遞歸就是函數直接或間接調用自身,那麽什麽時候被用到,比如做階乘的時候,以及做斐波那契數列的時候等等,可能會用到,比如說10的階乘。

函數遞歸:
    函數直接或間接調用自身;
    
    10!=10*9!=10*9*8!=10*9*8*7!=...

那麽如果用函數來進行表示的話,如何實現其遞歸的功能,假設這個數字為n,這個n能夠返回n*(n-1),而後能夠我們能夠讓其階乘為n*(n-1)!=n*(n-1)*(n-2)!=,而後再一次調用函數為n=1終止,則意味這一層一層的做出計算,稱之為遞歸返回,但是遞歸太多也是有很大的問題,數值太大的話,需要大量的內存空間來保留其中間值。

還有一種是斐波那契數列,其特性為第一個數為1,第二個數也為1,隨後每一個值都是前兩個值的和。

1, 1, 2, 3, 5, 8, 13, 21, ...

以下分別為兩個示例,階乘和斐波那契數列,在這裏我們不多做闡述。

#!/bin/bash
#
fact() {
if [ $1 -eq 0 -o $1 -eq 1 ]; then
echo 1
else
echo $[$1*$(fact $[$1-1])]
fi
}

fact 9
#!/bin/bash
#

fab() {
if [ $1 -eq 1 ]; then
echo -n "1 "
elif [ $1 -eq 2 ]; then
echo -n "1 "
else
echo -n "$[$(fab $[$1-1])+$(fab $[$1-2])] "
fi
}

for i in $(seq 1 $1); do
fab "$i "
done
echo


2018-7-10bash編程之case及函數