BASH (Bourne Again Shell) 是 GNU 開發的 Shell (外殼),是以由 Steve Bourne 開發的 Shell 為名,開發的目的,是希望借 BASH 代替一般商業化的 Shell。 Shell 的意念在 UNIX Version 7 時已經出現,其版本是 Bourne Shell ,也就是 sh。
後來,由 Bill Joy (名字很熟識吧?他就是 vi 的作者,亦是 BSD 早期開發的重要人物之一) 開發的 C Shell 更受廣泛使用,因為 C Shell 的語法很像 C,管理員只需懂得 C 程式就可以簡單使用 C Shell。對於建立 GNU 系統,撰寫 Shell 外殼是有必要的,而 BASH 是由 Brian Fox (GNU 出名的人物) 撰寫,並由 Chet Ramey 所維持,現時最新版本為 2.04。
現在,絕大多數的 Linux distribution 預設都使用 BASH,而在其它系統像 OpenBSD 及 FreeBSD 等都使用傳統的 csh (C Shell) 或是 ksh (Korn Shell)。而筆者今次介紹的是 BASH,集中在 bash 上的程式 (即 shell script) 寫作,而 bash 環境的應用只會提及而不深入講解,因為撰寫 shell script 對管理上有很大幫助。
第一個的 shell script
在很多情況上系統都會使用 shell script,例如 /etc/rc.d/rc.local、.bash_login 等檔案,甚至定時檢查與 ISP 的連線等都可以使用 shell script 的幫忙。請看例子一,'echo' 就是把字句或變數列印出來,在 bash 的編程中,以$開頭的就是變數,相信有使用 Perl 的朋友都看得懂吧!例子一所做的就是要打印該句出來:
例子一 (test-1.sh):
#!/bin/bash Var1="How are you today ?" echo $Var1
首先請留意一點,在 test-1.sh 的第一行,筆者把之設為交給 /bin/bash 執行,而不是 /bin/sh,原因可能部份 Unix 系統中的 /bin/sh 並不是 bash,這樣執行可能發生問題,所以您還是檢查一下,如果 /bin/sh 是連到 /bin/bash 的 symbolic link 就兩者都可以使用。
在 bash 中的變數是大少寫敏感的,即是說 $Var 不等同 $VAR,這點很多的程式語言都一樣。把 test-1.sh 儲存成檔案後,執行的方法有兩種,您可以直接使用 bash 來執行這個檔案,或是把檔案設為可執行(executable):
例子一執行方法一:
shell@www :~# sh test-1.sh
How are you today ?
例子一執行方法二:
shell@www :~# chmod 0700 test-1.sh shell@www :~# ./test-1.sh How are you today ?
兩種方法都可行,在不同情況上,您可以選擇哪個方法較好。
Bash 的編程支援參數(parameters)。在 C 的編程上,您可使用 argc 及 argv 來找尋輸入至程式的參數;而在 bash 上,$# 就是 argc,是顯示參數的數目;而 $1、$2、$3 等等就是參數值,$@ 是所有參數的陣列(Array)。請參考例子二,它示範了這幾個內置變數的功能:
例子二 (test-2.sh):
#!/bin/bash echo "Total Parameters of $0 : $#" echo "They are : $@"; echo "First parameter is : $1"
例子二執行方法:
shell@www :~# chmod 700 test-2.sh shell@www :~# ./test-2.sh 1st 2nd 3rd Total Parameters of ./test.sh : 3 They are : 1st 2nd 3rd First parameter is : 1st
在例子三中,我們嘗試使用 shell script 執行外部的程式。它是一個在某檔案中找尋字串的 shell script:
例子三 (test-3.sh):
#!/bin/bash echo "Now searching file : ${1##/*/}" grep $2 $1
例子三執行方法:
shell@www :~# ./test-3.sh /etc/services ftp Now searching file : services ftp-data 20/tcp ftp 21/tcp tftp 69/udp sftp 115/tcp bftp 152/tcp
參數一是檔案位置,而參數二就是字串。在這個只有兩行的 shell script中,當然沒有甚麼特別的功能,目的只是顯示在 shell script 中如何執行外部指令。或者我們可以使例子變得複雜一點,當然,請繼續看下去。例子中的 ${1##/*/} 就是把 /etc/ 等刪去,請參考表一。
表一:
變數 | 結果 |
${path} | /usr/local/bin/emacs |
${path#/*/} | local/bin/emacs |
${path##/*/} | emacs |
${path%/*} | /usr/local/bin |
${path:15} | emacs |
${path:10:4} | /bin |
此外,在 bash 的變數中,還有些能即時回傳測試,例如測試變數是否存在,如果沒有,會做別的事情,這些在撰寫功能多的 shell script 時很常用,請參考表二(這裡先假設 $Var1 的值是 "Yes",而 $Var2 沒有定義):
表二:
用法 | 結果 |
${Var1:-No} | 印出 "No" |
${Var2:-Yes} | Var2 沒有變數值,印出 "Yes" 字串 |
${Var1:=No} | 印出 "Yes" 字串 |
${Var2:=Yes} | Var2 現在變數值為 "Yes" |
${Var1:?No} | 印出 "Yes" 字串 |
${Var2:?Yes} | 印出 "Var2: Yes" 字句 |
順帶一提,如果在Shell的環境下直接輸入 echo -e ${PATH//:/' '},印出的就是在 $PATH 變數內的值,而 /:/' ' 會把分隔 $PATH 的 ':'字串變為 newline 印出。
編者註︰要清楚以上所有變數的替換,請看 bash 的 manpage 裡有關 "Parameter Expansion" 的一段。
if... then... else... fi 語法: if <條件 1>; then 執行指令 elif <條件 2>; then 執行指令 else 執行指令 if
在例子四中,我們加入了 if 的條件控制,令 shell script 功能變得好一點;或許在看例子前參考表三,表三是 bash 內可使用的比較字元及測試字元:
表三:
用法 | 解釋 |
str1 = str2 | 比較字串變數是否相同 |
str1 != str2 | 比較字串變數是否不同 |
str1 < str2 | 比較 str1 是否大於 str2 |
str1 > str2 | 比較 str1 是否小於 str2 |
-n str1 | 檢查 str1 是否有定義 |
-z str1 | 檢查 str1 是否沒有定義 |
例子四 (test-4.sh):
#!/bin/bash if [ -z "$1" ]; then echo "Usage : test-4.sh [File] [Keyword]" exit 1; fi echo "Now searching file : ${1##/*/}" grep $2 $1
我們使用了 -z 檢查 $1,即是第一個參數,如果沒有輸入參數,程式會回傳使用的方法,然後結束程式執行;當然我們還可以加入其它更多的檢查,使這個程式變得更強。在例子五中,我們加入對檔案狀態的檢查。
例子五 (test-5.sh):
#!/bin/bash if [ $# != "2" ]; then echo "Usage : test-5.sh [File] [Keyword]" exit 1; fi if [ ! -r "$1" ]; then echo "No such files or permission denied !" fi echo "Now searching file : ${1##/*/}" grep $2 $1
在例子五中加入了一段 if 的條件,"!"的意思是 NOT,即是檢查 $1 是否不能讀 (! -r),就像是 Slackware 的 rc 檔案會檢驗在 /etc/rc.d/ 內有沒有 rc.* 的檔案,並且檢查是否可以執行,有關這些檢查的運算子,可以參考表四:
表四:
選項 | 解釋 |
-d | 檢查是否目錄 |
-e | 檢查是否檔案 |
-f | 檢查是否檔案(非特別檔案如 /dev/*) |
-r | 檢查是否可讀 |
-s | 檢查檔案是否存在及內容不是空的 |
-w | 檢查是否可寫 |
-x | 檢查是否可執行 |
-O | 檢查 Owner 是執行者 |
-G | 檢查 Group 是執行者的群組 |
編者註︰若想知更多,請參考 test 的 manpage。
例子六 (test-6.sh):
#!/bin/bash filename="test-6.sh" echo -n "File owner of $filename is " if [ -O $filename ]; then echo "you !" else echo "not you !" fi
例子六是另一個有關檢查檔案的例子,與前例差不多,唯獨要留意的就是在 echo 中的 -n 參數,-n 的意思就是令 echo 不會在句尾加入 ,這樣,在印出字句時,便不會開新的一行。
for ... in ...; do ...; done
語法:
for x in list do 執行指令 done
for loop 在某些情況很常用,例如要在很多的目錄工作時。請參考例子七;它是一個很簡單的 shell script,作用只是在數個目錄內找尋符合字串的檔案名稱:
例子七 (test-7.sh):
#!/bin/bash IFS=: PATH=/usr/bin:/usr/sbin:/sbin:/bin:/usr/local/sbin:/usr/local/bin if [ -z "$1" ]; then echo "Usage : test-7.sh [Keyword]" exit 1 fi for dir in $PATH do echo "Working in $dir ..." ls -al $dir | grep $1 done exit 0
例子七執行方法︰
shell@www :~# ./test-7.sh login Working in /usr/bin ... -r-sr-xr-x 1 root bin 20480 Dec 24 13:29 login -r-sr-xr-x 1 root bin 20480 Dec 24 13:18 rlogin -r-sr-xr-x 2 root bin 184320 Feb 1 10:13 slogin Working in /usr/sbin ... -r-sr-xr-x 1 root bin 12288 Dec 24 13:19 sliplogin Working in /sbin ... -r-xr-xr-x 1 root bin 20480 Dec 24 13:18 nologin Working in /bin ... Working in /usr/local/sbin ... Working in /usr/local/bin ... case... in ... esac
語法:
case <變數> in pattern 1 ) 執行指令 ;; pattern 2 ) 執行指令 ;; esac
case 就像是在 C 語言內的 select 一樣,其流程就是在 [expression] 中與各 [pattern] 作配對,如果找尋得到便會執行 [pattern] 內的語句;使用 case 的最常見例子就是 RedHat 等的 init script,當輸入 '/etc/rc.d/init.d/network restart' 時,程式會因應 'start'、'stop'、'restart' 等字眼而執行不同的語句。
在例子八中就是一個使用 case 來控制網絡介面卡的 shell script。
例子八 (test-8.sh):
#!/bin/bash IP=202.181.234.40 Eth=eth0 case "$1" in up ) ifconfig $Eth up ;; down ) ifconfig $Eth down ;; test ) ping $IP ;; * ) echo "Usage { up | down | test }" echo esac exit 0 while 與 until while 語法: while <條件> do <語句> done
在某些工作上,您會需要使用一個不斷的迴路以檢查變數的變化。如果您有使用別的程式語言的經驗,相信 while 對您來說絕不陌生;請參考例子九,它使用到了新的功能,就是 getopts,getopts 與 C 程式中的 getopts 有相同功能,用以取得程式的參數。
例子九 (test-9.sh):
#!/bin/bash while getopts "a:b:c" opt; do case $opt in a) echo -n "Argv is -a ! " ;; b) echo -n "Argv is -b ! " ;; c) echo -n "Argv is -c ! " ;; *) echo "Usage : $0 -[abc] text" exit 1;; esac done echo $2
例子九執行方法:
shell@www :~# ./test-9.sh -a "The sting here" rgv is -a ! The string here
當然,例子九並不是一個很好的程式,只是想介紹 while 怎樣與 getopts 一併使用。while 與 getopts 一併使用的情況並不罕見。
until 語法: until <條件>; do <語句> done
until 與 while 的用法很相似,所以筆者亦不加以介紹,但是一般 until 會用在如檢查指令執行的狀態上。
Function
在文章開端時,筆者提及到在 bash 內有 function 功能,也就是子程序,在一般程式寫作時很常到用,但是在撰寫 shell script 時會較少;使用子程序有很多好處,例如在程式比較複雜時,或是某些語句需要重複,這時候子程序是有必要的。
語法:
function name { <語句> } 或 是 name() { <語句> }
'name' 就是子程序的名稱,而執行的方法是直接呼叫 'name',任何輸入至子程序的參數可以加在 'name' 的後面,如:
#!/bin/bash function test { echo "test..." } test
或者
#!/bin/bash function test { echo "Argument is $1" } test abc
結語
其實在 bash 的程式設計中,還有很多的功能筆者並未介紹,希望下次有機會時再為大家介紹;如果您希望學得更多有關 shell script 的程式設計時,您可以參考 O'Reilly 出版的Learning the Bash shell 或是參考系統上原有的 shell script 和 manpage,當中您會學到其它功能的使用。
文: Shell Hung shellhung@linux.org.hk