學習Shell Scripts

原贴地址:http://linux.vbird.org/linux_basic/0340bashshell-scripts.php
1. 什麼是 Shell Script
  1.1 幹嘛學習 shell scripts?
  1.2 第一支 script 的撰寫與執行
  1.3 撰寫 shell script 的良好習慣建立
2. 簡單的 shell script 練習:
3. 善用判斷式:
  3.1 利用 test 指令的測試功能
  3.2 利用判斷符號 [ ]
  3.3 Shell script 的預設變數($0, $1...)
4. 條件判斷式:
  4.1 利用 if .... then
  4.2 利用 case ..... esac 判斷
  4.3 利用 function 功能
5. 迴圈 (loop)
  5.1 while....do....done, until....do....done
  5.2 for...do...done
6. shell script 的追蹤與 debug
7. 本章習題練習
8. 針對本文的建議:http://phorum.vbird.org/viewtopic.php?t=23886
大標題的圖示什麼是 Shell scripts ?
這個有趣的問題趕緊來回答看看,什麼是 shell script 呢? shell 我們在 認識 bash 當中已經提過了,那是一個文字介面底下讓我們與系統溝通的一個工具介面,那麼 script 是啥? 字面上的意義, script 是『腳本、劇本』的意思。整句話是說, shell script 是針對 shell 所寫的『劇本!』 什麼東西啊?呵呵!其實, shell script 是利用 shell 的功能所寫的一個『程式 (program)』,這個程式是使用純文字檔,將一些 shell 的語法與指令寫在裡面, 搭配正規表示法、管線命令與資料流重導向等功能,以達到我們所想要的處理目的。

所以,簡單的說, shell script 就像是早期 DOS 年代的批次檔 (.bat) ,最簡單的功能就是將許多指令彙整寫在一起, 讓使用者很輕易的就能夠 one touch (執行一個檔案 "shell script" ,就能夠一次執行多個指令), 而, shell script 更提供陣列、迴圈、條件與邏輯判斷等重要功能,讓使用者也可以直接以 shell 來撰寫程式,而不必使用類似 C 程式語言等傳統程式撰寫的語法呢!

那,這麼說您可以瞭解了嗎?是的! shell script 可以簡單的被看成是批次檔, 也可以被說成是一個程式語言,且這個程式語言由於都是利用 shell 與相關工具指令, 所以不需要編譯即可執行,且擁有不錯的除錯 (debug) 工具,所以,他可以幫助系統管理員快速的管理好主機。


小標題的圖示 幹嘛學習 shell scripts?
這是個好問題,我又幹嘛一定要學 shell script ?我又不是資訊人,沒有寫程式的概念, 那我幹嘛還要學 shell script 呢?不要學可不可以啊?呵呵~如果 Linux 對您而言, 您只是想要『會用』而已,那麼,不需要學 shell script 也還無所謂,這部分先給他跳過去, 等到有空的時候,再來好好的瞧一瞧。但是,如果您是真的想要玩清楚 Linux 的來龍去脈, 那麼 shell script 就不可不知,為什麼呢?因為:

自動化管理的重要依據
不用鳥哥說您也知道,管理一部主機真不是件簡單的事情,每天要進行的任務就有: 查詢登錄檔、追蹤流量、監控使用者使用主機狀態、主機各項硬體設備狀態、 主機軟體更新查詢、更不要說得應付其他使用者的突然要求了。而這些工作, 您想要自行手動處理,還是寫個簡單的程式來幫您每日自動處理分析,若有問題才通知您呢? 當然是讓系統自動工作比較好,對吧!呵呵~這就得要良好的 shell script 來幫忙的啦!

追蹤與管理系統的重要工作
雖然我們還沒有提到服務啟動的方法,不過,這裡可以先提一下,我們 Linux 系統的服務 ( services ) 啟動的介面,在 /etc/init.d/ 這個目錄下,所有的檔案都是 scripts ; 另外,包括開機 (booting) 過程也都是利用 shell script 來幫忙搜尋系統的相關設定資料, 然後再代入各個服務的設定參數啊!舉例來說,如果我們想要重新啟動系統登錄檔, 可以使用:『/etc/init.d/syslogd restart』,那個 syslogd 檔案就是 script 啦! 另外,我曾經在某一代的 FC 上面發現,啟動 MySQL 這個資料庫服務時,確實是可以啟動的, 但是螢幕上卻老是出現『failure』,後來才發現,原來是啟動 MySQL 那個 script 會主動的以『空的密碼』去嘗試登入 MySQL ,但我修改過 MySQL 的密碼囉~當然就登入失敗~ 後來改了改 script ,就略去這個問題啦!如此說來, script 確實是需要學習的啊!

簡單入侵偵測功能
當我們的系統有異狀時,大多會將這些異狀記錄在系統記錄器,也就是我們常提到的『系統登錄檔』, 那麼我們可以在固定的幾分鐘內主動的去分析系統登錄檔,若察覺有問題,就立刻通報管理員, 或者是立刻加強防火牆的設定規則,如此一來,您的主機可就能夠達到『自我保護』的聰明學習功能啦~ 舉例來說,我們可以通過 shell script 去分析『當該封包嘗試幾次還是連線失敗之後,就予以抵擋住該 IP』之類的舉動,例如鳥哥寫過一個關於抵擋砍站軟體的 shell script , 就是用這個想法去達成的呢!

連續指令單一化
其實,對於新手而言, script 最簡單的功能就是:『 彙整一些在 command line 下達的連續指令,將他寫入 scripts 當中,而由直接執行 scripts 來啟動一連串的 command line 指令輸出入!』例如: 防火牆連續規則 ( iptables ),開機載入程序的項目 ( 就是在 /etc/rc.d/rc.local 裡頭的資料 ) ,等等都是相似的功能啦! 其實,說穿了,如果不考慮 program 的部分,那麼 scripts 也可以想成,僅是幫我們把一大串的指令彙整在一個檔案裡面, 而直接執行該檔案就可以執行那一串又臭又長的指令段!就是這麼簡單啦!

