Linux 基礎學習訓練教材 - CentOS 8.x

第 9 堂課:正規表示法與 shell script 初探

正規表示法用在非常多地方!熟悉正規表示法好處多多!因為其他程式語言也會用到這玩意兒喔!

最近更新時間: 2020/05/03

談完了指令列的 bash shell 的操作,再來得要思考一下,如果管理員有一堆指令要循序進行,且這些指令可能具有相依性 (例如判斷式), 或者是管理員需要寫一些讓使用者『互動』的指令碼時,應該如何處理?這時就得要透過 bash 的 shell script (程式化腳本) 來進行了。 此外,很多時候我們需要進行資料的擷取,這時好用的正規表示法就得要派上用場了!

9.1:正規表示法的應用

使用者在操作電子郵件時,常常會發現到許多郵件被丟到垃圾桶或者是被判定為病毒郵件,這些判定的方式,很多就是透過『正規表示法』來處理的! 正規表示法 (Regular Expression) 就是處理字串的方法,他是以行為單位來進行字串的處理行為,正規表示法透過一些特殊符號的輔助, 可以讓使用者輕易的達到『搜尋/刪除/取代』某特定字串的處理程序!

9.1.1:grep 指令的應用

由於正規表示法牽涉到資料的擷取,因此讀者們先了解一下最簡單的資料擷取指令: grep 的進階用法。例如,找出 /etc/passwd 當中, 含有 student 的那行,且列出行號:

[student@localhost ~]$ grep -n student /etc/passwd
44:student:x:1000:1000:student:/home/student:/bin/bash

讀者們會看到輸出的資訊中,最前面會多出一個行號的資訊,就可以讓使用者知道該資訊來自檔案的那一行這樣。 另外,當使用者有觀察開機流程所產生的資訊時,例如想要查詢開機過程產生的問題等等,可以使用 dmesg 這個指令。 只是這個指令輸出的資訊量非常龐大。若用戶僅須知道某張網路卡的相關資訊,那該如何處理呢? 先讓我們來查詢一下網路卡的代號:

# 單純使用 dmesg 的情況下,資料量相當龐大
[student@localhost ~]$ dmesg
......
[    9.775271] virbr0: port 1(virbr0-nic) entered listening state
[    9.839811] virbr0: port 1(virbr0-nic) entered disabled state
[   10.359755] f 4026531860#85: failed to wait on release 20 after...
[   11.638731] snd_hda_intel 0000:00:06.0: Invalid position buffer...

# 使用底下的方法可以找到網路卡的代號
[student@localhost ~]$ ip link show
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN mode DEFAULT group default qlen 1000
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
2: ens3: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc fq_codel state UP mode DEFAULT group default qlen 1000
    link/ether 52:54:00:85:b1:54 brd ff:ff:ff:ff:ff:ff
3: virbr0: <NO-CARRIER,BROADCAST,MULTICAST,UP> mtu 1500 qdisc noqueue state DOWN mode DEFAULT group default qlen 1000
    link/ether 52:54:00:b0:2f:5d brd ff:ff:ff:ff:ff:ff
4: virbr0-nic: <BROADCAST,MULTICAST> mtu 1500 qdisc fq_codel master virbr0 state DOWN mode DEFAULT group default qlen 1000
    link/ether 52:54:00:b0:2f:5d brd ff:ff:ff:ff:ff:ff

上面輸出的 ens3 就是網卡代號,而 lo 與 virbr0 等則是內部迴圈測試網卡與虛擬機器的橋接器,那個可以暫時略過不理。 現在,讓我們從 dmesg 的開機訊息中,找出網路卡相關的訊息吧:

[student@localhost ~]$ dmesg | grep -n -i ens3
552:[    1.853119] virtio_net virtio0 ens3: renamed from eth0
616:[    7.911858] IPv6: ADDRCONF(NETDEV_UP): ens3: link is not ready

大概在 616 行是比較重要的項目。那如果我們還需要知道該行之前 (before) 的 4 行以及之後 (after) 的 3 行, 以了解這行前後文的話,則可以這樣處理:

[student@localhost ~]$ dmesg | grep -n -A 3 -B 4 -i ens3
612-[    6.424222] RPC: Registered tcp transport module.
613-[    6.424223] RPC: Registered tcp NFSv4.1 backchannel transport module.
614-[    7.577042] f 4026531841#1: failed to wait on release 1 after spincount 301
615-[    7.905917] bridge: filtering via arp/ip/ip6tables is no longer available by default. Update your scripts to load br_netfilter if you need this.
616:[    7.911858] IPv6: ADDRCONF(NETDEV_UP): ens3: link is not ready  <==以這行為基準
617-[    7.926902] tun: Universal TUN/TAP device driver, 1.6
618-[    7.930979] virbr0: port 1(virbr0-nic) entered blocking state
619-[    7.930981] virbr0: port 1(virbr0-nic) entered disabled state

例題 9.1.1-1:使用反向的方式,取得非關鍵字的那幾行
  1. 使用 man grep ,找到反向 (invert) 這個關鍵字,找出其意義與選項參數功能
  2. 輸入 df 後,將 tmpfs 相關的那幾行取消,讓螢幕僅輸出一般的檔案系統,方便查閱。
  3. 針對上面的功能,寫一隻名為 df2 的命令別名,未來可以直接使用 df2 顯示出這個效果

9.1.2:正規表示法的符號意義

正規表示法既然是透過一些字符來作為資料擷取的判斷,那麼有哪些慣用的符號呢?大概有底下這些基本的符號:

