4.2 編寫Shell腳本
可以將Shell終端解釋器當作人與計算機硬件之間的“翻譯官”,它作為用戶與Linux系統內部的通信媒介,除了能夠支持各種變量與參數外,還提供了諸如循環、分支等高級編程語言才有的控制結構特性。要想正確使用Shell中的這些功能特性,准確下達命令尤為重要。Shell腳本命令的工作方式有兩種:交互式和批處理。
交互式(Interactive):用戶每輸入一條命令就立即執行。
批處理(Batch):由用戶事先編寫好一個完整的Shell腳本,Shell會一次性執行腳本中諸多的命令。
在Shell腳本中不僅會用到前面學習過的很多Linux命令以及正則表達式、管道符、數據流重定向等語法規則,還需要把內部功能模塊化后通過邏輯語句進行處理,最終形成日常所見的Shell腳本。
查看SHELL變量可以發現當前系統已經默認使用Bash作為命令行終端解釋器了:
[root@linuxprobe ~]# echo $SHELL
/bin/bash
1 、編寫簡單的腳本
估計讀者在看完上文中有關Shell腳本的復雜描述后,會累覺不愛吧。但是,上文指的是一個高級Shell腳本的編寫原則,其實使用Vim編輯器把Linux命令按照順序依次寫入到一個文件中,這就是一個簡單的腳本了。
例如,如果想查看當前所在工作路徑並列出當前目錄下所有的文件及屬性信息,實現這個功能的腳本應該類似於下面這樣:
[root@linuxprobe ~]# vim example.sh
#!/bin/bash
#For Example BY linuxprobe.com
pwd
ls -al
Shell腳本文件的名稱可以任意,但為了避免被誤以為是普通文件,建議將.sh后綴加上,以表示是一個腳本文件。在上面的這個example.sh腳本中實際上出現了三種不同的元素:第一行的腳本聲明(#!)用來告訴系統使用哪種Shell解釋器來執行該腳本;第二行的注釋信息(#)是對腳本功能和某些命令的介紹信息,使得自己或他人在日后看到這個腳本內容時,可以快速知道該腳本的作用或一些警告信息;第三、四行的可執行語句也就是我們平時執行的Linux命令了。什么?!你們不相信這么簡單就編寫出來了一個腳本程序,那我們來執行一下看看結果:
[root@linuxprobe ~]# bash example.sh
/root/Desktop
total 8
drwxr-xr-x. 2 root root 23 Jul 23 17:31 .
dr-xr-x---. 14 root root 4096 Jul 23 17:31 ..
-rwxr--r--. 1 root root 55 Jul 23 17:31 example.sh
除了上面用bash解釋器命令直接運行Shell腳本文件外,第二種運行腳本程序的方法是通過輸入完整路徑的方式來執行。但默認會因為權限不足而提示報錯信息,此時只需要為腳本文件增加執行權限即可(詳見第5章)。初次學習Linux系統的讀者不用心急,等下一章學完用戶身份和權限后再來做這個實驗也不遲:
[root@linuxprobe ~]# ./example.sh
bash: ./Example.sh: Permission denied
[root@linuxprobe ~]# chmod u+x example.sh
[root@linuxprobe ~]# ./example.sh
/root/Desktop
total 8
drwxr-xr-x. 2 root root 23 Jul 23 17:31 .
dr-xr-x---. 14 root root 4096 Jul 23 17:31 ..
-rwxr--r--. 1 root root 55 Jul 23 17:31 example.sh
2、 接收用戶的參數
但是,像上面這樣的腳本程序只能執行一些預先定義好的功能,未免太過死板了。為了讓Shell腳本程序更好地滿足用戶的一些實時需求,以便靈活完成工作,必須要讓腳本程序能夠像之前執行命令時那樣,接收用戶輸入的參數。
其實,Linux系統中的Shell腳本語言早就考慮到了這些,已經內設了用於接收參數的變量,變量之間可以使用空格間隔。例如$0對應的是當前Shell腳本程序的名稱,$#對應的是總共有幾個參數,$*對應的是所有位置的參數值,$?對應的是顯示上一次命令的執行返回值,而$1、$2、$3……則分別對應着第N個位置的參數值,如圖4-15所示。

圖4-15 Shell腳本程序中的參數位置變量
理論過后我們來練習一下。嘗試編寫一個腳本程序示例,通過引用上面的變量參數來看下真實效果:
[root@linuxprobe ~]# vim example.sh
#!/bin/bash
echo "當前腳本名稱為$0"
echo "總共有$#個參數,分別是$*。"
echo "第1個參數為$1,第5個為$5。"
[root@linuxprobe ~]# sh example.sh one two three four five six
當前腳本名稱為example.sh
總共有6個參數,分別是one two three four five six。
第1個參數為one,第5個為five。
3 、判斷用戶的參數
學習是一個登堂入室、由淺入深的過程。在學習完Linux命令、掌握Shell腳本語法變量和接收用戶輸入的信息之后,就要踏上新的高度—能夠進一步處理接收到的用戶參數。
在本書前面章節中講到,系統在執行mkdir命令時會判斷用戶輸入的信息,即判斷用戶指定的文件夾名稱是否已經存在,如果存在則提示報錯;反之則自動創建。Shell腳本中的條件測試語法可以判斷表達式是否成立,若條件成立則返回數字0,否則便返回其他隨機數值。條件測試語法的執行格式如圖4-16所示。切記,條件表達式兩邊均應有一個空格。

圖4-16 條件測試語句的執行格式
按照測試對象來划分,條件測試語句可以分為4種:
文件測試語句;
邏輯測試語句;
整數值比較語句;
字符串比較語句。
文件測試即使用指定條件來判斷文件是否存在或權限是否滿足等情況的運算符,具體的參數如表4-3所示。
表4-3 文件測試所用的參數
| 操作符 | 作用 |
| -d | 測試文件是否為目錄類型 |
| -e | 測試文件是否存在 |
| -f | 判斷是否為一般文件 |
| -r | 測試當前用戶是否有權限讀取 |
| -w | 測試當前用戶是否有權限寫入 |
| -x | 測試當前用戶是否有權限執行 |
下面使用文件測試語句來判斷/etc/fstab是否為一個目錄類型的文件,然后通過Shell解釋器的內設$?變量顯示上一條命令執行后的返回值。如果返回值為0,則目錄存在;如果返回值為非零的值,則意味着目錄不存在:
[root@linuxprobe ~]# [ -d /etc/fstab ]
[root@linuxprobe ~]# echo $?
1
再使用文件測試語句來判斷/etc/fstab是否為一般文件,如果返回值為0,則代表文件存在,且為一般文件:
[root@linuxprobe ~]# [ -f /etc/fstab ]
[root@linuxprobe ~]# echo $?
0
邏輯語句用於對測試結果進行邏輯分析,根據測試結果可實現不同的效果。例如在Shell終端中邏輯“與”的運算符號是&&,它表示當前面的命令執行成功后才會執行它后面的命令,因此可以用來判斷/dev/cdrom文件是否存在,若存在則輸出Exist字樣。
[root@linuxprobe ~]# [ -e /dev/cdrom ] && echo "Exist"
Exist
除了邏輯“與”外,還有邏輯“或”,它在Linux系統中的運算符號為||,表示當前面的命令執行失敗后才會執行它后面的命令,因此可以用來結合系統環境變量USER來判斷當前登錄的用戶是否為非管理員身份:
[root@linuxprobe ~]# echo $USER
root
[root@linuxprobe ~]# [ $USER = root ] || echo "user"
[root@linuxprobe ~]# su - linuxprobe
[linuxprobe@linuxprobe ~]$ [ $USER = root ] || echo "user"
user
第三種邏輯語句是“非”,在Linux系統中的運算符號是一個嘆號(!),它表示把條件測試中的判斷結果取相反值。也就是說,如果原本測試的結果是正確的,則將其變成錯誤的;原本測試錯誤的結果則將其變成正確的。
我們現在切換回到root管理員身份,再判斷當前用戶是否為一個非管理員的用戶。由於判斷結果因為兩次否定而變成正確,因此會正常地輸出預設信息:
[linuxprobe@linuxprobe ~]$ exit
logout
[root@linuxprobe root]# [ $USER != root ] || echo "administrator"
administrator
就技術圖書的寫作來講,一般有兩種套路:讓讀者真正搞懂技術了;讓讀者覺得自己搞懂技術了。因此市面上很多淺顯的圖書會讓讀者在學完之后感覺進步特別快,這基本上是作者有意為之,目的就是讓您覺得“圖書很有料,自己收獲很大”,但是在步入工作崗位后就露出短板吃大虧。所以劉遄老師決定繼續提高難度,為讀者增加一個綜合的示例,一方面作為前述知識的總結,另一方面幫助讀者夯實基礎,能夠在今后工作中更靈活地使用邏輯符號。
當前我們正在登錄的即為管理員用戶—root。下面這個示例的執行順序是,先判斷當前登錄用戶的USER變量名稱是否等於root,然后用邏輯運算符“非”進行取反操作,效果就變成了判斷當前登錄的用戶是否為非管理員用戶了。最后若條件成立則會根據邏輯“與”運算符輸出user字樣;或條件不滿足則會通過邏輯“或”運算符輸出root字樣,而如果前面的&&不成立才會執行后面的||符號。
[root@linuxprobe ~]# [ $USER != root ] && echo "user" || echo "root"
root
整數比較運算符僅是對數字的操作,不能將數字與字符串、文件等內容一起操作,而且不能想當然地使用日常生活中的等號、大於號、小於號等來判斷。因為等號與賦值命令符沖突,大於號和小於號分別與輸出重定向命令符和輸入重定向命令符沖突。因此一定要使用規范的整數比較運算符來進行操作。可用的整數比較運算符如表4-4所示。
表4-4 可用的整數比較運算符
| 操作符 | 作用 |
| -eq | 是否等於 |
| -ne | 是否不等於 |
| -gt | 是否大於 |
| -lt | 是否小於 |
| -le | 是否等於或小於 |
| -ge | 是否大於或等於 |
接下來小試牛刀。我們先測試一下10是否大於10以及10是否等於10(通過輸出的返回值內容來判斷):
[root@linuxprobe ~]# [ 10 -gt 10 ]
[root@linuxprobe ~]# echo $?
1
[root@linuxprobe ~]# [ 10 -eq 10 ]
[root@linuxprobe ~]# echo $?
0
在2.4節曾經講過free命令,它可以用來獲取當前系統正在使用及可用的內存量信息。接下來先使用free -m命令查看內存使用量情況(單位為MB),然后通過grep Mem:命令過濾出剩余內存量的行,再用awk '{print $4}'命令只保留第四列,最后用FreeMem=`語句`的方式把語句內執行的結果賦值給變量。
這個演示確實有些難度,但看懂后會覺得很有意思,沒准在運維工作中也會用得上。
[root@linuxprobe ~]# free -m
total used free shared buffers cached
Mem: 1826 1244 582 9 1 413
-/+ buffers/cache: 830 996
Swap: 2047 0 2047
[root@linuxprobe ~]# free -m | grep Mem:
Mem: 1826 1244 582 9
[root@linuxprobe ~]# free -m | grep Mem: | awk '{print $4}'
582
[root@linuxprobe ~]# FreeMem=`free -m | grep Mem: | awk '{print $4}'`
[root@linuxprobe ~]# echo $FreeMem
582
上面用於獲取內存可用量的命令以及步驟可能有些“超綱”了,如果不能理解領會也不用擔心,接下來才是重點。我們使用整數運算符來判斷內存可用量的值是否小於1024,若小於則會提示“Insufficient Memory”(內存不足)的字樣:
[root@linuxprobe ~]# [ $FreeMem -lt 1024 ] && echo "Insufficient Memory"
Insufficient Memory
字符串比較語句用於判斷測試字符串是否為空值,或兩個字符串是否相同。它經常用來判斷某個變量是否未被定義(即內容為空值),理解起來也比較簡單。字符串比較中常見的運算符如表4-5所示。
表4-5 常見的字符串比較運算符
| 操作符 | 作用 |
| = | 比較字符串內容是否相同 |
| != | 比較字符串內容是否不同 |
| -z | 判斷字符串內容是否為空 |
接下來通過判斷String變量是否為空值,進而判斷是否定義了這個變量:
[root@linuxprobe ~]# [ -z $String ]
[root@linuxprobe ~]# echo $?
0
再嘗試引入邏輯運算符來試一下。當用於保存當前語系的環境變量值LANG不是英語(en.US)時,則會滿足邏輯測試條件並輸出“Not en.US”(非英語)的字樣:
[root@linuxprobe ~]# echo $LANG
en_US.UTF-8
[root@linuxprobe ~]# [ $LANG != "en.US" ] && echo "Not en.US"
Not en.US
4.3 流程控制語句
盡管此時可以通過使用Linux命令、管道符、重定向以及條件測試語句來編寫最基本的Shell腳本,但是這種腳本並不適用於生產環境。原因是它不能根據真實的工作需求來調整具體的執行命令,也不能根據某些條件實現自動循環執行。例如,我們需要批量創建1000位用戶,首先要判斷這些用戶是否已經存在;若不存在,則通過循環語句讓腳本自動且依次創建他們。
接下來我們通過if、for、while、case這4種流程控制語句來學習編寫難度更大、功能更強的Shell腳本。為了保證下文的實用性和趣味性,做到寓教於樂,我會盡可能多地講解各種不同功能的Shell腳本示例,而不是逮住一個腳本不放,在它原有內容的基礎上修修補補。盡管這種修補式的示例教學也可以讓讀者明白理論知識,但是卻無法開放思路,不利於日后的工作。
4.3.1 if條件測試語句
if條件測試語句可以讓腳本根據實際情況自動執行相應的命令。從技術角度來講,if語句分為單分支結構、雙分支結構、多分支結構;其復雜度隨着靈活度一起逐級上升。
if條件語句的單分支結構由if、then、fi關鍵詞組成,而且只在條件成立后才執行預設的命令,相當於口語的“如果……那么……”。單分支的if語句屬於最簡單的一種條件判斷結構,語法格式如圖4-17所示。
圖4-17 單分支的if語句
下面使用單分支的if條件語句來判斷/media/cdrom文件是否存在,若存在就結束條件判斷和整個Shell腳本,反之則去創建這個目錄:
[root@linuxprobe ~]# vim mkcdrom.sh
#!/bin/bash
DIR="/media/cdrom"
if [ ! -e $DIR ]
then
mkdir -p $DIR
fi
由於第5章才講解用戶身份與權限,因此這里繼續用“bash 腳本名稱”的方式來執行腳本。在正常情況下,順利執行完腳本文件后沒有任何輸出信息,但是可以使用ls命令驗證/media/cdrom目錄是否已經成功創建:
[root@linuxprobe ~]# bash mkcdrom.sh
[root@linuxprobe ~]# ls -d /media/cdrom
/media/cdrom
if條件語句的雙分支結構由if、then、else、fi關鍵詞組成,它進行一次條件匹配判斷,如果與條件匹配,則去執行相應的預設命令;反之則去執行不匹配時的預設命令,相當於口語的“如果……那么……或者……那么……”。if條件語句的雙分支結構也是一種很簡單的判斷結構,語法格式如圖4-18所示。

圖4-18 雙分支的if條件語句
下面使用雙分支的if條件語句來驗證某台主機是否在線,然后根據返回值的結果,要么顯示主機在線信息,要么顯示主機不在線信息。這里的腳本主要使用ping命令來測試與對方主機的網絡聯通性,而Linux系統中的ping命令不像Windows一樣嘗試4次就結束,因此為了避免用戶等待時間過長,需要通過-c參數來規定嘗試的次數,並使用-i參數定義每個數據包的發送間隔,以及使用-W參數定義等待超時時間。
[root@linuxprobe ~]# vim chkhost.sh
#!/bin/bash
ping -c 3 -i 0.2 -W 3 $1 &> /dev/null
if [ $? -eq 0 ]
then
echo "Host $1 is On-line."
else
echo "Host $1 is Off-line."
fi
我們在4.2.3小節中用過$?變量,作用是顯示上一次命令的執行返回值。若前面的那條語句成功執行,則$?變量會顯示數字0,反之則顯示一個非零的數字(可能為1,也可能為2,取決於系統版本)。因此可以使用整數比較運算符來判斷$?變量是否為0,從而獲知那條語句的最終判斷情況。這里的服務器IP地址為192.168.10.10,我們來驗證一下腳本的效果:
[root@linuxprobe ~]# bash chkhost.sh 192.168.10.10
Host 192.168.10.10 is On-line.
[root@linuxprobe ~]# bash chkhost.sh 192.168.10.20
Host 192.168.10.20 is Off-line.
if條件語句的多分支結構由if、then、else、elif、fi關鍵詞組成,它進行多次條件匹配判斷,這多次判斷中的任何一項在匹配成功后都會執行相應的預設命令,相當於口語的“如果……那么……如果……那么……”。if條件語句的多分支結構是工作中最常使用的一種條件判斷結構,盡管相對復雜但是更加靈活,語法格式如圖4-19所示。
圖 4-19 多分支的if條件語句
下面使用多分支的if條件語句來判斷用戶輸入的分數在哪個成績區間內,然后輸出如Excellent、Pass、Fail等提示信息。在Linux系統中,read是用來讀取用戶輸入信息的命令,能夠把接收到的用戶輸入信息賦值給后面的指定變量,-p參數用於向用戶顯示一定的提示信息。在下面的腳本示例中,只有當用戶輸入的分數大於等於85分且小於等於100分,才輸出Excellent字樣;若分數不滿足該條件(即匹配不成功),則繼續判斷分數是否大於等於70分且小於等於84分,如果是,則輸出Pass字樣;若兩次都落空(即兩次的匹配操作都失敗了),則輸出Fail字樣:
[root@linuxprobe ~]# vim chkscore.sh
#!/bin/bash
read -p "Enter your score(0-100):" GRADE
if [ $GRADE -ge 85 ] && [ $GRADE -le 100 ] ; then
echo "$GRADE is Excellent"
elif [ $GRADE -ge 70 ] && [ $GRADE -le 84 ] ; then
echo "$GRADE is Pass"
else
echo "$GRADE is Fail"
fi
[root@linuxprobe ~]# bash chkscore.sh
Enter your score(0-100):88
88 is Excellent
[root@linuxprobe ~]# bash chkscore.sh
Enter your score(0-100):80
80 is Pass
下面執行該腳本。當用戶輸入的分數分別為30和200時,其結果如下:
[root@linuxprobe ~]# bash chkscore.sh
Enter your score(0-100):30
30 is Fail
[root@linuxprobe ~]# bash chkscore.sh
Enter your score(0-100):200
200 is Fail
為什么輸入的分數為200時,依然顯示Fail呢?原因很簡單—沒有成功匹配腳本中的兩個條件判斷語句,因此自動執行了最終的兜底策略。可見,這個腳本還不是很完美,建議讀者自行完善這個腳本,使得用戶在輸入大於100或小於0的分數時,給予Error報錯字樣的提示。
4.3.2 for條件循環語句
for循環語句允許腳本一次性讀取多個信息,然后逐一對信息進行操作處理,當要處理的數據有范圍時,使用for循環語句再適合不過了。for循環語句的語法格式如圖4-20所示。

圖4-20 for循環語句的語法格式
下面使用for循環語句從列表文件中讀取多個用戶名,然后為其逐一創建用戶賬戶並設置密碼。首先創建用戶名稱的列表文件users.txt,每個用戶名稱單獨一行。讀者可以自行決定具體的用戶名稱和個數:
[root@linuxprobe ~]# vim users.txt
andy
barry
carl
duke
eric
george
接下來編寫Shell腳本Example.sh。在腳本中使用read命令讀取用戶輸入的密碼值,然后賦值給PASSWD變量,並通過-p參數向用戶顯示一段提示信息,告訴用戶正在輸入的內容即將作為賬戶密碼。在執行該腳本后,會自動使用從列表文件users.txt中獲取到所有的用戶名稱,然后逐一使用“id 用戶名”命令查看用戶的信息,並使用$?判斷這條命令是否執行成功,也就是判斷該用戶是否已經存在。
需要多說一句,/dev/null是一個被稱作Linux黑洞的文件,把輸出信息重定向到這個文件等同於刪除數據(類似於沒有回收功能的垃圾箱),可以讓用戶的屏幕窗口保持簡潔。
[root@linuxprobe ~]# vim Example.sh
#!/bin/bash
read -p "Enter The Users Password : " PASSWD
for UNAME in `cat users.txt`
do
id $UNAME &> /dev/null
if [ $? -eq 0 ]
then
echo "Already exists"
else
useradd $UNAME &> /dev/null
echo "$PASSWD" | passwd --stdin $UNAME &> /dev/null
if [ $? -eq 0 ]
then
echo "$UNAME , Create success"
else
echo "$UNAME , Create failure"
fi
fi
done
執行批量創建用戶的Shell腳本Example.sh,在輸入為賬戶設定的密碼后將由腳本自動檢查並創建這些賬戶。由於已經將多余的信息通過輸出重定向符轉移到了/dev/null黑洞文件中,因此在正常情況下屏幕窗口除了“用戶賬戶創建成功”(Create success)的提示后不會有其他內容。
在Linux系統中,/etc/passwd是用來保存用戶賬戶信息的文件。如果想確認這個腳本是否成功創建了用戶賬戶,可以打開這個文件,看其中是否有這些新創建的用戶信息。
[root@linuxprobe ~]# bash Example.sh
Enter The Users Password : linuxprobe
andy , Create success
barry , Create success
carl , Create success
duke , Create success
eric , Create success
george , Create success
[root@linuxprobe ~]# tail -6 /etc/passwd
andy:x:1001:1001::/home/andy:/bin/bash
barry:x:1002:1002::/home/barry:/bin/bash
carl:x:1003:1003::/home/carl:/bin/bash
duke:x:1004:1004::/home/duke:/bin/bash
eric:x:1005:1005::/home/eric:/bin/bash
george:x:1006:1006::/home/george:/bin/bash
您還記得在學習雙分支if條件語句時,用到的那個測試主機是否在線的腳本么?既然我們現在已經掌握了for循環語句,不妨做些更酷的事情,比如嘗試讓腳本從文本中自動讀取主機列表,然后自動逐個測試這些主機是否在線。
首先創建一個主機列表文件ipadds.txt:
[root@linuxprobe ~]# vim ipadds.txt
192.168.10.10
192.168.10.11
192.168.10.12
然后前面的雙分支if條件語句與for循環語句相結合,讓腳本從主機列表文件ipadds.txt中自動讀取IP地址(用來表示主機)並將其賦值給HLIST變量,從而通過判斷ping命令執行后的返回值來逐個測試主機是否在線。腳本中出現的$(命令)是一種完全類似於第3章的轉義字符中反引號`命令`的Shell操作符,效果同樣是執行括號或雙引號括起來的字符串中的命令。大家在編寫腳本時,多學習幾種類似的新方法,可在工作中大顯身手:
[root@linuxprobe ~]# vim CheckHosts.sh
#!/bin/bash
HLIST=$(cat ~/ipadds.txt)
for IP in $HLIST
do
ping -c 3 -i 0.2 -W 3 $IP &> /dev/null
if [ $? -eq 0 ] ; then
echo "Host $IP is On-line."
else
echo "Host $IP is Off-line."
fi
done
[root@linuxprobe ~]# ./CheckHosts.sh
Host 192.168.10.10 is On-line.
Host 192.168.10.11 is Off-line.
Host 192.168.10.12 is Off-line.