簡易的資料處理
由前一章 正規表示法 的 awk 程式說明中, 您可以發現, awk 可以用來處理簡單的數據資料呢!例如薪資單的處理啊等等的。 shell script 的功能更強大,例如鳥哥曾經用 shell script 直接處理數據資料的比對啊, 文字資料的處理啊等等的,撰寫方便,速度又快(因為在 Linux 效能較佳), 真的是很不錯用的啦!

跨平台支援與學習歷程較短
幾乎所有的 Unix Like 上面都可以跑 shell script ,連 MS Windows 系列也有相關的模擬器可以用, 此外, shell script 的語法是相當親和的,看都看的懂得文字,而不是機器碼, 很容易學習~這些都是您可以加以考量的學習點啊!

上面這些都是您考慮學習 shell script 的特點~此外, shell script 還可以簡單的以 vi 來直接編寫,實在是很方便的好東西!所以,還是建議您學習一下啦。

不過,雖然 shell script 號稱是程式 (program) ,但實際上, shell script 處理資料的速度上是不太夠的。因為 shell script 用的是外部的指令與 bash shell 的一些預設工具,所以,他常常會去呼叫外部的函式庫,因此,運算速度上面當然比不上傳統的程式語言。 所以囉, shell script 用在系統管理上面是很好的一項工具,但是用在處理大量數值運算上, 就不夠好了~而且還很麻煩,因為: Shell scripts 的速度較慢, 且使用的 CPU 資源較多,造成主機資源的分配不良。還好, 我們確實很少看到利用 shell script 在進行大量數據運算的,所以,不必擔心的啦!


小標題的圖示 第一支 script 的撰寫與執行
如同前面講到的, shell script 其實就是純文字檔 (ASCII) ,我們可以編輯這個檔案, 然後讓這個檔案來幫我們一次執行多個指令,或者是利用一些運算與邏輯判斷來幫我們達成某些功能。 所以啦,要編輯這個檔案的內容時,當然就需要具備有 bash shell 指令下達的相關認識。 我們說過,要下達指令需要注意的事項在 bash 章節內已經提過, 在 shell script 的撰寫同樣需要用到這些注意事項的: 如同前面 bash command 提到的,指令與參數間的多個空白會被忽略掉; 而空白行也將被忽略掉!,並且 [tab] 也是不會被理會的! 如果讀取到一個 Enter 符號 ( CR )),就嘗試開始執行該行命令; 至於如果一行的內容太多,則可以使用 /[Enter] 來延伸至下一行; 此外,使用最多的 # 可做為註解!任何加在 # 後面的字,將全部被視為註解文字而被忽略! 如此一來,我們在 script 內所撰寫的程式,就會被一行一行的執行。好了,那麼這個程式假設檔名是 shell.sh 好了,如何執行這個檔案?很簡單,可以有底下幾個方法: 將 shell.sh 加上可讀與執行 (rx) 的權限,然後就能夠以 ./shell.sh 來執行了; 直接以 sh shell.sh 的方式來直接執行即可。反正重點就是要讓那個 shell.sh 內的指令可以被執行的意思啦!咦!那我為何需要使用 ./shell.sh 來下達指令? 還記得我們在 bash 裡面一直強調的,指令是否能夠被執行與 PATH 這個環境變數有關, 所以,要執行『目前這個目錄下的某個檔案』就需要加上 ./ 這個目錄啦!另外,其實您也可以將 shell.sh 放在您家目錄下的 ~/bin 這個目錄中,然後利用 PATH="$PATH":~/bin 的設定, 嘿嘿,就能夠直接執行您的 script 囉~ ^_^

那,為何 sh shell.sh 也可以執行呢?這是因為 /bin/sh 其實就是 /bin/bash , 使用 sh shell.sh 亦即告訴系統,我想要直接以 bash 的功能來執行 shell.sh 這個檔案內的相關指令的意思。 而我們也可以利用 sh 的參數,如 -n 及 -x 來檢查與追蹤 shell.sh 的語法是否正確呢! ^_^


撰寫第一支 script
不論是那個門派,要學武功要從掃地做起,那麼要學程式呢?呵呵,肯定是由『秀出 Hello World!』 這個字眼開始的!OK!那麼鳥哥就先寫一支 script 給大家瞧一瞧:
[root@linux ~]# 
[root@linux scripts]# 

在我們這個章節當中,請將所有的撰寫的 script 放置到您家目錄的 ~/scripts 這個目錄內, 比較好管理啦!上面的寫法當中,我主要將整個程式的撰寫分成數段,大致是這樣:
  1. 第一行 #!/bin/bash 在宣告這個 script 使用的 shell 名稱:
    因為我們使用的是 bash ,所以,必須要以『 #!/bin/bash 』來宣告這個檔案內的語法使用 bash 的語法!那麼當這個程式被執行時,他就能夠載入 bash 的相關環境設定檔, 並且執行 bash 來使我們底下的指令能夠執行!這很重要的!(在很多狀況中,如果沒有設定好這一行, 那麼該程式很可能會無法執行,因為系統可能無法判斷該程式需要使用什麼 shell 來執行啊!)

  2. 程式內容的宣告:
    整個 script 當中,除了第一行的 #! 是用來宣告 shell 的之外,其他的 # 都是『註解』用途! 所以上面的程式當中,第二行以下就是用來說明整個程式的狀態。一般來說, 建議您一定要養成說明該 script 的:1. 內容與功能; 2. 版本資訊; 3. 作者與聯絡方式; 4. 建檔日期;5. 歷史紀錄 等等。這將有助於未來程式的改寫與 debug 呢!

  3. 主要環境變數的宣告:
    建議務必要將一些重要的環境變數設定好,鳥哥個人認為, PATH 是當中最重要的! 如此一來,則可讓我們這支程式在進行時,可以直接下達指令, 而不必寫絕對路徑呢!比較好啦!

  4. 主要程式部分
    就將主要的程式寫好即可!在這個例子當中,就是 echo 那一行啦!

  5. 執行成果告知
    是否記得我們在 bash 裡面要討論一個指令的執行成功與否,可以使用 $? 這個變數來觀察~ 那麼我們也可以利用 exit 這個指令來讓程式中斷,並且回傳一個數值給系統。 在我們這個例子當中,我使用 exit 0 ,這代表離開 script ,並且回傳一個 0 給系統, 所以我執行完這個 script 後,若接著下達 echo $? 則可得到 0 的值喔! 更聰明的讀者應該也知道了,呵呵!利用這個 exit n 的功能,我們還可以自訂錯誤訊息, 讓這支程式變得更加的 smart 呢!
