shell script - BASH

燕鸿波
2023-12-01

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

 类似资料: