1. 程式人生 > 其它 >《Linux就該這麼學》第四章_vim編輯器和Shell命令指令碼

《Linux就該這麼學》第四章_vim編輯器和Shell命令指令碼

《Linux就該這麼學》讀書隨筆集

第四章

4.1 vim文字編輯器

Linux系統中一切都是檔案,而配置一個服務就是在修改其配置檔案的引數。”而且在日常工作中大家也肯定免不了要編寫文件,這些工作都是通過文字編輯器來完成的。劉遄老師寫作本書的目的是讓讀者切實掌握Linux系統的運維方法,而不是僅僅停留在“會用某個作業系統”的層面上,所以我們這裡選擇使用Vim文字編輯器,它預設會安裝在當前所有的Linux作業系統上,是一款超棒的文字編輯器。

Vim之所以能得到廣大廠商與使用者的認可,原因在於Vim編輯器中設定了3種模式—命令模式、末行模式和編輯模式,每種模式分別又支援多種不同的命令快捷鍵,這大大提高了工作效率,而且使用者在習慣之後也會覺得相當順手。要想高效地操作文字,就必須先搞清這3種模式的操作區別以及模式之間的切換方法(見圖4-1)。

命令模式:控制游標移動,可對文字進行復制、貼上、刪除和查詢等工作。

輸入模式:正常的文字錄入。

末行模式:儲存或退出文件,以及設定編輯環境。

圖4-1 Vim編輯器模式的切換方法

在每次執行Vim編輯器時,預設進入命令模式,此時需要先切換到輸入模式後再進行文件編寫工作。而每次在編寫完文件後需要先返回命令模式,然後再進入末行模式,執行文件的儲存或退出操作。在Vim中,無法直接從輸入模式切換到末行模式。Vim編輯器中內建的命令有成百上千種用法,為了能夠幫助讀者更快地掌握Vim編輯器,表4-1總結了在命令模式中最常用的一些命令。

表4-1 命令模式中最常用的一些命令

命令 作用
dd 刪除(剪下)游標所在整行
5dd 刪除(剪下)從游標處開始的5行
yy 複製游標所在整行
5yy 複製從游標處開始的5行
n 顯示搜尋命令定位到的下一個字串
N 顯示搜尋命令定位到的上一個字串
u 撤銷上一步的操作
p 將之前刪除(dd)或複製(yy)過的資料貼上到游標後面

末行模式主要用於儲存或退出檔案,以及設定Vim編輯器的工作環境,還可以讓使用者執行外部的Linux命令或跳轉到所編寫文件的特定行數。要想切換到末行模式,在命令模式中輸入一個冒號就可以了。末行模式中常用的命令如表4-2所示。

表4-2 末行模式中最常用的一些命令

命令 作用
:w 儲存
:q 退出
:q! 強制退出(放棄對文件的修改內容)
:wq! 強制儲存退出
:set nu 顯示行號
:set nonu 不顯示行號
:命令 執行該命令
:整數 跳轉到該行
: s/one/two 將當前游標所在行的第一個one替換成two
: s/one/two/g 將當前游標所在行的所有one替換成two
:%s/one/two/g 將全文中的所有one替換成two
?字串 在文字中從下至上搜索該字串
/字串 在文字中從上至下搜尋該字串

4.1.1 編寫簡單文件

可以分別使用a、i、o三個鍵從命令模式切換到輸入模式。其中,a鍵與i鍵分別是在游標後面一位和游標當前位置切換到輸入模式,而o鍵則是在游標的下面再建立一個空行,此時可敲擊a鍵進入到編輯器的輸入模式。

4.1.2 配置主機名稱

為了便於在區域網中查詢某臺特定的主機,或者對主機進行區分,除了要有IP地址外,還要為主機配置一個主機名,主機之間可以通過這個類似於域名的名稱來相互訪問。在Linux系統中,主機名大多儲存在/etc/hostname檔案中,接下來將/etc/hostname配置檔案的內容修改為“linuxprobe.com”,步驟如下。

第1步:使用Vim編輯器修改/etc/hostname主機名稱檔案。

第2步:把原始主機名稱刪除後追加“linuxprobe.com”。注意,使用Vim編輯器修改主機名稱檔案後,要在末行模式下執行“:wq!”命令才能儲存並退出文件。

第3步:儲存並退出文件,然後使用hostname命令檢查是否修改成功。

[root@linuxprobe ~]# vim /etc/hostname
linuxprobe.com

hostname命令用於檢視當前的主機名稱,但有時主機名稱的改變不會立即同步到系統中,所以如果發現修改完成後還顯示原來的主機名稱,可重啟虛擬機器後再行檢視:

[root@linuxprobe ~]# hostname
linuxprobe.com

4.1.3 配置網絡卡資訊

如果您具備一定的運維經驗或者熟悉早期的Linux系統,則在學習本書時會遇到一些不容易接受的差異變化。在RHEL 5、RHEL 6中,網絡卡配置檔案的字首為eth,第1塊網絡卡為eth0,第2塊網絡卡為eth1;以此類推。在RHEL 7中,網絡卡配置檔案的字首則以ifcfg開始,再加上網絡卡名稱共同組成了網絡卡配置檔案的名字,例如ifcfg-eno16777736。而在RHEL 8中,網絡卡配置檔案的字首依然為ifcfg,區別是網絡卡名稱改成了類似於ens160的樣子,不過好在除了檔名發生變化外,網絡卡引數沒有其他大的區別。

現在有一個名稱為ifcfg-ens160的網絡卡裝置,將其配置為開機自啟動,並且IP地址、子網、閘道器等資訊由人工指定,其步驟如下所示。

第1步:首先切換到/etc/sysconfig/network-scripts目錄中(存放著網絡卡的配置檔案)。

第2步:使用Vim編輯器修改網絡卡檔案ifcfg-ens160,逐項寫入下面的配置引數並儲存退出。由於每臺裝置的硬體及架構是不一樣的,因此請讀者使用ifconfig命令自行確認各自網絡卡的預設名稱。

裝置型別:TYPE=Ethernet

地址分配模式:BOOTPROTO=static

網絡卡名稱:NAME=ens160

是否啟動:ONBOOT=yes

IP地址:IPADDR=192.168.10.10

子網掩碼:NETMASK=255.255.255.0

閘道器地址:GATEWAY=192.168.10.1

DNS地址:DNS1=192.168.10.1

第3步:重啟網路服務並測試網路是否連通。

下面正式開幹!

進入到網絡卡配置檔案所在的目錄,然後編輯網絡卡配置檔案,在其中填入下面的資訊:

[root@linuxprobe ~]# cd /etc/sysconfig/network-scripts/
[root@linuxprobe network-scripts]# vim ifcfg-ens160
TYPE=Ethernet
BOOTPROTO=static
NAME=ens160
ONBOOT=yes
IPADDR=192.168.10.10
NETMASK=255.255.255.0
GATEWAY=192.168.10.1
DNS1=192.168.10.1

執行重啟網絡卡裝置的命令,然後通過ping命令測試網路能否連通。由於在Linux系統中ping命令不會自動終止,因此需要手動按下Ctrl+C組合鍵來強行結束程序。

[root@linuxprobe network-scripts]# nmcli connection reload ens160
[root@linuxprobe network-scripts]# ping 192.168.10.10
PING 192.168.10.10 (192.168.10.10) 56(84) bytes of data.
64 bytes from 192.168.10.10: icmp_seq=1 ttl=64 time=0.083 ms
64 bytes from 192.168.10.10: icmp_seq=2 ttl=64 time=0.110 ms
64 bytes from 192.168.10.10: icmp_seq=3 ttl=64 time=0.106 ms
64 bytes from 192.168.10.10: icmp_seq=4 ttl=64 time=0.035 ms
^C
--- 192.168.10.10 ping statistics ---
4 packets transmitted, 4 received, 0% packet loss, time 84ms
rtt min/avg/max/mdev = 0.035/0.083/0.110/0.031 ms
[root@linuxprobe network-scripts]# 

4.1.4 配置軟體倉庫

既然要使用軟體倉庫,就要先把它搭建起來,然後將其配置規則確定好才行。鑑於第6章才會講解Linux的儲存結構和裝置掛載操作,所以當前還是將重心放到Vim編輯器的學習上。如果遇到看不懂的引數也不要緊,後面章節會單獨講解。

Yum與DNF軟體倉庫的配置檔案是通用的,也就是說填寫好配置檔案資訊後,這兩個軟體倉庫的命令都是可以正常使用。建議在RHEL 8中使用dnf作為軟體的安裝命令,因為它具備更高的效率,而且支援多執行緒同時安裝軟體。

搭建並配置軟體倉庫的大致步驟如下所示。

第1步:進入/etc/yum.repos.d/目錄中(因為該目錄存放著軟體倉庫的配置檔案)。

第2步:使用Vim編輯器建立一個名為rhel8.repo的新配置檔案(檔名稱可隨意,但字尾必須為.repo),逐項寫入下面的配置引數並儲存退出。

倉庫名稱:具有唯一性的標識名稱,不應與其他軟體倉庫發生衝突。