接下來執行看看結果是怎樣吧?
[root@linux scripts]# 
Hello World !

您會看到螢幕是這樣,而且應該還會聽到『咚』的一聲,為什麼呢?還記得前一章提到的 printf 吧?用 echo 接著那些特殊的按鍵也可以發生同樣的事情~ 不過, echo 必須要加上 -e 的參數才行! 呵呵!在您寫完這個小 script 之後,您就可以大聲的說:『我也會寫程式了』!哈哈! 很簡單有趣吧~ ^_^

另外,你也可以利用:『chmod a+x sh01.sh; ./sh01.sh』來執行這個 script 的呢!

小標題的圖示 撰寫 shell script 的良好習慣建立
一個良好習慣的養成是很重要的~大家在剛開始撰寫程式的時候,最容易忽略這部分, 認為程式寫出來就好了,其他的不重要。其實,如果程式的說明能夠更清楚, 那麼對您自己是有很大的幫助的。

舉例來說,鳥哥自己為了自己的需求,曾經撰寫了不少的 script 來幫我進行主機 IP 的偵測啊、 登錄檔分析與管理啊、自動上傳下載重要設定檔啊等等的,不過,早期就是因為太懶了, 管理的主機又太多了,常常同一個程式在不同的主機上面進行更改,到最後,到底哪一支才是最新的都記不起來, 而且,重點是,我到底是改了哪裡??為什麼做那樣的修改?都忘的一乾二淨~真要命~

所以,後來鳥哥在寫程式的時候,通常會比較仔細的將程式的設計過程給他記錄下來, 而且還會記錄一些歷史紀錄,如此一來,好多了~ 至少很容易知道我修改了哪些資料,以及程式修改的理念與邏輯概念等等, 在維護上面是輕鬆很多很多的喔!

另外,在一些環境的設定上面,畢竟每個人的環境都不相同,為了取得較佳的執行環境, 我都會自行先定義好一些一定會被用到的環境變數,例如 PATH 這個玩意兒! 這樣比較好啦~所以說,建議您一定要養成良好的 script 撰寫習慣, 在每個 script 的檔頭處記錄好: script 的功能; script 的版本資訊; script 的作者與聯絡方式; script 的版權宣告方式; script 的 History (歷史紀錄); script 內較特殊的指令,使用絕對路徑的方式來下達; script 運作時需要的環境變數預先宣告與設定。

大標題的圖示 簡單的 shell script 練習
在第一支 shell script 撰寫完畢之後,相信您應該具有基本的撰寫功力了。 接下來,在開始更深入的程式概念之前,我們先來玩一些比較有趣的簡單的小範例好了。 底下的範例中,達成結果的方式相當的多,建議您先自行撰寫看看,寫完之後再與鳥哥寫的內容比對, 這樣才能更加深概念喔!好!不囉唆,我們就一個一個來玩吧!


變數內容由使用者決定
很多時候我們需要使用者輸入一些內容,好讓程式可以順利運作。 簡單的來說,大家應該都有安裝過軟體的經驗,安裝的時候,他不是會問您『要安裝到那個目錄去?』嗎? 那個讓使用者輸入的資料的動作,就是讓使用者輸入變數內容啦。

你應該還記得在 bash 的時候,我們有學到一個 read 指令吧?忘記的話,請自行回頭去閱讀一番。 現在,請你以 read 指令的用途,撰寫一個 script ,他可以讓使用者輸入:1 first name 與 2. last name, 最後並且在螢幕上顯示:『Your full name is: 』的內容:
[root@linux scripts]# 

將上面這個 sh02.sh 執行一下,你就能夠發現使用者自己輸入的變數可以被取用的哩! 很不錯吧!加油!


利用 date 進行檔案的建立
想像一個狀況,如果我每天要進行備份,而備份的資料又不想被覆蓋掉,也就是說, 我想要將每天備份的資料放在不同的檔案中。哇!這真困擾啊?難道要我每天去修改 script ? 不需要啊!因為每天的『日期』並不相同,所以我可以將檔名取成類似: backup.20050802 , 不就可以每天一個不同檔名了嗎?呵呵!確實如此。好了,接下來出個例子: 我想要建立三個空的檔案,檔名最開頭由使用者輸入決定,假設使用者輸入 filename 好了, 那今天的日期是 2005/08/23 ,我想要以前天、昨天、今天的日期來建立這個檔案,亦即 filename_20050821, filename_20050822, filename_20050823 ,該如何是好?
[root@linux scripts]# 