RE 字符意義與範例
^word意義:待搜尋的字串(word)在行首!
範例:搜尋行首為『 # 』開始的那一行,並列出行號
grep -n '^#' regular_express.txt
word$意義:待搜尋的字串(word)在行尾!
範例:將行尾為『 ! 』的那一行列印出來,並列出行號
grep -n '!$' regular_express.txt
.意義:代表『一定有一個任意字元』的字符!
範例:搜尋的字串可以是 (eve) (eae) (eee) (e e), 但不能僅有 (ee) !亦即 e 與 e 中間『一定』僅有一個字元,而空白字元也是字元!
grep -n 'e.e' regular_express.txt
\意義:跳脫字符,將特殊符號的特殊意義去除!
範例:搜尋含有單引號『 ' 』的那一行!
grep -n \' regular_express.txt
*意義:重複零個到無窮多個的前一個 RE 字符
範例:找出含有 (es) (ess) (esss) 等等的字串,注意,因為 * 可以是 0 個,所以 es 也是符合帶搜尋字串。另外,因為 * 為重複『前一個 RE 字符』的符號, 因此,在 * 之前必須要緊接著一個 RE 字符喔!例如任意字元則為 『.*』 !
grep -n 'ess*' regular_express.txt
[list]意義:字元集合的 RE 字符,裡面列出想要擷取的字元!
範例:搜尋含有 (gl) 或 (gd) 的那一行,需要特別留意的是,在 [] 當中『謹代表一個待搜尋的字元』, 例如『 a[afl]y 』代表搜尋的字串可以是 aay, afy, aly 即 [afl] 代表 a 或 f 或 l 的意思!
grep -n 'g[ld]' regular_express.txt
[n1-n2]意義:字元集合的 RE 字符,裡面列出想要擷取的字元範圍!
範例:搜尋含有任意數字的那一行!需特別留意,在字元集合 [] 中的減號 - 是有特殊意義的,他代表兩個字元之間的所有連續字元!但這個連續與否與 ASCII 編碼有關,因此,你的編碼需要設定正確(在 bash 當中,需要確定 LANG 與 LANGUAGE 的變數是否正確!) 例如所有大寫字元則為 [A-Z]
grep -n '[A-Z]' regular_express.txt
[^list]意義:字元集合的 RE 字符,裡面列出不要的字串或範圍!
範例:搜尋的字串可以是 (oog) (ood) 但不能是 (oot) ,那個 ^ 在 [] 內時,代表的意義是『反向選擇』的意思。 例如,我不要大寫字元,則為 [^A-Z]。但是,需要特別注意的是,如果以 grep -n [^A-Z] regular_express.txt 來搜尋,卻發現該檔案內的所有行都被列出,為什麼?因為這個 [^A-Z] 是『非大寫字元』的意思, 因為每一行均有非大寫字元,例如第一行的 "Open Source" 就有 p,e,n,o.... 等等的小寫字
grep -n 'oo[^t]' regular_express.txt
\{n,m\}意義:連續 n 到 m 個的『前一個 RE 字符』
意義:若為 \{n\} 則是連續 n 個的前一個 RE 字符,
意義:若是 \{n,\} 則是連續 n 個以上的前一個 RE 字符!
範例:在 g 與 g 之間有 2 個到 3 個的 o 存在的字串,亦即 (goog)(gooog)
grep -n 'go\{2,3\}g' regular_express.txt

另外,由於字元擷取通常會有大小寫、數字、特殊字元等等的差異,因此我們也能夠使用如下的符號來代表某些特殊字元:

特殊符號代表意義
[:alnum:]代表英文大小寫字元及數字,亦即 0-9, A-Z, a-z
[:alpha:]代表任何英文大小寫字元,亦即 A-Z, a-z
[:blank:]代表空白鍵與 [Tab] 按鍵兩者
[:cntrl:]代表鍵盤上面的控制按鍵,亦即包括 CR, LF, Tab, Del.. 等等
[:digit:]代表數字而已,亦即 0-9
[:graph:]除了空白字元 (空白鍵與 [Tab] 按鍵) 外的其他所有按鍵
[:lower:]代表小寫字元,亦即 a-z
[:print:]代表任何可以被列印出來的字元
[:punct:]代表標點符號 (punctuation symbol),亦即:" ' ? ! ; : # $...
[:upper:]代表大寫字元,亦即 A-Z
[:space:]任何會產生空白的字元,包括空白鍵, [Tab], CR 等等
[:xdigit:]代表 16 進位的數字類型,因此包括: 0-9, A-F, a-f 的數字與字元

請操作者進行如下的例題來處理相關任務:

例題 9.1.2-1: 使用 grep 熟悉正規表示法的應用
  1. 找出 /etc/services 內含 http 關鍵字的那幾行
  2. 承上,若僅須『開頭含有 http 』字樣的那幾行?
  3. 承上,若僅須『開頭含有 http 或 https 』字樣的那幾行?
  4. 承上,若僅須『開頭含有 http 或 https 』字樣之外,且後面僅能接空白字元或 [tab] 字元的那幾行?
  5. 承上,若僅須『開頭含有 http 且後續接有 80 』字樣的那幾行
  6. 找出 /etc/services 內含有星號 (*) 的那幾行
  7. 找出 /etc/services 內含有星號,且星號前為英文(不論大小寫)的那幾行
  8. 找出 /etc/services 含有一個數字且後面緊鄰一個大寫字元的那幾行
  9. 找出 /etc/services 開頭是一個數字緊鄰一個大寫字元的那幾行
  10. 使用 find /etc 找出檔名,並找出結尾含有『 .conf 』的檔名資料
  11. 承上,且含有『大寫字元或數字』檔名在內的那幾個

9.1.3:sed 工具的使用

  • 利用 sed 進行資料的取代

sed 也是支援正規表示法的一項工具軟體,具有很多很好用的功能在內!過去我們曾經使用過 ifconfig 與 awk 來找到 IP , 現在讓我們使用 sed 來處理 IP 的設定。最基礎的 sed 功能為取代,如下所示:

[student@localhost ~]$ sed 's/舊字串/新字串/g' 檔案內容

使用者只要替換『新舊字串』內容,即可處理相關的字串修訂。現在讓我們來處理 IP 的擷取。使用 ifconfig ens3 來輸出網路資料, 之後以 grep 取出 inet 那一行 (注意: ens3 請依據你的系統網卡來調整):

[student@localhost ~]$ ifconfig ens3 | grep 'inet[[:space:]]'
        inet 172.16.5.237  netmask 255.255.0.0  broadcast 172.16.255.255

使用 sed 取代開頭到 inet 空白的項目:

[student@localhost ~]$ ifconfig ens3 | grep 'inet[[:space:]]' | \
> sed 's/^.*inet[[:space:]]//g'
172.16.5.237  netmask 255.255.0.0  broadcast 172.16.255.255

再接著取消空白netmask 之後的訊息

[student@localhost ~]$ ifconfig ens3 | grep 'inet[[:space:]]' | \
> sed 's/^.*inet[[:space:]]//g' | \
> sed 's/[[:space:]]*netmask.*$//g'
172.16.5.237
  • 以 sed 進行資料的輸出

除了替換資料之外,sed 還可以擷取出特定的關鍵行數,例如只想要取出 10-15 行的 /etc/passwd 內容時,可以這樣做:

[student@localhost ~]$ cat -n /etc/passwd | sed -n '10,15p' 
請使用 man sed 查詢一下 -n 這個選項的功能!此外,亦請分別就上面這個指令,列出有加 -n 與沒加 -n 的差別!

上面這個動作在處理一些腳本化程式時,相當有幫助!而如果想要直接修改檔案內容時,例如想要將 .bashrc 內的 function 改成大寫時, 也可以這樣做:

[student@localhost ~]$ sed 's/function/FUNCTION/g' .bashrc
.......
# User specific aliases and FUNCTIONs >==這裡會變大寫

[student@localhost ~]$ sed -i 's/function/FUNCTION/g' .bashrc

加上 -i 選項後,該改變直接寫入檔案,且不會在螢幕上輸出了!因此使用上需要特別注意!

例題 9.1.3-1: 使用 sed 進行正規表示法的練習
  1. 找出 /etc/passwd 裡面,結尾是 bash 的那幾行
  2. 承上,透過 sed 將 /bin/bash 改成 /sbin/nologin 顯示到螢幕上
  3. 承上,透過 tr 這個指令,將全部的英文都變成大寫字元

9.2:學習 shell script

shell script 對管理員來說,是一項非常好用的工具!請讀者們一定要自己手動設計過一次相關的腳本程式, 而且能夠針對自己管理的伺服器進行一些例行工作的優化,才會更有感覺。

9.2.1:基礎 shell script 的撰寫與執行

shell script 的撰寫其實沒有很難,基本上需要注意到:

  • 指令的執行是從上而下、從左而右的分析與執行 (這點跟傳統程式語言差很大!);
  • 指令的下達中,指令、選項與參數間的多個空白都會被忽略掉;
  • 空白行也將被忽略掉,並且 [tab] 按鍵所推開的空白同樣視為空白鍵;
  • 如果讀取到一個 Enter 符號 (CR) ,就嘗試開始執行該行 (或該串) 命令;
  • 至於如果一行的內容太多,則可以使用『 \[Enter] 』來延伸至下一行;
  • 『 # 』可做為註解!任何加在 # 後面的資料將全部被視為註解文字而被忽略!

至於 shell script 的執行,例如有名為 /home/student/shell.sh 的 script 時,可以用底下的方法:

  • 直接指令下達: shell.sh 檔案必須要具備可讀與可執行 (rx) 的權限,然後:
    • 絕對路徑:使用 /home/student/shell.sh 來下達指令;
    • 相對路徑:假設工作目錄在 /home/student/ ,則使用 ./shell.sh 來執行
    • 變數『PATH』功能:將 shell.sh 放在 PATH 指定的目錄內,例如: ~/bin/
  • 以 bash 程式來執行:透過『 bash shell.sh 』或『 sh shell.sh 』來執行