描述資訊(name):可以是一些介紹性的詞,易於識別軟體倉庫的用處。

倉庫位置(baseurl):軟體包的獲取方式,可以使用FTP或HTTP下載,也可以是本地的檔案(需要在後面新增file引數)。

是否啟用(enabled):設定此源是否可用;1為可用,0為禁用。

是否校驗(gpgcheck):設定此源是否校驗檔案;1為校驗,0為不校驗。

公鑰位置(gpgkey):若上面的引數開啟了校驗功能,則此處為公鑰檔案位置。若沒有開啟,則省略不寫。

第3步:按配置引數中所填寫的倉庫位置掛載光碟,並把光碟掛載資訊寫入/etc/fstab檔案中。

第4步:使用“dnf install httpd -y”命令檢查軟體倉庫是否已經可用。

開始實戰!

進入/etc/yum.repos.d目錄後建立軟體倉庫的配置檔案:

[root@linuxprobe ~]# cd /etc/yum.repos.d/
[root@linuxprobe yum.repos.d]# vim rhel8.repo
[BaseOS]
name=BaseOS
baseurl=file:///media/cdrom/BaseOS
enabled=1
gpgcheck=0
[AppStream]
name=AppStream
baseurl=file:///media/cdrom/AppStream
enabled=1
gpgcheck=0

建立掛載點後進行掛載操作,並設定成開機自動掛載(詳見第6章):

[root@linuxprobe yum.repos.d]# mkdir -p /media/cdrom 
[root@linuxprobe yum.repos.d]# mount /dev/cdrom /media/cdrom
mount: /media/cdrom: WARNING: device write-protected, mounted read-only.
[root@linuxprobe yum.repos.d]# vim /etc/fstab
/dev/cdrom /media/cdrom iso9660 defaults 0 0

嘗試使用軟體倉庫的dnf命令來安裝Web服務,軟體包名稱為httpd,安裝後出現“Complete!”則代表配置正確:

[root@linuxprobe ~]# dnf install httpd -y
Updating Subscription Management repositories.
Unable to read consumer identity
This system is not registered to Red Hat Subscription Management. You can use subscription-manager to register.
AppStream 3.1 MB/s | 3.2 kB 00:00
BaseOS 2.7 MB/s | 2.7 kB 00:00
Dependencies resolved.
………………省略部分輸出資訊………………
Installed:
httpd-2.4.37-10.module+el8+2764+7127e69e.x86_64
apr-util-bdb-1.6.1-6.el8.x86_64
apr-util-openssl-1.6.1-6.el8.x86_64
apr-1.6.3-9.el8.x86_64
apr-util-1.6.1-6.el8.x86_64
httpd-filesystem-2.4.37-10.module+el8+2764+7127e69e.noarch
httpd-tools-2.4.37-10.module+el8+2764+7127e69e.x86_64
mod_http2-1.11.3-1.module+el8+2443+605475b7.x86_64
redhat-logos-httpd-80.7-1.el8.noarch

Complete!

4.2 編寫Shell指令碼

可以將Shell終端直譯器當作人與計算機硬體之間的“翻譯官”,它作為使用者與Linux系統內部的通訊媒介,除了能夠支援各種變數與引數外,還提供了諸如迴圈、分支等高階程式語言才有的控制結構特性。要想正確使用Shell中的這些功能特性,準確下達命令尤為重要。Shell指令碼命令的工作方式有下面兩種。

互動式(Interactive):使用者每輸入一條命令就立即執行。

批處理(Batch):由使用者事先編寫好一個完整的Shell指令碼,Shell會一次性執行指令碼中諸多的命令。

在Shell指令碼中不僅會用到前面學習過的很多Linux命令以及正則表示式、管道符、資料流重定向等語法規則,還需要把內部功能模組化後通過邏輯語句進行處理,最終形成日常所見的Shell指令碼。

通過檢視SHELL變數可以發現,當前系統已經預設使用Bash作為命令列終端直譯器了:

[root@linuxprobe ~]# echo $SHELL
/bin/bash

4.2.1 編寫簡單的指令碼

估計讀者在看完上文中有關Shell指令碼的複雜描述後,會累覺不愛吧。但是,上文指的是一個高階Shell指令碼的編寫原則,其實使用Vim編輯器把Linux命令按照順序依次寫入到一個檔案中,就是一個簡單的指令碼了。

例如,如果想檢視當前所在工作路徑並列出當前目錄下所有的檔案及屬性資訊,實現這個功能的指令碼應該類似於下面這樣:

[root@linuxprobe ~]# vim example.sh
#!/bin/bash 
#For Example BY linuxprobe.com 
pwd 
ls -al

Shell指令碼檔案的名稱可以任意,但為了避免被誤以為是普通檔案,建議將.sh字尾加上,以表示是一個指令碼檔案。

在上面的這個example.sh指令碼中實際上出現了3種不同的元素:第一行的指令碼宣告(#!)用來告訴系統使用哪種Shell直譯器來執行該指令碼;第二行的註釋資訊(#)是對指令碼功能和某些命令的介紹資訊,使得自己或他人在日後看到這個指令碼內容時,可以快速知道該指令碼的作用或一些警告資訊;第三、四行的可執行語句也就是我們平時執行的Linux命令了。你們不相信這麼簡單就編寫出來了一個指令碼程式?!那我們來執行一下看看結果:

[root@linuxprobe ~]# bash example.sh
/root
total 60
dr-xr-x---. 15 root root  4096 Oct 12 00:41 .
dr-xr-xr-x. 17 root root   224 Jul 21 05:04 ..
-rw-------.  1 root root  1407 Jul 21 05:09 anaconda-ks.cfg
-rw-------.  1 root root   335 Jul 24 06:33 .bash_history
-rw-r--r--.  1 root root    18 Aug 13  2018 .bash_logout
-rw-r--r--.  1 root root   176 Aug 13  2018 .bash_profile
………………省略部分輸出資訊………………

除了上面用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
total 60
dr-xr-x---. 15 root root  4096 Oct 12 00:41 .
dr-xr-xr-x. 17 root root   224 Jul 21 05:04 ..
-rw-------.  1 root root  1407 Jul 21 05:09 anaconda-ks.cfg
-rw-------.  1 root root   335 Jul 24 06:33 .bash_history
-rw-r--r--.  1 root root    18 Aug 13  2018 .bash_logout
-rw-r--r--.  1 root root   176 Aug 13  2018 .bash_profile
………………省略部分輸出資訊………………

4.2.2 接受使用者的引數

但是,像上面這樣的指令碼程式只能執行一些預先定義好的功能,未免太過死板。為了讓Shell指令碼程式更好地滿足使用者的一些實時需求,以便靈活完成工作,必須要讓指令碼程式能夠像之前執行命令時那樣,接收使用者輸入的引數。

比如,當用戶執行某一個命令時,加或不加引數的輸出結果是不同的:

[root@linuxprobe ~]# wc -l anaconda-ks.cfg 
44 anaconda-ks.cfg
[root@linuxprobe ~]# wc -c anaconda-ks.cfg 
1407 anaconda-ks.cfg
[root@linuxprobe ~]# wc -w anaconda-ks.cfg 
121 anaconda-ks.cfg

這意味著命令不僅要能接收使用者輸入的內容,還要有能力進行判斷區別,根據不同的輸入呼叫不同的功能。

其實,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 ~]# bash example.sh one two three four five six
當前指令碼名稱為example.sh
總共有6個引數,分別是one two three four five six。
第1個引數為one,第5個為five。

4.2.3 判斷使用者的引數

本書在前面章節中講到,系統在執行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 ~]# [ ! $USER = root ] || echo "administrator"
administrator

歎號應該放到判斷語句的前面,代表對整個的測試語句進行取反值操作,而不應該寫成“$USER != root”,因為“!=”代表的是不等於符號(≠),儘管執行效果一樣,但缺少了邏輯關係,這一點還請多加註意。

Tips

&&是邏輯“與”,只有當前面的語句執行成功的時候才會執行後面的語句。
||是邏輯“或”,只有當前面的語句執行失敗的時候才會執行後面的語句。
!是邏輯“非”,代表對邏輯測試結果取反值;之前若為正確則變成錯誤,若為錯誤則變成正確。

整數比較運算子僅是對數字的操作,不能將數字與字串、檔案等內容一起操作,而且不能想當然地使用日常生活中的等號、大於號、小於號等來判斷。因為等號與賦值命令符衝突,大於號和小於號分別與輸出重定向命令符和輸入重定向命令符衝突。因此一定要使用規範的整數比較運算子來進行操作。可用的整數比較運算子如表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

字串比較語句用於判斷測試字串是否為空值,或兩個字串是否相同。它經常用來判斷某個變數是否未被定義(即內容為空值),理解起來也比較簡單。字串比較中常見的運算子如表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指令碼,但是這種指令碼並不適用於生產環境。原因是它不能根據真實的工作需求來調整具體的執行命令,也不能根據某些條件實現自動迴圈執行。通俗來講,就是不能根據實際情況做出調整。

