鳥哥的 Linux 私房菜Shell Scripts篇(四)
12.4 條件判斷式
只要講到『程式』的話,那麼條件判斷式,亦即是『 if then 』這種判別式肯定一定要學習的!因為很多時候,我們都必須要依據某些資料來判斷程式該如何進行。舉例來說,我們在上頭的ans_yn.sh 討論輸入迴應的範例中不是有練習當使用者輸入Y/N時,必須要執行不同的訊息輸出嗎?簡單的方式可以利用&&與|| ,但如果我還想要執行一堆指令呢?那真的得要if then來幫忙囉~底下我們就來聊一聊!
Top12.4.1 利用if .... then
這個if .... then 是最常見的條件判斷式了~簡單的說,就是當符合某個條件判斷的時候, 就予以進行某項工作就是了。
- 單層、簡單條件判斷式
如果你只有一個判斷式要進行,那麼我們可以簡單的這樣看:
if [條件判斷式]; then
當條件判斷式成立時,可以進行的指令工作內容;
fi <==將if反過來寫,就成為fi啦!結束if之意!
|
至於條件判斷式的判斷方法,與前一小節的介紹相同啊!較特別的是,如果我有多個條件要判別時,除了ans_yn.sh那個案例所寫的,也就是『將多個條件寫入一箇中括號內的情況』之外,我還可以有多箇中括號來隔開喔!
- && 代表AND ;
- || 代表or ;
所以,在使用中括號的判斷式中, && 及|| 就與指令下達的狀態不同了。舉例來說, ans_yn.sh 裡面的判斷式可以這樣修改:
[ "${yn}" == "Y" -o "${yn}" == "y" ]
上式可替換為
[ "${yn}" == "Y" ] || [ "${ yn}" == "y" ]
之所以這樣改,很多人是習慣問題!很多人則是喜歡一箇中括號僅有一個判別式的原因。
[[email protected] bin]$ cp ans_yn.sh ans_yn-2.sh <==用複製來修改的比較快! [[email protected] bin]$ vim ans_yn-2.sh #!/bin/bash # Program: # This program shows the user's choice # History: # 2015/07/16 VBird First release PATH=/bin:/sbin:/usr/bin:/usr/sbin:/usr/local/bin:/usr/local/sbin:~/bin export PATH read -p "Please input (Y/N): " yn if [ "${yn}" == "Y" ] || [ "${yn}" == "y" ]; then echo "OK, continue" exit 0 fi if [ "${yn}" == "N" ] || [ "${yn}" == "n" ]; then echo "Oh, interrupt!" exit 0 fi echo "I don't know what your choice is" && exit 0 |
不過,由這個例子看起來,似乎也沒有什麼了不起吧?原本的ans_yn.sh 還比較簡單呢~ 但是如果以邏輯概念來看,其實上面的範例中,我們使用了兩個條件判斷呢!明明僅有一個${yn} 的變數,為何需要進行兩次比對呢?此時,多重條件判斷就能夠來測試測試囉!
- 多重、複雜條件判斷式
在同一個資料的判斷中,如果該資料需要進行多種不同的判斷時,應該怎麼作?舉例來說,上面的ans_yn.sh指令碼中,我們只要進行一次${yn}的判斷就好(僅進行一次if ),不想要作多次if的判斷。此時你就得要知道底下的語法了:
#一個條件判斷,分成功進行與失敗進行(else)
if [條件判斷式]; then
當條件判斷式成立時,可以進行的指令工作內容;
else
當條件判斷式不成立時,可以進行的指令工作內容;
fi
|
如果考慮更復雜的情況,則可以使用這個語法:
#多個條件判斷(if ... elif ... elif ... else)分多種不同情況執行
if [條件判斷式一]; then
當條件判斷式一成立時,可以進行的指令工作內容;
elif [條件判斷式二]; then 當條件判斷式二成立時,可以進行的指令工作內容;
else 當條件判斷式一與二均不成立時,可以進行的指令工作內容; fi |
你得要注意的是, elif 也是個判斷式,因此出現elif 後面都要接then 來處理!但是else 已經是最後的沒有成立的結果了, 所以else 後面並沒有then 喔!好!我們來將ans_yn-2.sh 改寫成這樣:
[[email protected] bin]$ cp ans_yn-2.sh ans_yn-3.sh [[email protected] bin]$ vim ans_yn-3.sh #!/bin/bash # Program: # This program shows the user's choice # History: # 2015/07/16 VBird First release PATH=/bin:/sbin:/usr/bin:/usr/sbin:/usr/local/bin:/usr/local/sbin:~/bin export PATH read -p "Please input (Y/N): " yn if [ "${yn}" == "Y" ] || [ "${yn}" == "y" ]; then echo "OK, continue" elif [ "${yn}" == "N" ] || [ "${yn}" == "n" ]; then echo "Oh, interrupt!" else echo "I don't know what your choice is" fi |
是否程式變得很簡單,而且依序判斷,可以避免掉重複判斷的狀況,這樣真的很容易設計程式的啦!^_^!好了,讓我們再來進行另外一個案例的設計。一般來說,如果你不希望使用者由鍵盤輸入額外的資料時,可以使用上一節提到的引數功能($1)!讓使用者在下達指令時就將引數帶進去!現在我們想讓使用者輸入『 hello 』這個關鍵字時,利用引數的方法可以這樣依序設計:
- 判斷$1 是否為hello,如果是的話,就顯示"Hello, how are you ?";
- 如果沒有加任何引數,就提示使用者必須要使用的引數下達法;
- 而如果加入的引數不是hello ,就提醒使用者僅能使用hello 為引數。
整個程式的撰寫可以是這樣的:
[[email protected] bin]$ vim hello-2.sh #!/bin/bash # Program: # Check $1 is equal to "hello" # History: # 2015/07/16 VBird First release PATH=/bin:/sbin:/usr/bin:/usr/sbin:/usr/local/bin:/usr/local/sbin:~/bin export PATH if [ "${1}" == "hello" ]; then echo "Hello, how are you ?" elif [ "${1}" == "" ]; then echo "You MUST input parameters, ex> {${0} someword}" else echo "The only parameter is 'hello', ex> {${0} hello}" fi |
然後你可以執行這支程式,分別在$1 的位置輸入hello, 沒有輸入與隨意輸入, 就可以看到不同的輸出囉~是否還覺得挺簡單的啊!^_^。事實上, 學到這裡,也真的很厲害了~好了,底下我們繼續來玩一些比較大一點的計畫囉~
我們在第十章已經學會了grep 這個好用的玩意兒,那麼多學一個叫做netstat的指令,這個指令可以查詢到目前主機有開啟的網路服務埠口(service ports),相關的功能我們會在伺服器架設篇繼續介紹,這裡你只要知道,我可以利用『 netstat -tuln』來取得目前主機有啟動的服務,而且取得的資訊有點像這樣:
[[email protected] ~]$ netstat -tuln Active Internet connections (only servers) Proto Recv-Q Send-Q Local Address Foreign Address State tcp 0 0 0.0.0.0:22 0.0.0.0:* LISTEN tcp 0 0 127.0.0.1:25 0.0.0.0:* LISTEN tcp6 0 0 :::22 :::* LISTEN tcp6 0 0 ::1:25 :::* LISTEN udp 0 0 0.0.0.0:123 0.0.0.0:* udp 0 0 0.0.0.0:5353 0.0.0.0:* udp 0 0 0.0.0.0:44326 0.0.0.0:* udp 0 0 127.0.0.1:323 0.0.0.0:* udp6 0 0 :::123 :::* udp6 0 0 ::1:323 :::* #封包格式本地IP:埠口遠端IP:埠口是否監聽 |
上面的重點是『Local Address (本地主機的IP與埠口對應)』那個欄位,他代表的是本機所啟動的網路服務!IP的部分說明的是該服務位於那個介面上,若為127.0.0.1 則是僅針對本機開放,若是0.0.0.0 或::: 則代表對整個Internet 開放(更多資訊請參考伺服器架設篇的介紹)。每個埠口(port) 都有其特定的網路服務,幾個常見的port 與相關網路服務的關係是:
- 80: WWW
- 22: ssh
- 21: ftp
- 25: mail
- 111: RPC(遠端程式呼叫)
- 631: CUPS(列印服務功能)
假設我的主機有興趣要偵測的是比較常見的port 21, 22, 25及80 時,那我如何透過netstat 去偵測我的主機是否有開啟這四個主要的網路服務埠口呢?由於每個服務的關鍵字都是接在冒號『 : 』後面, 所以可以藉由擷取類似『 :80 』來偵測的!那我就可以簡單的這樣去寫這個程式喔:
[[email protected] bin]$ vim netstat.sh #!/bin/bash # Program: # Using netstat and grep to detect WWW,SSH,FTP and Mail services. # History: # 2015/07/16 VBird First release PATH=/bin:/sbin:/usr/bin:/usr/sbin:/usr/local/bin:/usr/local/sbin:~/bin export PATH # 1. 先作一些告知的動作而已~ echo "Now, I will detect your Linux server's services!" echo -e "The www, ftp, ssh, and mail(smtp) will be detected! \n" # 2. 開始進行一些測試的工作,並且也輸出一些資訊囉! testfile=/dev/shm/netstat_checking.txt netstat -tuln > ${testfile} #先轉存資料到記憶體當中!不用一直執行netstat testing=$(grep ":80 " ${testfile}) #偵測看port 80在否? if [ "${testing}" != "" ]; then echo "WWW is running in your system." fi testing=$(grep ":22 " ${testfile}) #偵測看port 22在否? if [ "${testing}" != "" ]; then echo "SSH is running in your system." fi testing=$(grep ":21 " ${testfile}) #偵測看port 21在否? if [ "${testing}" != "" ]; then echo "FTP is running in your system." fi testing=$(grep ":25 " ${testfile}) #偵測看port 25在否? if [ "${testing}" != "" ]; then echo "Mail is running in your system." fi |
實際執行這支程式你就可以看到你的主機有沒有啟動這些服務啦!是否很有趣呢?條件判斷式還可以搞的更復雜!舉例來說,在臺灣當兵是國民應盡的義務,不過,在當兵的時候總是很想要退伍的!那你能不能寫個指令碼程式來跑,讓使用者輸入他的退伍日期,讓你去幫他計算還有幾天才退伍?
由於日期是要用相減的方式來處置,所以我們可以透過使用date 顯示日期與時間,將他轉為由1970-01-01 累積而來的秒數, 透過秒數相減來取得剩餘的秒數後,再換算為日數即可。整個指令碼的製作流程有點像這樣:
- 先讓使用者輸入他們的退伍日期;
- 再由現在日期比對退伍日期;
- 由兩個日期的比較來顯示『還需要幾天』才能夠退伍的字樣。
似乎挺難的樣子?其實也不會啦,利用『date --date="YYYYMMDD" +%s』轉成秒數後,接下來的動作就容易的多了!如果你已經寫完了程式,對照底下的寫法試看看:
[[email protected] bin]$ vim cal_retired.sh #!/bin/bash # Program: # You input your demobilization date, I calculate how many days before you demobilize. # History: # 2015/07/16 VBird First release PATH=/bin:/sbin:/usr/bin:/usr/sbin:/usr/local/bin:/usr/local/sbin:~/bin export PATH # 1. 告知使用者這支程式的用途,並且告知應該如何輸入日期格式? echo "This program will try to calculate :" echo "How many days before your demobilization date..." read -p "Please input your demobilization date (YYYYMMDD ex>20150716): " date2 # 2.測試一下,這個輸入的內容是否正確?利用正規表示法囉~ date_d=$(echo ${date2} |grep '[0-9]\{8\}') #看看是否有八個數字 if [ "${date_d}" == "" ]; then echo "You input the wrong date format...." exit 1 fi # 3.開始計算日期囉~ declare -i date_dem=$(date --date="${date2}" +%s) #退伍日期秒數 declare -i date_now=$(date +%s) #現在日期秒數 declare -i date_total_s=$((${date_dem}-${date_now})) #剩餘秒數統計 declare -i date_d=$((${date_total_s}/60/60/24)) #轉為日數 if [ "${date_total_s}" -lt "0" ]; then #判斷是否已退伍 echo "You had been demobilization before: " $((-1*${date_d})) " ago" else declare -i date_h=$(($((${date_total_s}-${date_d}*60*60*24))/60/60)) echo "You will demobilize after ${date_d} days and ${date_h} hours." fi |
瞧一瞧,這支程式可以幫你計算退伍日期呢~如果是已經退伍的朋友,還可以知道已經退伍多久了~哈哈!很可愛吧~指令碼中的date_d變數宣告那個/60/60/24是來自於一天的總秒數(24小時*60分*60秒) 。瞧~全部的動作都沒有超出我們所學的範圍吧~ ^_^還能夠避免使用者輸入錯誤的數字,所以多了一個正規表示法的判斷式呢~這個例子比較難,有興趣想要一探究竟的朋友,可以作一下課後練習題 關於計算生日的那一題喔!~加油!
Top12.4.2 利用case ..... esac 判斷
上個小節提到的『 if .... then .... fi 』對於變數的判斷是以『比對』的方式來分辨的,如果符合狀態就進行某些行為,並且透過較多層次(就是elif ...)的方式來進行多個變數的程式碼撰寫,譬如 hello-2.sh那個小程式,就是用這樣的方式來撰寫的囉。好,那麼萬一我有多個既定的變數內容,例如hello-2.sh當中,我所需要的變數就是"hello"及空字串兩個,那麼我只要針對這兩個變數來設定狀況就好了,對吧?那麼可以使用什麼方式來設計呢?呵呵~就用case ... in .... esac吧~,他的語法如下:
case $變數名稱in <==關鍵字為case ,還有變數前有錢字號
"第一個變數內容" ) <==每個變數內容建議用雙引號括起來,關鍵字則為小括號) 程式段
;; <==每個類別結尾使用兩個連續的分號來處理! "第二個變數內容" ) 程式段 ;; * ) <==最後一個變數內容都會用*來代表所有其他值 不包含第一個變數內容與第二個變數內容的其他程式執行段 exit 1 ;; esac <==最終的case結尾!『反過來寫』思考一下! |
要注意的是,這個語法以case (實際案例之意) 為開頭,結尾自然就是將case 的英文反過來寫!就成為esac 囉!不會很難背啦!另外,每一個變數內容的程式段最後都需要兩個分號(;;) 來代表該程式段落的結束,這挺重要的喔!至於為何需要有* 這個變數內容在最後呢?這是因為,如果使用者不是輸入變數內容一或二時, 我們可以告知使用者相關的資訊啊!廢話少說,我們拿hello-2.sh 的案例來修改一下,他應該會變成這樣喔:
[[email protected] bin]$ vim hello-3.sh #!/bin/bash # Program: # Show "Hello" from $1.... by using case .... esac # History: # 2015/07/16 VBird First release PATH=/bin:/sbin:/usr/bin:/usr/sbin:/usr/local/bin:/usr/local/sbin:~/bin export PATH case ${1} in "hello") echo "Hello, how are you ?" ;; "") echo "You MUST input parameters, ex> {${0} someword}" ;; *) #其實就相當於萬用字元,0~無窮多個任意字元之意! echo "Usage ${0} {hello}" ;; esac |
在上面這個hello-3.sh的案例當中,如果你輸入『 sh hello-3.sh test 』來執行,那麼螢幕上就會出現『Usage hello-3.sh {hello}』的字樣,告知執行者僅能夠使用hello喔~這樣的方式對於需要某些固定字串來執行的變數內容就顯的更加的方便呢!這種方式你真的要熟悉喔!這是因為早期系統的很多服務的啟動scripts都是使用這種寫法的(CentOS 6.x以前)。雖然CentOS 7已經使用systemd,不過仍有數個服務是放在/etc/init.d/目錄下喔!例如有個名為netconsole的服務在該目錄下,那麼你想要重新啟動該服務,是可以這樣做的(請注意,要成功執行,還是得要具有root身份才行!一般帳號能執行,但不會成功!):
/etc/init.d/netconsole restart
重點是那個restart 啦!如果你使用『 less /etc/init.d/netconsole 』去查閱一下,就會看到他使用的是case 語法, 並且會規定某些既定的變數內容,你可以直接下達/etc/init.d/ netconsole , 該script 就會告知你有哪些後續接的變數可以使用囉~方便吧!^_^
一般來說,使用『 case $變數in 』這個語法中,當中的那個『 $變數』大致有兩種取得的方式:
- 直接下達式:例如上面提到的,利用『 script.sh variable 』的方式來直接給予$1這個變數的內容,這也是在/etc/init.d目錄下大多數程式的設計方式。
- 互動式:透過read這個指令來讓使用者輸入變數的內容。
這麼說或許你的感受性還不高,好,我們直接寫個程式來玩玩:讓使用者能夠輸入one, two, three , 並且將使用者的變數顯示到螢幕上,如果不是one, two, three時,就告知使用者僅有這三種選擇。
[[email protected] bin]$ vim show123.sh #!/bin/bash # Program: # This script only accepts the flowing parameter: one, two or three. # History: # 2015/07/17 VBird First release PATH=/bin:/sbin:/usr/bin:/usr/sbin:/usr/local/bin:/usr/local/sbin:~/bin export PATH echo "This program will print your selection !" # read -p "Input your choice: " choice #暫時取消,可以替換! # case ${choice} in #暫時取消,可以替換! case ${1} in #現在使用,可以用上面兩行替換! "one") echo "Your choice is ONE" ;; "two") echo "Your choice is TWO" ;; "three") echo "Your choice is THREE" ;; *) echo "Usage ${0} {one|two|three}" ;; esac |
此時,你可以使用『 sh show123.sh two 』的方式來下達指令,就可以收到相對應的迴應了。上面使用的是直接下達的方式,而如果使用的是互動式時,那麼將上面第10, 11 行的"#" 拿掉, 並將12 行加上註解(#),就可以讓使用者輸入引數囉~這樣是否很有趣啊?
Top12.4.3 利用function 功能
什麼是『函式(function)』功能啊?簡單的說,其實,函式可以在shell script當中做出一個類似自訂執行指令的東西,最大的功能是,可以簡化我們很多的程式碼~舉例來說,上面的show123.sh當中,每個輸入結果one, two, three其實輸出的內容都一樣啊~那麼我就可以使用function來簡化了!function的語法是這樣的:
function fname () {
程式段
}
|
那個fname就是我們的自訂的執行指令名稱~而程式段就是我們要他執行的內容了。要注意的是,因為shell script的執行方式是由上而下,由左而右,因此在shell script當中的function的設定一定要在程式的最前面,這樣才能夠在執行時被找到可用的程式段喔(這一點與傳統程式語言差異相當大!初次接觸的朋友要小心!)!好~我們將show123.sh改寫一下,自訂一個名為printit的函式來使用喔:
[[email protected] bin]$ vim show123-2.sh #!/bin/bash # Program: # Use function to repeat information. # History: # 2015/07/17 VBird First release PATH=/bin:/sbin:/usr/bin:/usr/sbin:/usr/local/bin:/usr/local/sbin:~/bin export PATH function printit(){ echo -n "Your choice is " #加上-n可以不斷行繼續在同一行顯示 } echo "This program will print your selection !" case ${1} in "one") printit ; echo ${1} | tr 'az' 'AZ' #將引數做大小寫轉換! ;; "two") printit ; echo ${1} | tr 'az' 'A-Z' ;; "three") printit ; echo ${1} | tr 'az' 'A-Z' ;; *) echo "Usage ${0} {one|two|three}" ;; esac |
以上面的例子來說,鳥哥做了一個函式名稱為printit ,所以,當我在後續的程式段裡面, 只要執行printit 的話,就表示我的shell script 要去執行『 function printit .... 』裡面的那幾個程式段落囉!當然囉,上面這個例子舉得太簡單了,所以你不會覺得function 有什麼好厲害的, 不過,如果某些程式碼一再地在script 當中重複時,這個function 可就重要的多囉~ 不但可以簡化程式碼,而且可以做成類似『模組』的玩意兒,真的很棒啦!
另外,function也是擁有內建變數的~他的內建變數與shell script很類似,函式名稱代表示$0 ,而後續接的變數也是以$1, $2...來取代的~這裡很容易搞錯喔~因為『 function fname() {程式段} 』內的$0, $1...等等與shell script的$0是不同的。以上面show123-2.sh來說,假如我下達:『 sh show123-2.sh one 』這表示在shell script內的$1為"one"這個字串。但是在printit()內的$1則與這個one無關。我們將上面的例子再次的改寫一下,讓你更清楚!
[[email protected] bin]$ vim show123-3.sh #!/bin/bash # Program: # Use function to repeat information. # History: # 2015/07/17 VBird First release PATH=/bin:/sbin:/usr/bin:/usr/sbin:/usr/local/bin:/usr/local/sbin:~/bin export PATH function printit(){ echo "Your choice is ${1}" #這個$1必須要參考底下指令的下達 } echo "This program will print your selection !" case ${1} in "one") printit 1 #請注意, printit指令後面還有接引數! ;; "two") printit 2 ;; "three") printit 3 ;; *) echo "Usage ${0} {one|two|three}" ;; esac |
在上面的例子當中,如果你輸入『 sh show123-3.sh one 』就會出現『 Your choice is 1 』的字樣~ 為什麼是1 呢?因為在程式段落當中,我們是寫了『 printit 1 』那個1 就會成為function 當中的$1 喔~ 這樣是否理解呢?function 本身其實比較困難一點,如果你還想要進行其他的撰寫的話。不過,我們僅是想要更加了解shell script 而已,所以,這裡看看即可~瞭解原理就好囉~ ^_^