底下我們將使用 student 的身份,並在 ~/bin 底下建立多個 shell script 來作為練習。首先,如果操作者執行 myid.sh 時, 系統會輸出這個帳號的 id 指令輸出訊息,並且輸出使用者的家目錄 (${HOME})、以及歷史命令紀錄筆數 (${HISTSIZE}), 最後列出所有的命令別名 (alias) 時,我們可以這樣做:

  1. 請先宣告使用的 script 為 bash ;
  2. 請先說明程式的功能 (需要用 # 註解)
  3. 請先說明程式的撰寫者 (需要用 # 註解)
  4. 秀出『 This script will show your account messages 』
  5. 秀出『 The 'id' command output is: 』
  6. 秀出 id 這個指令的結果
  7. 秀出『 your user's home is: ${HOME} 』的結果
  8. 秀出『 your history record: ${HISTSIZE} 』的結果
  9. 秀出『 your command aliases: 』
  10. 顯示出 alias 的結果
  11. 顯示出家目錄的檔名結果

上述的結果一項一項撰寫成為 myid.sh 的內容如下:

[student@localhost ~]$ mkdir bin
[student@localhost ~]$ cd bin
[student@localhost bin]$ vim myid.sh
#!/bin/bash
# This script will use id, echo to show account's messages
# write by VBird 2016/04/27
echo "This script will show your accout messages."
echo "The 'id' command output is: "
id
echo "your user's home is: ${HOME}"
echo "your history record: ${HISTSIZE}"
echo "your command aliases: "
alias
echo "your home dir's filenames: "
ll ~

例題 9.2.1-1: 基礎 shell script 的執行與相關環境參數的功能
  1. 如何直接以 bash 或 sh 去執行這隻腳本 (指的是直接用 bash 指令去執行,而不是執行 myid.sh)?
  2. 承上,執行過程中,如果還需要輸出程式碼之後才執行,可以加上哪個選項?(此功能相當有用!可用以檢測程式碼的錯誤, debug)
  3. 若需要直接輸入 myid.sh 就能執行時,需要有什麼設定 (包括權限、路徑等等的資料) ?
  4. 如何使用絕對路徑來執行?
  5. 若你剛好在工作目錄下看到這個腳本,但你不確定工作目錄有沒有在 PATH 環境中,該如何下達指令執行該腳本?
  6. 為何 alias 的結果沒有輸出?若執行該腳本時還要輸出目前的 alias ,該如何執行?為什麼?

9.2.2:shell script 的執行環境

之前曾經談過程序的觀察,且程序之間是有相依性的,因此可以使用 pstree 來觀察程序的相依行為。 那麼使用 shell script 時,他與當前的 shell 有無關係呢?底下舉例來瞧瞧。如果你有一個如下的腳本, 該如何進入到該目錄去?

[student@localhost ~]$ cd ~/bin
[student@localhost bin]$ vim gototmp.sh
#!/bin/bash
# this shell script will take you togo /tmp directory.
# VBird 2016/05/02
cd /tmp
pwd
[student@localhost bin]$ chmod a+x gototmp.sh
例題 9.2.2-1:請問,當你執行 gototmp.sh 這個腳本之後,以及『執行期間』,你的工作目錄會在哪裡?為什麼?
  • 使用 source 或 . 來執行腳本

事實上,執行腳本有基本的兩種方式:

  • 直接產生一個新的程序 (process) 來執行,例如 bash script.sh、 ./script.sh 都屬於這一種;
  • 將 script.sh 的指令,呼叫進來目前的這個 bash process 執行,而不是產生一個新的 process

上述的第二種就是透過 source 或 . 來處理的。現在,請使用『 source ~/bin/gototmp.sh 』指令, 再次查閱一下你的工作目錄是否正確的進入到 /tmp 了?

[student@localhost bin]$ source gototmp.sh
/tmp
[student@localhost tmp]$ pwd
/tmp
例題 9.2.2-2:你在操作 Linux 系統的過程中,可能會切換到許多不同的 shell (例如 bash 轉到 csh 等等)。不過這些操作環境中,均需要使用到底下的變數, 且這些變數是在有需要時才載入的 (不是寫入到 .bashrc),因此需要額外撰寫成 myenv.sh。內容需要:
  1. 設計所需要的環境操作參數:
    1. 設計 MYIP 的變數為目前系統的 IP (假設網路卡為 ens3 時)
    2. 設計 mywork 的變數為指定到 /usr/local/libexec 目錄中
    3. 設計 megacli 命令別名為 /opt/mega/cli/command 這個指令(此指令並不存在,僅作為範例用)
  2. 設計完畢之後,若要使用這個檔案內的資料,該如何執行?

9.2.3:以對談式腳本及外帶參數計算 pi

在第二堂課我們曾經使用過 bc 來計算數學的 pi,亦即使用『echo "scale=10; 4*a(1)" | bc -lq』來計算 pi。 而如果需要輸出更正確的 pi 值,可以將 scale 的參數放大,例如『echo "scale=20; 4*a(1)" | bc -lq』來計算。 我們是否能夠使用一個變數,讓該變數帶入腳本後,讓用戶可以與系統對談呢?這時有兩種基本的方法可以達到這個目的:

  • 以 read 作為對談式腳本的設計依據
  • 以 ${1} 等作為外帶參數的設計依據
  • 使用 read 讓用戶輸入參數:

讀者可以先理解 read 的用法:

[student@localhost ~]$ read -p 'Input your name: ' name1
Input your name: VBird Tsai

[student@localhost ~]$ echo ${name1}
VBird Tsai

亦即 read 會將使用者輸入的資料變成變數內容,之後就可以輕易的進行變數設定的任務。因此,若需要讓使用者與程式互動來輸入 pi 的計算精確度, 可以寫下如下的腳本:

[student@localhost ~]$ vim ~/bin/mypi.sh
#!/bin/bash
# Program:
#	User input a scale number to calculate pi number.
# History:
# 2015/07/16	VBird	First release
PATH=/bin:/sbin:/usr/bin:/usr/sbin:/usr/local/bin:/usr/local/sbin:~/bin
export PATH
echo -e "This program will calculate pi value. \n"
echo -e "You should input a float number to calculate pi value.\n"
read -p "The scale number (10~10000) ? " num
echo -e "Starting calculate pi value.  Be patient."
time echo "scale=${num}; 4*a(1)" | bc -lq

[student@localhost ~]$ chmod a+x ~/bin/mypi.sh
[student@localhost ~]$ mypi.sh
This program will calculate pi value.

You should input a float number to calculate pi value.

The scale number (10~10000) ? 50
Starting calculate pi value.  Be patient.
3.14159265358979323846264338327950288419716939937508

real    0m0.001s
user    0m0.000s
sys     0m0.002s

此時,只要用戶執行 mypi.sh ,就可以手動輸入 10 到 10000 之間不等的數值,讓系統直接進行運算工作!此外你得要注意, 在 echo 顯示資訊時,可以使用 \t, \n 等方法來處理斷行、[tab] 按鈕的樣式~只是需要加上 -e 的選項才行!你可以『 man echo 』查詢可用的變數!

例題 9.2.3-1:建立一個名為 /usr/local/bin/listcmd.sh 的腳本,該腳本可以完成底下的各項工作:
  1. 先製作這個腳本:
    1. 第一行一定要宣告 shell
    2. 顯示出這個腳本的目的 (中英文均可,例如: This shell script will list your command's full path name and permissions.)
    3. 開始透過 read 讓用戶輸入指令名稱
    4. 透過上一步驟取得指令名稱後,透過 which 找到這個指令的完整路徑
    5. 利用 ls -l 列出這個指令的完整權限
    6. 利用 getfacl 列出這個指令的完整權限
    7. 離開 shell script,並回傳 0 的數值。
  2. 最後將該指令的權限修訂成全部成員均可執行,並執行一次確認狀態。執行的結果會有點像這樣:
    [student@localhost ~]$ listcmd.sh
    
    This shell script will list your command's full path name and permissions.
    
    Please input a command name: ls
    -rwxr-xr-x. 1 root root 166448  5月 12  2019 /usr/bin/ls
    getfacl: Removing leading '/' from absolute path names
    # file: usr/bin/ls
    # owner: root
    # group: root
    user::rwx
    group::r-x
    other::r-x
    
  • 透過外帶參數的功能執行:

在第 8 堂課的課程內容曾經短暫介紹過 shell 內有個名為 ${1} 的變數,即是 shell script 的外帶參數。事實上,外帶參數可以有多個, 相關的『數字變數』有底下的相關性:

/path/to/scriptname  opt1  opt2  opt3  opt4 
       $0             $1    $2    $3    $4

執行的腳本檔名為 ${0} 這個變數,第一個接的參數就是 ${1}。所以,只要在 script 裡面善用 ${1} ,就可以很簡單的立即下達某些指令功能了! 除了這些數字的變數之外,尚有底下這些常見的變數可以在 shell script 內呼叫:

  • $# :代表後接的參數『個數』,以上表為例這裡顯示為『 4 』;
  • "$@" :代表『 "$1" "$2" "$3" "$4" 』之意,每個變數是獨立的(用雙引號括起來);
  • "$*" :代表『 "$1c$2c$3c$4" 』,其中 c 為分隔字元,預設為空白鍵, 所以本例中代表『 "$1 $2 $3 $4" 』之意。

在某些時刻,執行腳本可能是在背景中,因此不可能跟使用者互動 (記得 jobs, fg, bg 的情況下),此時就能夠透過這種外帶參數的方式來執行。 例如我們將 mypi.sh 修改成外帶參數的 mypi2.sh ,讀者可以這樣嘗試:

[student@localhost ~]$ cp ~/bin/mypi.sh ~/bin/mypi2.sh
[student@localhost ~]$ vim ~/bin/mypi2.sh
#!/bin/bash
# Program:
#	User input a scale number to calculate pi number.
# History:
# 2015/07/16	VBird	First release
PATH=/bin:/sbin:/usr/bin:/usr/sbin:/usr/local/bin:/usr/local/sbin:~/bin
export PATH
num=${1}
echo -e "This program will calculate pi value. \n"
#read -p "The scale number (10~10000) ? " num
echo -e "Starting calculate pi value.  Be patient."
time echo "scale=${num}; 4*a(1)" | bc -lq

[student@localhost ~]$ chmod a+x ~/bin/mypi2.sh
[student@localhost ~]$ mypi2.sh 50
This program will calculate pi value.

Starting calculate pi value.  Be patient.
3.14159265358979323846264338327950288419716939937508

real    0m0.001s
user    0m0.000s
sys     0m0.002s

讀者可以發現,mypi2.sh 將 mypi.sh 內的兩行輸出 (說明程式功能與 read 的功能) 取消,而在不修改其他程式碼的情況下, 讓 num=${1} 來讓精確度使用第一個外帶參數的方式來處理。

例題 9.2.3-2:建立一個名為 /usr/local/bin/listcmd2.sh 的腳本,該腳本可以完成底下的各項工作:
  1. 先修改這個腳本,處理前,請先將 listcmd.sh 複製成為 listcmd2.sh 喔!
    1. 增加一行功能,讓指令名稱由外帶參數來處理 (${1})
    2. 註解取消,或刪除 read 的使用者打字輸入功能。
  2. 最後將該指令的權限修訂成全部成員均可執行,並執行一次確認狀態,指令執行有點像這樣:
    [root@station5-237 bin]# listcmd2.sh pwd
    
    This shell script will list your command's full path name and permissions.
    
    -rwxr-xr-x. 1 root root 46840  5月 12  2019 /usr/bin/pwd
    getfacl: Removing leading '/' from absolute path names
    # file: usr/bin/pwd
    # owner: root
    # group: root
    user::rwx
    group::r-x
    other::r-x
    

9.2.4:透過 if .. then 設計條件判斷

讀者可以思考 mypi.sh 這個腳本的運作,雖然指定使用者應該要輸入 10~10000 的數值,但是卻沒有在腳本中進行防呆, 因此,當使用者輸入 (1)非為數值的字串及 (2)輸入超過數值範圍時,就可能發生程式誤判的情況,如下:

[student@localhost ~]$ mypi.sh
This program will calculate pi value.

You should input a float number to calculate pi value.

The scale number (10~10000) ? whoami
Starting calculate pi value.  Be patient.
0

real    0m0.001s
user    0m0.001s
sys     0m0.001s

[student@localhost ~]$ mypi.sh
This program will calculate pi value.

You should input a float number to calculate pi value.

The scale number (10~10000) ?  <==這裡直接按下 enter 就好
Starting calculate pi value.  Be patient.
(standard_in) 1: syntax error

real    0m0.001s
user    0m0.000s
sys     0m0.002s

此時就會發生不可預期的錯誤。讀者在設計程式腳本時,應該就用戶可能會輸入的字元或通常的運作方式進行分析, 先設計好防呆,在程式碼的運作上比較不容易出問題。想要達成這種防呆的機制,需要用到條件判斷式的支援,一般 shell script 條件判斷的語法為:

if [ 條件判斷式 ]; then
	當條件判斷式成立時,可以進行的指令工作內容;
fi   <==將 if 反過來寫,就成為 fi 啦!結束 if 之意!

相關條件設定的方式已經在第八堂課談過,請自行前往參閱。若有多重條件判斷,則使用下列方式:

# 一個條件判斷,分成功進行與失敗進行 (else)
if [ 條件判斷式 ]; then
	當條件判斷式成立時,可以進行的指令工作內容;
else
	當條件判斷式不成立時,可以進行的指令工作內容;
fi

如果考慮更複雜的情況,則可以使用這個語法:

# 多個條件判斷 (if ... elif ... elif ... else) 分多種不同情況執行
if [ 條件判斷式一 ]; then
	當條件判斷式一成立時,可以進行的指令工作內容;
elif [ 條件判斷式二 ]; then
	當條件判斷式二成立時,可以進行的指令工作內容;
else
	當條件判斷式一與二均不成立時,可以進行的指令工作內容;
fi

如果考慮兩個以上的條件混合執行時,就需要使用 -a 或 -o 的協助。

# 兩個條件都要成立才算成立的情況:
if [ 條件判斷式一 -a 條件判斷二 ]; then
	兩個條件都成立,這時才執行 (and 的概念)
fi

# 兩個條件中,任何一個條件成立都算 OK 的情況:
if [ 條件判斷式一 -o 條件判斷二 ]; then
	隨便哪一個條件成立,都可以執行 (or 的概念)
fi

以上面的語法來補足 mypi.sh 的防呆,大致防呆的思考可以是:

  1. 使用者輸入的資料非為數值的狀態:
    • 不可以是空白也不可以有非數字的字元存在
    • 若有上述狀況,則數值改為預設的 20
  2. 使用者輸入的資料不在規範的範圍內:
    • 若小於 10 則以 10 取代;
    • 若大於 10000 則以 10000 取代;

大致的防呆流程就像上面所敘述,接下來讀者們可以使用語法來將這些流程加入 mypi.sh:

[student@localhost ~]$ vim ~/bin/mypi.sh
#!/bin/bash
# Program:
#	User input a scale number to calculate pi number.
# History:
# 2015/07/16	VBird	First release
PATH=/bin:/sbin:/usr/bin:/usr/sbin:/usr/local/bin:/usr/local/sbin:~/bin
export PATH
echo -e "This program will calculate pi value. \n"
echo -e "You should input a float number to calculate pi value.\n"
read -p "The scale number (10~10000) ? " num

# 判斷是否有非為數字的字元,若有,則會出現,若無則是空白
checking=$( echo ${num} | grep '[^0-9]' )
if [ "${num}" == "" -o "${checking}" != "" ]; then
        num=20
fi

if [ "${num}" -le 10 ]; then
        num=10
elif [ "${num}" -ge 10000 ]; then
        num=10000
fi
echo "Use scale number: ${num}"

echo -e "Starting calculate pi value.  Be patient."
time echo "scale=${num}; 4*a(1)" | bc -lq

接下來請執行數次 mypi.sh ,並分別輸入不同的資料 (Enter, 文字, 小於 10 的數字, 大於 10000 的數字等等), 以確認自己的處理方式應為可行。

例題 9.2.4-1:進行防呆測試
請透過相同的方法來修改 mypi2.sh ,讓該腳本也能夠防呆。

9.2.5:以 case .. esac 設計條件判斷

若讀者只想讓 mypi.sh 的操作者體驗一下 pi 的計算,因此只想給予 20, 100, 1000 三個數值, 當使用者不是輸入此類數值,則告知對方僅能輸入這三個數值。若以 if ... then 的方式來說,需要填寫的判斷式稍嫌多了些。 此時可以使用 case ... esac 來做設計。

case  $變數名稱 in   <==關鍵字為 case ,還有變數前有錢字號
  "第一個變數內容")   <==每個變數內容建議用雙引號括起來,關鍵字則為小括號 )
	程式段
	;;            <==每個類別結尾使用兩個連續的分號來處理!
  "第二個變數內容")
	程式段
	;;
  *)                  <==最後一個變數內容都會用 * 來代表所有其他值
	不包含第一個變數內容與第二個變數內容的其他程式執行段
	exit 1
	;;
esac                  <==最終的 case 結尾!『反過來寫』思考一下!

請使用上述的語法,搭配僅能輸入 20, 100, 1000 三個數值來撰寫 mypi3.sh 腳本:

[student@localhost ~]$ cp ~/bin/mypi.sh ~/bin/mypi3.sh
[student@localhost ~]$ vim ~/bin/mypi3.sh
#!/bin/bash
# Program:
#       User input a scale number to calculate pi number.
# History:
# 2015/07/16    VBird   First release
PATH=/bin:/sbin:/usr/bin:/usr/sbin:/usr/local/bin:/usr/local/sbin:~/bin
export PATH
echo -e "This program will calculate pi value. \n"
echo -e "You should input a float number to calculate pi value.\n"
read -p "The scale number (20,100,1000) ? " num

case ${num} in
"20")
        echo "Your input is 20"
        ;;
"100")
        echo "Your input is 100"
        ;;
"1000")
        echo "Your input is 1000"
        ;;
*)
        echo "You MUST input 20|100|1000"
        echo "I stop here"
        exit 0
        ;;
esac

echo -e "Starting calculate pi value.  Be patient."
time echo "scale=${num}; 4*a(1)" | bc -lq

[student@localhost ~]$ chmod a+x ~/bin/mypi3.sh
[student@localhost ~]$ mypi3.sh
This program will calculate pi value.

You should input a float number to calculate pi value.

The scale number (20,100,1000) ? 30
You MUST input 20|100|1000
I stop here

[student@localhost ~]$ mypi3.sh
This program will calculate pi value.

You should input a float number to calculate pi value.

The scale number (20,100,1000) ? 100
Your input is 100
Starting calculate pi value.  Be patient.
3.141592653589793238462643383279502884197169399375105820974944592307\
8164062862089986280348253421170676

real    0m0.003s
user    0m0.003s
sys     0m0.000s

這樣就可以縮減使用者輸入的參數了!

例題 9.2.5-1:
透過 case ... esac 的方法,修改 mypi2.sh 變成 mypi4.sh,以外帶參數的方式,讓 mypi4.sh 只能支援 20|100|1000 的數值, 若使用者外帶參數不是這三個,則顯示『 Usage: mypi4.sh 20|100|1000 』的螢幕提示,否則就直接計算 pi 值並輸出結果。

9.3:延伸正規表示法的字符

事實上,一般讀者只要瞭解基礎型的正規表示法大概就已經相當足夠了,不過,某些時刻為了要簡化整個指令操作, 瞭解一下使用範圍更廣的延伸型正規表示法的表示式會更方便呢!舉個簡單的例子,一般我們要去除資訊內的 (1)空白行與 (2)註解行時, 可能需要使用兩次基本正規表示法,如下所示:

[student@localhost ~]$ grep -v '^$' /etc/crontab  | grep -v '^#'

延伸型正規表示法,可以透過『群組』的功能,將需要擷取的關鍵字寫在一起,整個指令就會變得更清爽:

[student@localhost ~]$ egrep -v '^$|^#' /etc/crontab

grep 支援基本正規表示法,若要使用到延伸正規表示法,就得要使用 egrep 這個指令來替換才行。

熟悉了正規表示法之後,到這個延伸型的正規表示法,你應該也會想到,不就是多幾個重要的特殊符號嗎? 是的~所以,我們就直接來說明一下,延伸型正規表示法有哪幾個特殊符號?