通常指令碼都是從上到下一股腦兒地執行,效率是很高,但一旦某條命令執行失敗了,則後面的功能全都會受到影響。接下來我們通過if、for、while、case這4種流程控制語句來學習編寫難度更大、功能更強的Shell指令碼。

4.3.1 if條件測試語句

if條件測試語句可以讓指令碼根據實際情況自動執行相應的命令。從技術角度來講,if語句分為單分支結構、雙分支結構、多分支結構;其複雜度隨著靈活度一起逐級上升。

if條件語句的單分支結構由if、then、fi關鍵片語成,而且只在條件成立後才執行預設的命令,相當於口語的“如果……那麼……”。單分支的if語句屬於最簡單的一種條件判斷結構,語法格式如圖4-18所示。

圖4-18 單分支的if條件語句

下面使用單分支的if條件語句來判斷/media/cdrom目錄是否存在,若不存在就建立這個目錄,反之則結束條件判斷和整個Shell指令碼的執行。

[root@linuxprobe ~]# vim mkcdrom.sh
#!/bin/bash
DIR="/media/cdrom"
if [ ! -d $DIR ]
then    
        mkdir -p $DIR
fi 

由於第5章才講解使用者身份與許可權,因此這裡繼續用“bash指令碼名稱”的方式來執行指令碼。在正常情況下,順利執行完指令碼檔案後沒有任何輸出資訊,但是可以使用ls命令驗證/media/cdrom目錄是否已經成功建立:

[root@linuxprobe ~]# bash mkcdrom.sh
[root@linuxprobe ~]# ls -ld /media/cdrom
drwxr-xr-x. 2 root root 6 Oct 13 21:34 /media/cdrom

if條件語句的雙分支結構由if、then、else、fi關鍵片語成,它進行一次條件匹配判斷,如果與條件匹配,則去執行相應的預設命令;反之則去執行不匹配時的預設命令,相當於口語的“如果……那麼……或者……那麼……”。if條件語句的雙分支結構也是一種很簡單的判斷結構,語法格式如圖4-19所示。

圖4-19 雙分支的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-20所示。

下面使用多分支的if條件語句來判斷使用者輸入的分數在哪個成績區間內,然後輸出如Excellent、Pass、Fail等提示資訊。在Linux系統中,read是用來讀取使用者輸入資訊的命令,能夠把接收到的使用者輸入資訊賦值給後面的指定變數,-p引數用於向用戶顯示一些提示資訊。

圖 4-20 多分支的if條件語句

在下面的指令碼示例中,只有當用戶輸入的分數大於等於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

4.3.2 for條件迴圈語句

for迴圈語句允許指令碼一次性讀取多個資訊,然後逐一對資訊進行操作處理。當要處理的資料有範圍時,使用for迴圈語句就再適合不過了。for迴圈語句的語法格式如圖4-21所示。

圖4-21 for範圍迴圈語句

下面使用for迴圈語句從列表檔案中讀取多個使用者名稱,然後為其逐一建立使用者賬戶並設定密碼。首先建立使用者名稱稱的列表檔案users.txt,每個使用者名稱稱單獨一行。讀者可以自行決定具體的使用者名稱稱和個數:

[root@linuxprobe ~]# vim users.txt
andy
barry
carl
duke
eric
george

接下來編寫Shell指令碼addusers.sh。在指令碼中使用read命令讀取使用者輸入的密碼值,然後賦值給PASSWD變數,並通過-p引數向用戶顯示一段提示資訊,告訴使用者正在輸入的內容即將作為賬戶密碼。在執行該指令碼後,會自動使用從列表檔案users.txt中獲取到所有的使用者名稱稱,然後逐一使用“id使用者名稱”命令檢視使用者的資訊,並使用$?判斷這條命令是否執行成功,也就是判斷該使用者是否已經存在。

[root@linuxprobe ~]# vim addusers.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 "$UNAME , Already exists"
        else
                useradd $UNAME
                echo "$PASSWD" | passwd --stdin $UNAME &> /dev/null
                echo "$UNAME , Create success"
        fi
done

Tips

/dev/null是一個被稱作Linux黑洞的檔案,把輸出資訊重定向到這個檔案等同於刪除資料(類似於沒有回收功能的垃圾箱),可以讓使用者的螢幕視窗保持簡潔。

執行批量建立使用者的Shell指令碼addusers.sh,在輸入為賬戶設定的密碼後將由指令碼自動檢查並建立這些賬戶。由於已經將多餘的資訊通過輸出重定向符轉移到了/dev/null黑洞檔案中,因此在正常情況下螢幕視窗除了“使用者賬戶建立成功”(Create success)的提示後不會有其他內容。