我透過一些簡單的動作,這些動作都可以在 bash 那一章裡面找到, 包括小指令 (`) 的取得訊息、變數的設定功能、變數的累加以及利用 touch 指令輔助! 如果您開始執行這個 sh03.sh 之後,你可以進行兩次輸入,一次直接按 [Enter] 來查閱檔名是啥? 一次可以輸入一些字元,這樣來判斷你的檔案喔!關於 date 的指令應用,請 man date 吧! ^_^


數值運算的方法
各位看官應該還記得,我們可以使用 declare 來定義變數的類型吧?! 這樣才能夠進行加減運算啊!可惜的是, bash shell 裡頭預設僅支援到整數的資料。 OK!那我們來玩玩看,如果我們要使用者輸入兩個變數,然後將兩個變數的內容相乘, 最後輸出相乘的結果,那可以怎麼做?
[root@linux scripts]# 

在數字的運算上,我們可以使用『 declare -i total=$firstnu*$secnu 』 也可以使用上面的方式來進行!基本上,鳥哥比較建議使用這樣的方式來進行運算: var=$((運算內容))不但容易記憶,而且也比較方便的多~未來您可以使用這種方式來計算的呀!至於數值運算上的處理, 則有: +, -, *, /, %等等。 那個 % 是取餘數啦~舉例來說, 13 對 3 取餘數,結果是 13=4*3+1,所以餘數是 1 啊!就是:
[root@linux scripts]# 
1
這樣瞭解了吧?!多多學習與應用喔! ^_^

大標題的圖示 善用判斷式
在 bash 章節中,我們提到過 $? 這個變數所代表的意義, 此外,也透過 && 及 || 來作為前一個指令是否能夠成功進行的一個參考。 那麼,如果我想要知道 /dmtsai 這個目錄是否存在時,難道一定要使用 ls 來執行, 然後再以 $? 來判斷執行成果嗎?呵呵!當然不需要! 我們可以透過『 test 』這個指令來偵測呢!


小標題的圖示 利用 test 指令的測試功能
當我要檢測系統上面某些檔案或者是相關的屬性時,利用 test 這個指令來工作, 真是好用得不得了,舉例來說,我要檢查 /dmtsai 是否存在時,使用:
[root@linux ~]# 
執行結果並不會顯示任何訊息,但最後我們可以透過 $? 或 && 及 || 來展現整個結果呢! 例如我們在將上面的例子改寫成這樣:
[root@linux ~]# 
最終的結果可以告知我們是『exist』還是『Not exist』呢!那我知道 -e 是測試一個『東西』在不在, 如果還想要測試一下該檔名是啥玩意兒時,還有哪些標誌可以來判斷的呢?呵呵!有底下這些東西喔!

測試的標誌 代表意義
1. 關於某個檔名的『類型』偵測(存在與否),如 test -e filename
-e 該『檔名』是否存在?(常用)
-f 該『檔名』是否為檔案(file)?(常用)
-d 該『檔名』是否為目錄(directory)?(常用)
-b 該『檔名』是否為一個 block device 裝置?
-c 該『檔名』是否為一個 character device 裝置?
-S 該『檔名』是否為一個 Socket 檔案?
-p 該『檔名』是否為一個 FIFO (pipe) 檔案?
-L 該『檔名』是否為一個連結檔?
2. 關於檔案的權限偵測,如 test -r filename
-r 偵測該檔名是否具有『可讀』的屬性?
-w 偵測該檔名是否具有『可寫』的屬性?
-x 偵測該檔名是否具有『可執行』的屬性?
-u 偵測該檔名是否具有『SUID』的屬性?
-g 偵測該檔名是否具有『SGID』的屬性?
-k 偵測該檔名是否具有『Sticky bit』的屬性?
-s 偵測該檔名是否為『非空白檔案』?
3. 兩個檔案之間的比較,如: test file1 -nt file2
-nt (newer than)判斷 file1 是否比 file2 新
-ot (older than)判斷 file1 是否比 file2 舊
-ef 判斷 file2 與 file2 是否為同一檔案,可用在判斷 hard link 的判定上。 主要意義在判定,兩個檔案是否均指向同一個 inode 哩!
4. 關於兩個整數之間的判定,例如 test n1 -eq n2
-eq 兩數值相等 (equal)
-ne 兩數值不等 (not equal)
-gt n1 大於 n2 (greater than)
-lt n1 小於 n2 (less than)
-ge n1 大於等於 n2 (greater than or equal)
-le n1 小於等於 n2 (less than or equal)
5. 判定字串的資料
test -z string 判定字串是否為 0 ?若 string 為空字串,則為 true
test -n string 判定字串是否非為 0 ?若 string 為空字串,則為 false。
註: -n 亦可省略
test str1 = str2 判定 str1 是否等於 str2 ,若相等,則回傳 true
test str1 != str2 判定 str1 是否不等於 str2 ,若相等,則回傳 false
6. 多重條件判定,例如: test -r filename -a -x filename
-a (and)兩狀況同時成立!例如 test -r file -a -x file,則 file 同時具有 r 與 x 權限時,才回傳 true。
-o (or)兩狀況任何一個成立!例如 test -r file -o -x file,則 file 具有 r 或 x 權限時,就可回傳 true。
! 反相狀態,如 test ! -x file ,當 file 不具有 x 時,回傳 true

OK!現在我們就利用 test 來幫我們寫幾個簡單的例子。首先,判斷一下, 讓使用者輸入一個檔名,我們判斷:
  1. 這個檔案是否存在,若不存在則給予一個『Filename does not exist』的訊息,並中斷程式;
  2. 若這個檔案存在,則判斷他是個檔案或目錄,結果輸出『Filename is regular file』或 『Filename is directory』
  3. 判斷一下,執行者的身份對這個檔案或目錄所擁有的權限,並輸出權限資料!
你可以先自行創作看看,然後再跟底下的結果討論討論。注意利用 test 與 && 還有 || 等標誌!
[root@linux scripts]# 

很有趣的例子吧!您可以自行再以其他的案例來撰寫一下可用的功能呢!


小標題的圖示 利用判斷符號 [ ]
除了我們很喜歡使用的 test 之外,其實,我們還可以利用判斷符號『 [ ] 』來進行資料的判斷呢! 舉例來說,如果我想要知道 $HOME 這個變數是否為空的,可以這樣做:
[root@linux ~]# 
但使用 [] 要特別注意的是,在上述的每個元件中間都需要有空白鍵來分隔,假設我空白鍵使用『□』來表示, 那麼,在這些地方你都需要有空白鍵:
[  "$HOME"  ==  "$MAIL"  ]
[□"$HOME"□==□"$MAIL"□]
 ↑       ↑  ↑       ↑
上面的例子在說明,兩個字串 $HOME 與 $MAIL 是否相同的意思,相當於 test $HOME = $MAIL 的意思啦! 而如果沒有空白分隔,例如 [$HOME==$MAIL] 時,我們的 bash 就會顯示錯誤訊息了!這可要很注意啊! 所以說,您最好要注意: 在中括號 [] 內的每個元件都需要有空白鍵來分隔; 在中括號內的變數,最好都以雙引號來設定; 在中括號內的常數,最好都以單或雙引號來設定。舉例來說,假如我設定了 name="VBird Tsai" ,然後這樣判定:
[root@linux ~]# 
[root@linux ~]# 
bash: [: too many arguments
為什麼呢?因為 $name 如果沒有使用雙引號刮起來,那麼上面的判定式會變成: [ VBird Tsai == "VBird" ] 而不是我們要的: [ "VBird Tsai" == "VBird" ] 這可是差很多的喔!另外,中括號的使用方法與標誌與 test 幾乎一模一樣啊~ 只是中括號比較常用在條件判斷式 if ..... then ..... fi 的情況中就是了。 好,那我們也繼續來做一個小案例好了:
  1. 當執行一個程式的時候,這個程式會讓使用者選擇 Y 或 N ,
  2. 如果使用者輸入 Y 或 y 時,就顯示『 OK, continue 』
  3. 如果使用者輸入 n 或 N 時,就顯示『 Oh, interrupt !』
  4. 如果不是 Y/y/N/n 之內的其他字元,就顯示『I don't know what is your choice』
利用中括號、 && 與 || 來繼續吧!
[root@linux scripts]# 

很有趣吧!利用這個字串判別的方法,我們就可以很輕鬆的將使用者想要進行的工作分門別類呢! 接下來,我們再來談一些其他有的沒有的東西吧!
Tips:
為什麼判斷式裡面下達等於要用 == 而不是一個 = 就好了呢?我們在前一章正規表示法裡面的 awk 提到, 只有一個 = 用來給予一個變數設定其內容,邏輯判斷時,則會給予兩個等於, 亦即『比較』而非『設定』的意思~這裡要好好的分辨一下喔! ^_^
鳥哥的圖示

小標題的圖示 Shell script 的預設變數($0, $1...)
其實,當我們執行一個 shell script 時,在這個 shell script 裡面就已將幫我們做好一些可用的變數了。 舉例來說,在不久的將來,您就會發現,當我們要啟動一個系統服務時,可能會下達類似這樣的指令:
[root@linux ~]# 
那是啥玩意兒?呵呵!就是『向 /etc/init.d/crond 這個 script 下達 restart 的指令』, 咦!我們不是都使用 read 來讀取使用者輸入的變數內容嗎?為啥我可以直接在 script 後面接上這個參數? 這是因為 shell script 幫我們設定好一些指定的變數了!變數的對應是這樣的:


這樣夠清楚了吧?!執行的檔名為 $0 這個變數,第一個接的參數就是 $1 啊~ 所以,只要我們在 script 裡面善用 $1 的話,就可以很簡單的立即下達某些指令功能了! 好了,來做個例子吧~假設我要執行一個 script ,執行後,該 script 會自動列出自己的檔名, 還有後面接的前三個參數,該如何是好?
[root@linux scripts]# 

這支程式裡面鳥哥加上了一些控制式,亦即利用 && 及 || 來加以判斷 $1 ~ $3 是否存在? 若存在才顯示,若不存在就中斷~執行結果如下:
[root@linux scripts]# 
The script name is ==> sh07.sh
The 1st paramter is ==> theone
The 2nd paramter is ==> haha
The 3th paramter is ==> quot
上面這7個例子都很簡單吧?幾乎都是利用 bash 的相關功能而已~ 不難啦~底下我們就要使用條件判斷式來進行一些分別功能的設定了,好好瞧一瞧先~

大標題的圖示 條件判斷式:
只要講到『程式』的話,那麼條件判斷式,亦即是『 if then 』這種判別式肯定一定要學習的! 因為很多時候,我們都必須要依據某些資料來判斷程式該如何進行。舉例來說,我們在上頭不是有練習當使用者輸入 Y/N 時,必須要執行不同的訊息輸出嗎?簡單的方式可以利用 && 與 || ,但如果我還想要執行一堆指令呢? 那真的得要 if then 來幫忙囉~底下我們就來聊一聊!


小標題的圖示 利用 if .... then
這個 if .... then 是最常見的條件判斷式了~簡單的說,就是當符合某個條件判斷的時候, 就予以進行某項工作就是了。我們可以簡單的這樣看:
 條件判斷式 
	當條件判斷式成立時,可以進行的指令工作內容;

至於條件判斷式的判斷方法,與前一小節的介紹相同啊!較特別的是,如果我有多個條件要判別時, 除了 sh06.sh 那個案例,也就是將多個條件寫入一個中括號內的情況之外, 我還可以有多個中括號來隔開喔!而括號與括號之間,則以 && 或 || 來隔開,他們的意義是: && 代表 AND ; || 代表 or ; 所以,在使用中括號的判斷式中, && 及 || 就與指令下達的狀態不同了。舉例來說, sh06.sh 那個例子我可以改寫成這樣:
[root@linux scripts]# 

不過,由這個例子看起來,似乎也沒有什麼了不起吧? sh06.sh 還比較簡單呢~ 但是,如果我們考慮底下的狀態,您就會知道 if then 的好處了:
 條件判斷式 
	當條件判斷式成立時,可以進行的指令工作內容;

	當條件判斷式不成立時,可以進行的指令工作內容;

如果考慮更複雜的情況,則可以使用這個語法:
 條件判斷式一 
	當條件判斷式一成立時,可以進行的指令工作內容;
 條件判斷式二 
	當條件判斷式二成立時,可以進行的指令工作內容;

	當條件判斷式一與二均不成立時,可以進行的指令工作內容;

那我就可以將 sh06-2.sh 改寫成這樣:
[root@linux scripts]# 

是否程式變得很簡單,而且依序判斷,可以避免掉重複判斷的狀況,這樣真的很容易設計程式的啦! ^_^ 好了,那麼如果我要偵測你所輸入的參數是否為 hello 呢 , 也就是說,如果我想要知道,你在程式後面所接的第一個參數 (就是 $1 啊!) 是否為 hello ,
  1. 如果是的話,就顯示 "Hello, how are you ?";
  2. 如果沒有加任何參數,就提示使用者必須要使用的參數下達法;
  3. 而如果加入的參數不是 hello ,就提醒使用者僅能使用 hello 為參數。
整個程式的撰寫可以是這樣的:
[root@linux scripts]# 

然後您可以執行這支程式,分別在 $1 的位置輸入 hello, 沒有輸入與隨意輸入, 就可以看到不同的輸出囉~是否還覺得挺簡單的啊! ^_^。事實上, 學到這裡,也真的很厲害了~好了,底下我們繼續來玩一些比較大一點的囉~ 我們在前一章已經學會了 grep 這個好用的玩意兒,那麼多學一個叫做 netstat 的指令, 這個指令可以查詢到目前主機有開啟的網路服務埠口 (service ports), 相關的功能我們會在伺服器架設篇繼續介紹,這裡您只要知道,我可以利用『 netstat -tuln 』來取得目前主機有啟動的服務, 而且取得的資訊有點像這樣:
[root@linux ~]# 
Active Internet connections (only servers)
Proto Recv-Q Send-Q Local Address   Foreign Address    State
tcp        0      0 0.0.0.0:     0.0.0.0:*          LISTEN
tcp        0      0 :::           :::*               LISTEN
tcp        0      0 :::           :::*               LISTEN
tcp        0      0 :::           :::*               LISTEN
上面的重點是特殊字體的那個部分,那些特殊字體的部分代表的就是 port 囉~ 那麼每個 port 代表的意義為何呢?幾個常見的 port 與相關網路服務的關係是: 80: WWW 22: ssh 21: ftp 25: mail那我如何透過 netstat 去偵測我的主機是否有開啟這四個主要的網路服務埠口呢? 我可以簡單的這樣去寫這個程式喔:
[root@linux scripts]# 

這樣又能夠一個一個的檢查囉~是否很有趣啊! ^_^。接下來,我們再來玩更難一點的。 我們知道可以利用 date 來顯示日期與時間,也可以利用 $((計算式)) 來計算數值運算。 另外, date 也可以用來顯示自 19710101 以來的『總秒數』 (請自行查閱 man date 及 info date) 。那麼,您是否可以撰寫一支小程式,用來『計算退伍日期還剩幾天?』也就是說:
  1. 先讓使用者輸入他們的退伍日期;
  2. 再由現在日期比對退伍日期;
  3. 由兩個日期的比較來顯示『還需要幾天』才能夠退伍的字樣。
似乎挺難的樣子?其實也不會啦,利用『 date --date="YYYYMMDD" +%s 』就能夠達到我們所想要的囉~如果您已經寫完了程式,對照底下的寫法試看看:
[root@linux scripts]# 

瞧一瞧,這支程式可以幫您計算退伍日期呢~如果是已經退伍的朋友, 還可以知道已經退伍多久了~哈哈!很可愛吧~利用 date 算出自 1971/01/01 以來的總秒數, 再與目前的總秒數來比對,然後以一天的總秒數 (60*60*24) 為基數去計算總日數, 就能夠得知兩者的差異了~瞧~全部的動作都沒有超出我們所學的範圍吧~ ^_^ 還能夠避免使用者輸入錯誤的數字,所以多了一個正規表示法的判斷式呢~ 這個例子比較難,有興趣想要一探究竟的朋友,可以作一下 課後練習題 關於計算生日的那一題喔!~加油!


小標題的圖示 利用 case ..... esac 判斷
上個小節提到的『 if .... then .... fi 』對於變數的判斷中, 是以比對的方式來分辨的,如果符合狀態就進行某些行為,並且透過較多層次 ( 就是 elif ... ) 的方式來進行多個變數的程式碼撰寫,譬如 sh08.sh 那個小程式,就是用這樣的方式來的囉。 好,那麼萬一我有多個既定的變數內容,例如 sh08.sh 當中,我所需要的變數就是 "hello" 及空字串兩個, 那麼我只要針對這兩個變數來設定狀況就好了對吧?!那麼可以使用什麼方式來設計呢? 呵呵~就用 case ... in .... esac 吧~,他的語法如下:
 $變數名稱 
  "第一個變數內容"
	程式段
	
  "第二個變數內容"
	程式段
	
  *
	不包含第一個變數內容與第二個變數內容的其他程式執行段
	exit 1
	

要注意的是,這個語法是以 case 為開頭,而以 esac 為結尾,啥?為何是 esac 呢?想一想,既然 if 的結尾是 fi ,那麼 case 的結尾當然就是將 case 倒著寫,自然就是 esac 囉~ ^_^,很好記吧~ 另外,每一個變數內容的程式段最後都需要兩個分號 (;;) 來代表該程式段落的結束,這挺重要的喔! 至於為何需要有 * 這個變數內容在最後呢?這是因為,如果使用者不是輸入變數內容一或二時, 我們可以告知使用者相關的資訊啊!舉例來說,我們如果將 sh08.sh 改寫的話, 他應該會變成這樣喔!
[root@linux scripts]# 

在上面這個 sh08-2.sh 的案例當中,如果你輸入『 sh sh08-2.sh test 』來執行, 那麼螢幕上就會出現『Usage sh08-2.sh {hello}』的字樣,告知執行者僅能夠使用 hello 喔~ 這樣的方式對於需要某些固定字串來執行的變數內容就顯的更加的方便呢? 這種方式您真的要熟悉喔!這是因為系統的很多服務的啟動 scripts 都是使用這種寫法的, 舉例來說,我們 Linux 的服務啟動放置目錄是在 /etc/init.d/ 當中,我已經知道裡頭有個 syslog 的服務,我想要重新啟動這個服務,可以這樣做: /etc/init.d/syslog restart重點是那個 restart 啦~如果您進入 /etc/init.d/syslog 就會看到他使用的是 case 語法, 並且會規定某些既定的變數內容,你可以直接下達 /etc/init.d/syslog , 該 script 就會告知你有哪些後續接的變數可以使用囉~方便吧! ^_^

一般來說,使用『 case $變數 in 』這個語法中,當中的那個 $變數 大致有兩種取得的方式:
  • 直接下達式:例如上面提到的,利用『 script.sh variable 』 的方式來直接給予 $1 這個變數的內容,這也是在 /etc/init.d 目錄下大多數程式的設計方式。
  • 互動式:透過 read 這個指令來讓使用者輸入變數的內容。
這麼說或許您的感受性還不高,好,我們直接寫個程式來玩玩:讓使用者能夠輸入 one, two, three , 並且將使用者的變數顯示到螢幕上,如果不是 one, two, three 時,就告知使用者僅有這三種選擇。
[root@linux scripts]# 

此時,您可以使用『 sh sh11.sh two 』的方式來下達指令,就可以收到相對應的回應了。 上面使用的是直接下達的方式,而如果使用的是互動式時,那麼將上面第 10, 11 行的 "#" 拿掉, 並將 12 行加上註解 (#),就可以讓使用者輸入參數囉~這樣是否很有趣啊?!


小標題的圖示 利用 function 功能
什麼是『 函數 (function)』功能啊?簡單的說,其實, 函數可以在 shell script 當中做出一個類似自訂執行指令的東西,最大的功能是, 可以簡化我們很多的程式碼~舉例來說,上面的 sh11.sh 當中,每個輸入結果 one, two, three 其實輸出的內容都一樣啊~那麼我就可以使用 function 來簡化了! function 的語法是這樣的:
function fname() {
	程式段
}
那個 fname 就是我們的自訂的執行指令名稱~而程式段就是我們要他執行的內容了。 要注意的是,在 shell script 當中, function 的設定一定要在程式的最前面, 這樣才能夠在執行時被找到可用的程式段喔!好~我們將 sh11.sh 改寫一下:
[root@linux scripts]# 

以上面的例子來說,我做了一個函數名稱為 printit ,所以,當我在後續的程式段裡面, 只要執行 printit 的話,就表示我的 shell script 要去執行『 function printit .... 』 裡面的那幾個程式段落囉! 當然囉,上面這個例子舉得太簡單了,所以您不會覺得 function 有什麼好厲害的, 不過,如果某些程式碼一再地在 script 當中重複時,這個 function 可就重要的多囉~ 不但可以簡化程式碼,而且可以做成類似『模組』的玩意兒,真的很棒啦!

另外, function 也是擁有內建變數的~他的內建變數與 shell script 很類似, 函數名稱代表示 $0 ,而後續接的變數也是以 $1, $2... 來取代的~ 這裡很容易搞錯喔~因為『 function fname() { 程式段 } 』內的 $0, $1... 等等與 shell script 的 $0 是不同的。以上面 sh11-2.sh 來說,假如我下達:『 sh sh11-2.sh one 』 這表示在 shell script 內的 $1 為 "one" 這個字串。但是在 printit() 內的 $1 則與這個 one 無關。 我們將上面的例子再次的改寫一下,讓您更清楚!
[root@linux scripts]# 

在上面的例子當中,如果您輸入『 sh sh11-3.sh one 』就會出現『 Your choice is 1 』的字樣~ 為什麼是 1 呢?因為在程式段落當中,我們是寫了『 printit 1 』那個 1 就會成為 function 當中的 $1 喔~ 這樣是否理解呢? function 本身其實比較困難一點,如果您還想要進行其他的撰寫的話。 不過,我們僅是想要更加瞭解 shell script 而已,所以,這裡看看即可~瞭解原理就好囉~ ^_^

大標題的圖示 迴圈 (loop)
除了 if...then...fi 這種條件判斷式之外,迴圈可能是程式當中最重要的一環了~ 迴圈可以不斷的執行某個程式段落,直到使用者設定的條件達成為止。 所以,重點是那個『條件的達成』是什麼。底下我們就來談一談:


小標題的圖示 while do done, until do done
一般來說,迴圈最常見的就是底下這兩種狀態了:
 condition 
	程式段落

這種方式中, while 是『當....時』,所以,這種方式說的是『 當 condition 條件成立時,就進行迴圈,直到 condition 的條件不成立才停止』的意思。
 condition 
	程式段落

這種方式恰恰與 while 相反,它說的是『 當 condition 條件成立時,就終止迴圈, 否則就持續進行迴圈的程式段。』是否剛好相反啊~我們以 while 來做個簡單的練習好了。 假設我要讓使用者輸入 yes 或者是 YES 才結束程式的執行,否則就一直進行告知使用者輸入字串。
[root@linux scripts]# 

上面這個例題的說明是『當 $yn 這個變數不是 "yes" 且 $yn 也不是 "YES" 時,才進行迴圈內的程式。』 而如果 $yn 是 "yes" 或 "YES" 時,就會離開迴圈囉~那如果使用 until 呢?呵呵有趣囉~ 他的條件會變成這樣:
[root@linux scripts]# 

仔細比對一下這兩個東西有啥不同喔! ^_^再來,如果我想要計算 1+2+3+....+100 這個數據呢? 利用迴圈啊~他是這樣的:
[root@linux scripts]# 

嘿嘿!當您執行了『 sh sh13.sh 』之後,就可以得到 5050 這個數據才對啊!這樣瞭呼~ 那麼讓您自行做一下,如果想要讓使用者自行輸入一個數字,讓程式由 1+2+... 直到您輸入的數字為止, 該如何撰寫呢?應該很簡單吧?!答案可以參考一下習題練習裡面的一題喔!


小標題的圖示 for...do....done
相對於 while, until 的迴圈方式是必須要『符合某個條件』的狀態, for 這種語法,則是『 已經知道要進行幾次迴圈』的狀態!他的語法是:
 初始值 限制值 執行步階 
	程式段

這種語法適合於數值方式的運算當中,在 for 後面的括號內的三串內容意義為: 初始值:某個變數在迴圈當中的起始值,直接以類似 i=1 設定好; 限制值:當變數的值在這個限制值的範圍內,就繼續進行迴圈。例如 i<=100; 執行步階:每作一次迴圈時,變數的變化量。例如 i=i+1。值得注意的是,在『執行步階』的設定上,如果每次增加 1 ,則可以使用類似『i++』的方式,亦即是 i 每次迴圈都會增加一的意思。好,我們以這種方式來進行 1 累加到 100 的迴圈吧!
[root@linux scripts]# 

一樣也是很簡單吧!利用這個 for 則可以直接限制迴圈要進行幾次呢!這麼好用的東西難道只能在數值方面動作? 當然不是啦~我們還可以利用底下的方式來進行非數字方面的迴圈運作喔!
 var  con1 con2 con3 ...

	程式段

以上面的例子來說,這個 $var 的變數內容在迴圈工作時:
  1. 第一次迴圈時, $var 的內容為 con1 ;
  2. 第二次迴圈時, $var 的內容為 con2 ;
  3. 第三次迴圈時, $var 的內容為 con3 ;
  4. ....
我們可以做個簡單的練習。假設我有三種動物,分別是 dog, cat, elephant 三種, 我想每一行都輸出這樣:『There are dogs...』之類的字樣,則可以:
[root@linux scripts]# 

很簡單是吧! ^_^。好了,那麼如果我想要讓使用者輸入某個目錄, 然後我找出某目錄內的檔名的權限呢?又該如何是好?呵呵!可以這樣做啦~
[root@linux scripts]# 

呵呵!很有趣的例子吧~利用這種方式,您可以很輕易的來處理一些檔案的特性呢~ 我們迴圈就介紹到這裡了~其他更多的應用,就得視您的需求來玩囉~。

大標題的圖示 shell script 的追蹤與 debug
scripts 在執行之前,最怕的就是出現問題了!那麼我們如何 debug 呢?有沒有辦法不需要透過直接執行該 scripts 就可以來判斷是否有問題呢!?呵呵! 當然是有的!我們就直接以 bash 的相關參數來進行判斷吧!
[root@linux ~]# 



[root@linux ~]# 



[root@linux ~]# 
+ PATH=/bin:/sbin:/usr/bin:/usr/sbin:/usr/local/bin:/usr/local/sbin:/home/vbird/bin
+ export PATH
+ for animal in dog cat elephant
+ echo 'There are dogs.... '
There are dogs....
+ for animal in dog cat elephant
+ echo 'There are cats.... '
There are cats....
+ for animal in dog cat elephant
+ echo 'There are elephants.... '
There are elephants....

在上面的範例二當中,我們可以透過這個簡單的參數 -x 來達成 debug 的目的,這可是一個不可多得的參數, 通常如果您執行 script 卻發生問題時,利用這個 -x 參數,就可以知道問題是發生在哪一行上面了!

熟悉 sh 的用法,將可以使您在管理 Linux 的過程中得心應手!至於在 Shell scripts 的學習方法上面,需要『 多看、多模仿、並加以修改成自己的樣式!』 是最快的學習手段了!網路上有相當多的朋友在開發一些相當有用的 scripts ,若是您可以將對方的 scripts 拿來,並且改成適合自己主機的樣子!那麼學習的效果會是最快的呢!

另外,我們 Linux 系統本來就有很多的啟動 script ,如果您想要知道每個 script 所代表的功能是什麼? 可以直接以 vi 進入該 script 去查閱一下,通常立刻就知道該 script 的目的了。 舉例來說,我們的 Linux 裡頭有個檔案名稱為: /etc/init.d/portmap ,這個 script 是幹嘛用的? 利用 vi 去查閱最前面的幾行字,他出現如下資訊:
# description: The portmapper manages RPC connections, which are used by /
#              protocols such as NFS and NIS. The portmap server must be /
#              running on machines which act as servers for protocols which /
#              make use of the RPC mechanism.
# processname: portmap
簡單的說,他是被用在 NFS 與 NIS 上面的一個啟動 RPC 的 script , 然後我們再利用 http://www.google.com.tw 去搜尋一下 NFS, NIS 與 RPC , 立刻就能夠知道這個 script 的功能囉~所以,下次您發現不明的 script 時, 如果是系統提供的,那麼利用這個檢查的方式,一定可以約略瞭解的啦! 加油的囉~ ^_^

另外,本章所有的範例都可以在 http://linux.vbird.org/linux_basic/0340bashshell-scripts/scripts.tgz 裡頭找到喔!加油~


大標題的圖示 本章習題練習
( 要看答案請將滑鼠移動到『答:』底下的空白處,按下左鍵圈選空白處即可察看 )
  • 請建立一支 script ,當你執行該 script 的時候,該 script 可以顯示: 1. 你目前的身份 (用 whoami ) 2. 你目前所在的目錄 (用 pwd)
    #!/bin/bash
    echo -e "Your name is ==> `whoami`"
    echo -e "The current directory is ==> `pwd`"
  • 請自行建立一支程式,該程式可以用來計算『您還有幾天可以過生日』啊??
    #!/bin/bash
    read -p "Pleas input your birthday (MMDD, ex> 0709): " bir
    now=`date +%m%d`
    if [ "$bir" == "$now" ]; then
    echo "Happy Birthday to you!!!"
    elif [ "$bir" -gt "$now" ]; then
    year=`date +%Y`
    total_d=$(($((`date --date="$year$bir" +%s`-`date +%s`))/60/60/24))
    echo "Your birthday will be $total_d later"
    else
    year=$((`date +%Y`+1))
    total_d=$(($((`date --date="$year$bir" +%s`-`date +%s`))/60/60/24))
    echo "Your birthday will be $total_d later"
    fi
  • 讓使用者輸入一個數字,程式可以由 1+2+3... 一直累加到使用者輸入的數字為止。
    #!/bin/bash
    read -p "Please input an integer number: " number
    i=0
    s=0
    while [ "$i" != "$number" ]
    do
    i=$(($i+1))
    s=$(($s+$i))
    done
    echo "the result of '1+2+3+...$number' is ==> $s"
  • 撰寫一支程式,他的作用是: 1.) 先查看一下 /root/test/logical 這個名稱是否存在; 2.) 若不存在,則建立一個檔案,使用 touch 來建立,建立完成後離開; 3.) 如果存在的話,判斷該名稱是否為檔案,若為檔案則將之刪除後建立一個目錄,檔名為 logical ,之後離開; 4.) 如果存在的話,而且該名稱為目錄,則移除此目錄!
    #!/bin/bash
    if [ ! -e logical ]; then
    touch logical
    echo "Just make a file logical"
    exit 1
    elif [ -e logical ] && [ -f logical ]; then
    rm logical
    mkdir logical
    echo "remove file ==> logical"
    echo "and make directory logical"
    exit 1
    elif [ -e logical ] && [ -d logical ]; then
    rm -rf logical
    echo "remove directory ==> logical"
    exit 1
    else
    echo "Does here have anything?"
    fi
  • 我們知道 /etc/passwd 裡面以 : 來分隔,第一欄為帳號名稱。請寫一隻程式,可以將 /etc/passwd 的第一欄取出,而且每一欄都以一行字串『The 1 account is "root" 』來顯示,那個 1 表示行數。
    #!/bin/bash
    accounts=`cat /etc/passwd | cut -d':' -f1`
    for account in $accounts
    do
    declare -i i=$i+1
    echo "The $i account is /"$account/" "
    done

你可能感兴趣的:(Linux)