RE 字符意義與範例
+意義:重複『一個或一個以上』的前一個 RE 字符
範例:搜尋 (god) (good) (goood)... 等等的字串。 那個 o+ 代表『一個以上的 o 』所以,底下的執行成果會將第 1, 9, 13 行列出來。
egrep -n 'go+d' regular_express.txt
?意義:『零個或一個』的前一個 RE 字符
範例:搜尋 (gd) (god) 這兩個字串。 那個 o? 代表『空的或 1 個 o 』所以,上面的執行成果會將第 13, 14 行列出來。 有沒有發現到,這兩個案例( 'go+d' 與 'go?d' )的結果集合與 'go*d' 相同? 想想看,這是為什麼喔! ^_^
egrep -n 'go?d' regular_express.txt
|意義:用或( or )的方式找出數個字串
範例:搜尋 gd 或 good 這兩個字串,注意,是『或』! 所以,第 1,9,14 這三行都可以被列印出來喔!那如果還想要找出 dog 呢?
egrep -n 'gd|good' regular_express.txt
egrep -n 'gd|good|dog' regular_express.txt
()意義:找出『群組』字串
範例:搜尋 (glad) 或 (good) 這兩個字串,因為 g 與 d 是重複的,所以, 我就可以將 la 與 oo 列於 ( ) 當中,並以 | 來分隔開來,就可以啦!
egrep -n 'g(la|oo)d' regular_express.txt
()+意義:多個重複群組的判別
範例:將『AxyzxyzxyzxyzC』用 echo 叫出,然後再使用如下的方法搜尋一下!
echo 'AxyzxyzxyzxyzC' | egrep 'A(xyz)+C'
上面的例子意思是說,我要找開頭是 A 結尾是 C ,中間有一個以上的 "xyz" 字串的意思~

以上這些就是延伸型的正規表示法的特殊字元。另外,要特別強調的是,那個 ! 在正規表示法當中並不是特殊字元, 所以,如果你想要查出來檔案中含有 ! 與 > 的字行時,可以這樣:

[student@localhost ~]$ grep -n '[!>]' regular_express.txt

另外,延伸正規表示法對於一般使用者來說,大致上最重要的就是群組 (|) 這種用法~所以,目前只要知道 egrep 這個指令的使用時機, 以及透過 | 來分隔要同時擷取的關鍵字即可!