在Linux系統中,/etc/passwd是用來儲存使用者賬戶資訊的檔案。如果想確認這個指令碼是否成功建立了使用者賬戶,可以開啟這個檔案,看其中是否有這些新建立的使用者資訊。

[root@linuxprobe ~]# bash addusers.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迴圈語句,不妨做些更酷的事情,比如嘗試讓指令碼從文字中自動讀取主機列表,然後自動逐個測試這些主機是否線上。

首先建立一個主機列表檔案ipaddrs.txt:

[root@linuxprobe ~]# vim ipaddrs.txt
192.168.10.10
192.168.10.11
192.168.10.12

然後將前面的雙分支if條件語句與for迴圈語句相結合,讓指令碼從主機列表檔案ipaddrs.txt中自動讀取IP地址(用來表示主機)並將其賦值給HLIST變數,從而通過判斷ping命令執行後的返回值來逐個測試主機是否線上。指令碼中出現的“$(命令)”是一種完全類似於第3章的轉義字元中反引號命令的Shell操作符,效果同樣是執行括號或雙引號括起來的字串中的命令。大家在編寫指令碼時,多學習幾種類似的新方法,可在工作中大顯身手:

[root@linuxprobe ~]# vim CheckHosts.sh
#!/bin/bash
HLIST=$(cat ~/ipaddrs.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.

4.3.3 while迴圈條件語句

while條件迴圈語句是一種讓指令碼根據某些條件來重複執行命令的語句,它的迴圈結構往往在執行前並不確定最終執行的次數,完全不同於for迴圈語句中有目標、有範圍的使用場景。while迴圈語句通過判斷條件測試的真假來決定是否繼續執行命令,若條件為真就繼續執行,為假就結束迴圈。while語句的語法格式如圖4-22所示。

圖4-22 while條件迴圈語句

接下來結合使用多分支的if條件測試語句與while條件迴圈語句,編寫一個用來猜測數值大小的指令碼Guess.sh。該指令碼使用$RANDOM變數來調取出一個隨機的數值(範圍為0~32767),然後將這個隨機數對1000進行取餘操作,並使用expr命令取得其結果,再用這個數值與使用者通過read命令輸入的數值進行比較判斷。這個判斷語句分為3種情況,分別是判斷使用者輸入的數值是等於、大於還是小於使用expr命令取得的數值。當前,現在這些內容不是重點,我們要關注的是while條件迴圈語句中的條件測試始終為true,因此判斷語句會無限執行下去,直到使用者輸入的數值等於expr命令取得的數值後,才執行exit 0命令,終止指令碼的執行。

[root@linuxprobe ~]# vim Guess.sh
#!/bin/bash
PRICE=$(expr $RANDOM % 1000)
TIMES=0
echo "商品實際價格為0-999之間,猜猜看是多少?"
while true
do
        read -p "請輸入您猜測的價格數目:" INT
        let TIMES++
        if [ $INT -eq $PRICE ] ; then
                echo "恭喜您答對了,實際價格是 $PRICE"
                echo "您總共猜測了 $TIMES 次"
                exit
        elif [ $INT -gt $PRICE ] ; then
                echo "太高了!"
        else
                echo "太低了!"
        fi
done

在這個Guess.sh指令碼中,我們添加了一些互動式的資訊,從而使得使用者與系統的互動性得以增強。而且每當迴圈到let TIMES++命令時都會讓TIMES變數內的數值加1,用來統計迴圈總計執行了多少次。這可以讓使用者得知在總共猜測了多少次之後,才猜對價格。

[root@linuxprobe ~]# bash Guess.sh
商品實際價格為0-999之間,猜猜看是多少?
請輸入您猜測的價格數目:500
太低了!
請輸入您猜測的價格數目:800
太高了!
請輸入您猜測的價格數目:650
太低了!
請輸入您猜測的價格數目:720
太高了!
請輸入您猜測的價格數目:690
太低了!
請輸入您猜測的價格數目:700
太高了!
請輸入您猜測的價格數目:695
太高了!
請輸入您猜測的價格數目:692
太高了!
請輸入您猜測的價格數目:691
恭喜您答對了,實際價格是 691
您總共猜測了 9 次

當條件為true(真)的時候,while語句會一直迴圈下去,只有碰到exit才會結束,所以同學們一定要記得加上exit哦。

4.3.4 case條件測試語句

如果您之前學習過C語言,看到這一小節的標題肯定會會心一笑:“這不就是switch語句嘛!”是的,case條件測試語句和switch語句的功能非常相似!case語句是在多個範圍內匹配資料,若匹配成功則執行相關命令並結束整個條件測試;如果資料不在所列出的範圍內,則會去執行星號(*)中所定義的預設命令。case語句的語法結構如圖4-23所示。

圖4-23 case條件測試語句

在前文介紹的Guess.sh指令碼中有一個致命的弱點—只能接受數字!您可以嘗試輸入一個字母,會發現指令碼立即就崩潰了。原因是字母無法與數字進行大小比較,例如,“a是否大於等於3”這樣的命題是完全錯誤的。必須有一定的措施來判斷使用者輸入的內容,當用戶輸入的內容不是數字時,指令碼能予以提示,從而免於崩潰。

通過在指令碼中組合使用case條件測試語句和萬用字元(詳見第3章),完全可以滿足這裡的需求。接下來我們編寫指令碼Checkkeys.sh,提示使用者輸入一個字元並將其賦值給變數KEY,然後根據變數KEY的值向用戶顯示其值是字母、數字還是其他字元。

[root@linuxprobe ~]# vim Checkkeys.sh
#!/bin/bash
read -p "請輸入一個字元,並按Enter鍵確認:" KEY
case "$KEY" in
        [a-z]|[A-Z])
                echo "您輸入的是 字母。"
                ;;
        [0-9])
                echo "您輸入的是 數字。"
                ;;
        *)
                echo "您輸入的是 空格、功能鍵或其他控制字元。"