9.4:課後練習操作

  • 上課的課後練習,非作業:
  1. 正規表示法的應用-擷取重要的設定項目 for /etc/man_db.conf
    1. 先確認 /etc/man_db.conf 裡面有幾行?
    2. 再確認這個檔案裡面不是空白行也不是註解開頭的,總共有幾行?
    3. 最終只列出開頭是『 MANPATH_MAP 』的那幾行即可。
  2. 正規表示法的應用-擷取重要的設定項目 for /etc/rsyslog.conf
    1. 根據前一題的作法,請將 /etc/rsyslog.conf 當中,非空白行、非註解行取出 (注意喔, # 在最前面才是全註解!)
    2. 承上,輸出的資料中,還是含有 # ,請將每一行的 # 到最後面的資料,全部刪除,只保留需要的設定部份。
    3. 從 /etc/rsyslog.conf 當中,找出含有 tcp 或 udp 那一行,包括行號列出
    4. 根據上面的輸出結果,你覺得 rsyslog 這個服務,可能會啟動那一個網路埠口號碼?
  3. 撰寫一隻名為 lm 的指令,這個指令會取得後面所有的參數 ( "$@" ),然後使用『 ls -al ${@} | more 』的樣式, 進行一頁一頁翻動的功能。這個指令放在大家都能執行的 /usr/local/bin 裡面,請設定好權限之後, 以 student 的身份執行看看喔!
  • 作業 (不提供學生答案,僅提供教師參考答案)

作業硬碟一般操作說明:

  • 開啟雲端虛擬機器前,請務必確認你開啟的硬碟是『unit09』,否則就會做錯題目
  • 若要使用圖形界面,請務必使用 student 身份登入,若需要切換身份,再啟用終端機處理。
  • 若有簡答題需要使用中文,請自行以第一堂課的動作自行處理輸入法安裝。
  • 每部虛擬機器均有獨特的網卡位址,請勿使用他人硬碟上傳,否則計分為 0 分。
  • 每位同學均有自己的 IP 尾數,請先向老師詢問您的 IP 尾數,才可以進行作業上傳。
  • 最終上傳作業結果,請務必使用 root 身份上傳。
  • 進入作業硬碟後,先用 root 身份執行 vbird_book_setup_ip , 執行流程請參考:vbird_book_setup_ip

作業當中,某些部份可能為簡答題~若為簡答題時,請將答案寫入 /home/student/ans.txt 當中,並寫好正確題號,方便老師訂正答案。 請注意,檔名寫錯將無法上傳!

請使用 root 的身份進行如下實做的任務。直接在系統上面操作,操作成功即可,上傳結果的程式會主動找到你的實做結果。

  1. (20%)分析『本日』登錄檔資訊的相關設定,重點在實做與練習正規表示法:(將答案寫入 /root/ans09.txt 中)
    1. 先解析一下 /var/log/messages 的內容中,每條訊息的最前面紀錄的日期,如何使用 date 搭配選項來輸出一樣的字串?(你或許需要知道搭配語系輸出)
    2. 設定一個變數名稱為 logday ,讓 logday 的內容為剛剛查詢到的日期格式
    3. 如何透過 grep 搭配 ${logday} 等方式,將 /var/log/messages 的資訊中,跟今天有關的日期取出來到螢幕上查閱?(注意日期要出現在行首喔!)
    4. 承上,上述的輸出結果中,我不想要關鍵字 dbus-daemon 與 dbus[數字] 的內容又該如何處理?
    5. 我想要透過一串指令,直接將 /etc/selinux/config 檔案內,行首出現『SELINUX=??? 』的那一行(一整行喔)資料,強制替換成『SELINUX=enforcing』, 且直接修改該檔案,該如何處理?
    6. 我要把 /etc/hosts 內容全部轉成大寫字元後,轉存到 /dev/shm/upperhosts 檔案,指令該如何處理?
  2. (10%)建立一隻名為 /usr/local/bin/myprocess 的腳本,腳本內容主要為:
    1. 第一行一定要宣告 shell 為 bash 才行;
    2. 主要僅執行『 /bin/ps -Ao pid,user,cpu,tty,args 』
    3. 這隻腳本必須要讓所有人都可以執行才行!
  3. (10%)寫一隻名為 /usr/local/bin/mydate.sh 的腳本,執行後可以輸出如下的資料:
    1. 第一行一定要宣告 shell 才對!
    2. 以 西元年/月/日 顯示出目前的日期
    3. 以 小時:分鐘:秒鐘 顯示出目前的時間
    4. 輸出從 1970/01/01 到目前累計的秒數
    5. 列出這個月的月曆,且依據台灣的習慣,輸出時,以星期一為一週的開始。
    6. 這隻腳本必須要讓所有人都可以執行才行!
  4. (20%)寫一隻 /usr/local/bin/listcmd.sh 的腳本,該腳本執行後,會告知底下相關的事宜:
    1. 腳本的執行方式為『 listcmd.sh passwd 』,其中 passwd 可以使用任何檔名來取代
    2. 第一行一定要宣告 shell
    3. 先顯示出這個腳本的目的 (中英文均可,例如: This shell script will list your command's full path name and permissions.)
    4. 判斷是否有外帶參數,若沒有外帶參數,請 (1)螢幕顯示『 Usage: ${0} cmd_name 』,(2)並以回傳值 2 離開程式
    5. 使用『 which ${1} 2> /dev/null 』的結果,判斷該字串是否為指令,你可以將這個指令的結果帶入成為某個變數, 然後分析這個變數值。基本上,這個字串值若存在,則代表是指令,若為空值,則非指令。
    6. 若該字串為指令,則依序輸出:
      • 輸出指令的完整路徑
      • 用 ls -l 列出這個指令的完整權限
      • 利用 getfacl 列出這個指令的完整權限
      • 離開 shell script,並回傳 0 的數值。
    7. 若該字串不為指令,則使用 locate 後面加 /${1}$ 的正規表示法 (locate 要支援正規表示法,必須要輸入特定的選項 請自行 man locate 查到正確的選項支援),然後依據 locate 之後的回傳值處理後續工作
      • 若回傳值 (為 0) 顯示該字串其實具有相同的檔名,則使用 ls -ld 將檔名全部列出,然後以回傳值 0 離開程式
      • 若回傳值 (不為 0) 顯示該字串並不為檔名,則顯示找不到這個檔名,然後以回傳值 10 離開程式
  5. (10%)寫一隻名為 /usr/local/bin/myheha 的腳本,這隻腳本的執行結果會這樣:
    1. 腳本內第一行一定要宣告 shell 為 bash
    2. 當執行 myheha hehe 時,螢幕會輸出『 I am haha 』
    3. 當執行 myheha haha 時,螢幕會輸出『 You are hehe 』
    4. 當外帶參數不是 hehe 也不是 haha 時,螢幕會輸出『 Usage: myheha hehe|haha 』
  6. (20%)寫一隻判斷生日的腳本,名稱為 /usr/local/bin/yourbday.sh,內容為:
    1. 腳本內第一行一定要宣告 shell 為 bash
    2. 指令執行的方式為『 yourbday.sh YYYY-MM-DD 』
    3. 當使用者沒有輸入外帶參數時,螢幕顯示『 Usage: yourbday.sh YYYY-MM-DD 』,並且離開程式
    4. 以正規表示法的方式來查詢生日的格式是否正常,若不正常,重新顯示上面的訊息,並且離開程式
    5. 以 date --date="YYYY-MM-DD" +%s 的回傳值確認時間格式是否正確?若不正確請顯示『 invalid date 』後,離開程式
    6. 分別取得生日與現在的累積秒數,根據兩者的差異,同時假設一年 365.25 天,然後:
      • 如果生日比現在的總累積秒數還要大,代表來自未來,請輸出『You are not a real human..』,之後離開程式
      • 如果所有問題都排除了,那請搭配 bc 來顯示出你的歲數,歲數計算到小數點第二位, 例如『 You are 22.35 years old 』的樣式。
  7. (10%)有一隻腳本名為: /usr/local/sbin/examcheck ,當執行時,應該要出現如下的畫面, 但是程式開發人員寫錯了某些地方,請你將應該有問題的程式碼訂正,使得腳本得以展示如下的結果:
    1. 執行 examcheck ok 時,顯示『Yes! You are right!』
    2. 執行 examcheck false 時,顯示『So sad... your answer is wrong..』
    3. 執行 examcheck otherword 時,顯示『 Usage: examcheck ok|false 』(otherword 為任意字元,隨便不是 ok 與 false 的其他字元之意)

作業結果傳輸:請以 root 的身分執行 vbird_book_check_unit 指令上傳作業結果。 正常執行完畢的結果應會出現【XXXXXX_aa:bb:cc:dd:ee:ff_unitNN】字樣。若需要查閱自己上傳資料的時間, 請在作業系統上面使用瀏覽器查詢: http://192.168.251.254 檢查相對應的課程檔案。 相關流程請參考: vbird_book_check_unit

修改歷史:
  • 2016/05/02:這一章是因為期中考過後,要留點時間來檢討期中考的結果,因此幾乎少了 30~60 分鐘的時間!份量較少的原因!
  • 2017/03/30:已經不考慮要不要期中考檢討了!要將每章都補足 3 小時的課程~所以加入了 if..then, case..esac 的用法!
  • 2020/05/03:由於延伸型正規表示法的『 egrep 'keya|keyb' file 』相當有用途,所以只好加入章節中!
  • 2020/05/07:將課後作業搞定!
2020/05/03 以來統計人數
計數器
其他連結
環境工程模式篇
鳥園討論區
鳥哥舊站

今日 人數統計
昨日 人數統計
本月 人數統計
上月 人數統計