esac
[root@linuxprobe ~]# bash Checkkeys.sh
請輸入一個字元,並按Enter鍵確認:6
您輸入的是 數字。
[root@linuxprobe ~]# bash Checkkeys.sh
請輸入一個字元,並按Enter鍵確認:p
您輸入的是 字母。
[root@linuxprobe ~]# bash Checkkeys.sh
請輸入一個字元,並按Enter鍵確認:^[[15~
您輸入的是 空格、功能鍵或其他控制字元。

4.4 計劃任務服務程式

計劃任務分為一次性計劃任務與長期性計劃任務,大家可以按照如下方式理解。

一次性計劃任務:今晚23:30重啟網站服務。

長期性計劃任務:每週一的凌晨3:25把/home/wwwroot目錄打包備份為backup.tar.gz。

顧名思義,一次性計劃任務只執行一次,一般用於臨時的工作需求。可以用at命令實現這種功能,只需要寫成“at時間”的形式就行。如果想要檢視已設定好但還未執行的一次性計劃任務,可以使用at -l命令;要想將其刪除,可以使用“atrm任務序號”。at命令中的引數及其作用如表4-6所示。

表4-6 at命令的引數及其作用

引數 作用
-f 指定包含命令的任務檔案
-q 指定新任務名稱
-l 顯示待執行任務列表
-d 刪除指定待執行任務
-m 任務執行後給使用者發郵件

在使用at命令來設定一次性計劃任務時,預設採用的是互動式方法。例如,使用下述命令將系統設定為在今晚23:30自動重啟網站服務。

[root@linuxprobe ~]# at 23:30
warning: commands will be executed using /bin/sh
at> systemctl restart httpd
at> 此處請同時按下<Ctrl>+<d>鍵來結束編寫計劃任務
job 1 at Wed Oct 14 23:30:00 2020
[root@linuxprobe ~]# at -l
1 Wed Oct 14 23:30:00 2020 a root

看到warning提醒資訊不要慌,at命令只是在告訴我們接下來的任務將由sh直譯器負責執行。這與此前學習的Bash直譯器基本一致,不需要有額外的操作。

另外,如果大家想挑戰一下難度更大但簡捷性更高的方式,可以把前面學習的管道符(任意門)放到兩條命令之間,讓at命令接收前面echo命令的輸出資訊,以達到通過非互動式的方式建立計劃一次性任務的目的。

[root@linuxprobe ~]# echo "systemctl restart httpd" | at 23:30
warning: commands will be executed using /bin/sh
job 2 at Wed Oct 14 23:30:00 2020
[root@linuxprobe ~]# at -l
1 Wed Oct 14 23:30:00 2020 a root
2 Wed Oct 14 23:30:00 2020 a root

上面設定了兩條一樣的計劃任務,可以使用atrm命令輕鬆刪除其中一條:

[root@linuxprobe ~]# atrm 2
[root@linuxprobe ~]# at -l
1 Wed Oct 14 23:30:00 2020 a root

這裡還有一種特殊場景—把計劃任務寫入Shell指令碼中,當用戶啟用該指令碼後再開始倒計時執行,而不是像上面那樣在固定的時間(“at 23:30”命令)進行。這該怎麼辦呢?

一般我們會使用“at now +2 MINUTE”的方式進行操作,這表示2分鐘(MINUTE)後執行這個任務,也可以將其替代成小時(HOUR)、日(DAY)、月(MONTH)等詞彙:

[root@linuxprobe ~]# at now +2 MINUTE
warning: commands will be executed using /bin/sh
at> systemctl restart httpd
at> 此處請同時按下<Ctrl>+<d>鍵來結束編寫計劃任務
job 3 at Wed Oct 14 22:50:00 2020

還有些時候,我們希望Linux系統能夠週期性地、有規律地執行某些具體的任務,那麼Linux系統中預設啟用的crond服務簡直再適合不過了。建立、編輯計劃任務的命令為crontab -e,檢視當前計劃任務的命令為crontab -l,刪除某條計劃任務的命令為crontab -r。另外,如果您是以管理員的身份登入的系統,還可以在crontab命令中加上-u引數來編輯他人的計劃任務。crontab命令中的引數及其作用如表4-7所示。

表4-7 crontab命令的引數及其作用

引數 作用
-e 編輯計劃任務
-u 指定使用者名稱稱
-l 列出任務列表
-r 刪除計劃任務

在正式部署計劃任務前,請先跟劉遄老師念一下口訣“分、時、日、月、星期 命令”。這是使用crond服務設定任務的引數格式(其格式見表4-8)。需要注意的是,如果有些欄位沒有被設定,則需要使用星號(*****)佔位,如圖4-24所示。

圖4-24 使用crond設定任務的引數格式

表4-8 使用crond設定任務的引數欄位說明

欄位 說明
分鐘 取值為0~59的整數
小時 取值為0~23的任意整數
日期 取值為1~31的任意整數
月份 取值為1~12的任意整數
星期 取值為0~7的任意整數,其中0與7均為星期日
命令 要執行的命令或程式指令碼

假設在每週一、三、五的凌晨3:25,都需要使用tar命令把某個網站的資料目錄進行打包處理,使其作為一個備份檔案。我們可以使用crontab -e命令來建立計劃任務,為自己建立計劃任務時無須使用-u引數。crontab –e命令的具體實現效果和crontab -l命令的結果如下所示:

[root@linuxprobe ~]# crontab -e
no crontab for root - using an empty one
crontab: installing new crontab
[root@linuxprobe ~]# crontab -l
25 3 * * 1,3,5 /usr/bin/tar -czvf backup.tar.gz /home/wwwroot

需要說明的是,除了用逗號(,)來分別表示多個時間段,例如“8,9,12”表示8月、9月和12月。還可以用減號(-)來表示一段連續的時間週期(例如欄位“日”的取值為“12-15”,則表示每月的12~15日)。還可以用除號(/)表示執行任務的間隔時間(例如“*/2”表示每隔2分鐘執行一次任務)。

如果在crond服務中需要同時包含多條計劃任務的命令語句,應每行僅寫一條。例如我們再新增一條計劃任務,它的功能是每週一至週五的凌晨1點自動清空/tmp目錄內的所有檔案。尤其需要注意的是,在crond服務的計劃任務引數中,所有命令一定要用絕對路徑的方式來寫,如果不知道絕對路徑,請用whereis命令進行查詢。rm命令的路徑為下面輸出資訊中的加粗部分。

[root@linuxprobe ~]# whereis rm
rm: /usr/bin/rm /usr/share/man/man1/rm.1.gz /usr/share/man/man1p/rm.1p.gz
[root@linuxprobe ~]# crontab -e
crontab: installing new crontab
[root@linuxprobe ~]# crontab -l
25 3 * * 1,3,5 /usr/bin/tar -czvf backup.tar.gz /home/wwwroot
0  1 * * 1-5   /usr/bin/rm -rf /tmp/*

總結一下使用計劃服務的注意事項。

在crond服務的配置引數中,一般會像Shell指令碼那樣以#號開頭寫上註釋資訊,這樣在日後回顧這段命令程式碼時可以快速瞭解其功能、需求以及編寫人員等重要資訊。

計劃任務中的“分”欄位必須有數值,絕對不能為空或是*號,而“日”和“星期”欄位不能同時使用,否則就會發生衝突。

刪除crond計劃任務則非常簡單,直接使用crontab -e命令進入編輯介面,刪除裡面的文字資訊即可。也可以使用crontab -r命令直接進行刪除:

[root@linuxprobe ~]# crontab -r
[root@linuxprobe ~]# crontab -l
no crontab for root