UNIX Shell Programming
(2007-01-12 13:32)
分类: B_shell
Chap 0 -- 前言 ( UNIX Shell Programming not for guru ...... )
(0.1) 讀者需要那些基礎 ?
這篇文章的主題是 UNIX Shell Programming . 閱讀這篇文章須要一點
基礎 , 包括 : 對 UNIX 的命令要有一點認識 , 尤其是常用 , 重要的
命令 . 此外 , 你也必需會在 UNIX 上使用 editor , 像 vi , joe 或
emacs 等 ...... 當然 , 假如你有寫程式的經驗 , 那麼 UNIX Shell
Programming 對你來說不過是反掌折枝罷了 .
(0.2) 使用那一種 Shell ? Why ?
既然 Shell 有很多種 , 我們所要介紹是最早由 AT&T Bell Labs 的
Stephen Bourne 所寫的標準 Shell , 也就是所謂的 Bourne Shell .
在一些情況下 , 也會順便提一下 C Shell 相關的東西 .
既然現在已經有了更新更好的 Shell , 那我們為什麼還要介紹 Bourne
Shell 呢? 這是因為 , 在 UNIX Shell Programming 中 , 有許多都
還是用 Bourne Shell 的語法寫成的 , 尤其像 /etc/rc.X ( SunOS )
而一般使用者或許用 C Shell 的增強版 tcsh 或 bash ( Bourne Again
Shell ) 這些比較新的 Shell . 不過沒有關係 , 只要系統中有 sh
( Bourne Shell ) 這個直譯器存在 , 那麼 , 我們都可以用 Bourne
Shell 的語法來寫程式 . ( 沒有 sh 的系統 , 是很難想像的 ...... )
還有一點 , 用 sh 來寫程式實際上比用 csh 要好 , 網路上已經有
討論過了 , 很多書也都提到用 csh 寫程式的缺點 , 並建議不要用
csh 來寫程式 . 我個人沒有什麼意見 , sh 與 csh 就好比程式語言
中的 Pascal 與 C (只是比喻) , 想必學過這兩種語言的人都知道 ,
只要學會了其中一種 , 另一種也不算太難學 , sh 與 csh 也是一樣 .
況且寫程式能真正作事就可以 , 不需要一下子用 sh , 一下子用 csh
來寫程式 .
*************************************************************
注意 !!!! 所有的例子都要在 Bourne Shell 系列的環境下操作
建議先看看 2.3 , 才不會搞了半天 , 才發覺原來你是用 C Shell
系列的 Shell (Shell 的種類及系列可參考 2.3)
此外 , 文中的一些例子都是在 Linux 下操作 , 在 SunOS 或 HP
上 , 命令也許有少許的不同 . 當你在演練時 , 假如不能正確的
執行 , 你應該看看指令的 manual . 像 SunOS 的 find 與 Linux
的 find 就有差異 . 大部份的例子都可以在各種不同的平臺上執行 .
*************************************************************
(0.3) 本篇文章的架構
本來想用 html 來寫的 , 這樣似乎比較好 , 但是覺得有點麻煩 , 所
以還是用平常的格式來寫 , 各位假如用 joe , 那麼當看到 (參考 x.y)
時 , 你還是可以用 ctrl-k f 來尋找 x.y 的標題 ; 在 vi 下也可以用
/x.y 來尋找 , 這樣也許也可以達到 hypertext 的效果吧 ...... :)
Chap 1 -- 簡介
Chap 2 -- 基本知識
Chap 3 -- Shell Programming 中的資料表示
Chap 4 -- Shell Programming 中的語法
Chap 5 -- 讀取資料及程式偵錯
附錄一 -- Shell Summary 及 一些 Shell Script 的例子
附錄二 -- grep 簡介及 regular expressions
參考資料 -- 介紹一些相關的書及資料
(0.4) 假如你已經對 Bourne Shell Programming 很熟 ......
那也許附錄一 Shell Summary 會對你有複習及快速參考的好處 !
還有 , 參考資料中的書目 , 都是值得一看的書 , 假如你還沒看過 ,
那我想你可以參考看看 . 當然 , man 及 grep 的配合 , 也常常
可以助你一臂之力 .
Chap 1 -- 簡介
無疑的 , UNIX 是一個功能強大的作業系統 , 它多人多工特性 , 網路
的支援 , 多采多姿的 X Windows System , 都相當的吸引人 . 除此之外
UNIX 還有一項重要的特性 , 就是它提供了數以百計的命令 .
每個人都知道 , DOS 下面也不過二三十個命令 , 這二三十個命令可能
一兩天就可以學的很好了 ; 但 UNIX 作業系統則不然 , 數百個命令就算
都學會了 , 若我們不知道如何去 "組合這些命令" , 那我們將失去學習
UNIX 的最大樂趣 !!
有人會覺得疑惑 : "組合命令"會有趣嗎 ? 什麼叫作 "組合命令" ?
這就是我們所要學習的 . 現在就舉一個最簡單的例子而言 : 請計算目前
工作目錄下共有多少個普通檔案 ( Regular File ) ?
當你遇到這樣的問題 , 你要如何解決 ? 有人會說 : " 我用 ls -l 列
出目前工作目錄下所有的檔案 , 然後慢慢的數 ...... " , 假如你用了
這樣的方法 , 而目前工作目錄下又有數百個檔案 , 那麼 , 我真的很敬
佩你 "愚公移山" 的精神 . 還有些人比較 "暴力" , 他說 : " 這個嘛
很簡單 , 我寫一個 C 語言程式 , 用 opendir , lstat 等函數來計算
某一目錄下有多少個普通檔案 ...... " 假如你要這樣做 , 那我當然不
反對 , 常練習寫程式也是不錯的 . 然而 , 就為了這麼小小的一個問題
而寫一段 C 語言的程式 , 未免太小題大作 . 實際上 , 解答的關鍵就
在 : " 你是不是會從數百個 UNIX 的命令中挑出適合的命令 , 然後把
這些命令組合起來 , 以達到我們所要的目的 !! " . " UNIX 的每個命令
就好比各種不同的 IC , 每種 IC 都有它特殊的作用 , 藉由不同 IC 的
組合 . 我們可以達到我們所要的功能 " , " 要達到目的的方法 , 常常
不是只有一種 ......" 上面這幾句話相當重要 , 各位現在也許還不清
楚 . 不過上面幾句話所提的 , 的確就是 UNIX 頗為吸引人的地方之一 .
除了命令之外 , Shell 提供了一些類似一般程式語言的語法 , 我們可
以利用這些語法來寫程式 . 而這點也正是本篇文章的最主要內容 .
Chap 2 -- 基本知識
(2.1) 什麼是 Shell ?
我們可以把 UNIX 切成兩方面來看 , 一部份是系統的核心 (kernel) ,
另一部份就是系統中的應用程式 . 舉凡我們常用的一些命令 , 像
ls , find 與 Shell 一樣 , 都可被歸類為應用程式 . 所以 , Shell
的地位與其它的應用程式並沒有什麼差別 .
我們可以看看簽入 UNIX 作業系統的過程 : 當我們輸入 username 之
後 , getty 就結束 , 取而代之的是 login . 在我們輸入正確的
password 後 , 系統就依照 /etc/passwd 中的記錄 , 為使用者啟動
相對應的 Shell . 在這個時候 , 使用者就會得到一個提示符號 .
接著我們就可以下達一些命令 .
-- getty --> login --> shell --> logout --
^ |
|------------------------------------------
Shell 有另一個名稱叫做 "命令列直譯器" (command interpreter)
從這個名稱當中 , 我們就可以了解到 Shell 實際上為命令列的翻譯官
當我們在提示符號後輸入一長串的命令 , 並按下 Enter 之後 , Shell
就將我們所輸入的東西 , 做一個適當的分析及處理 .
(2.2) Shell 負責的事
對系統做交談性的使用 . 包含了輸出輸入導向 ( 參考 2.5 ) ,
管線 ( 參考 2.6 ) Wildcard 的展開及匹配 ( 參考 2.4 ) , 另外
還有 Job Control( 從 SVR4 開始) , 變數環境設定?. 解譯命令 ....
還有一點很重要 , Shell 有它自己內建的程式語言 , 利用它的語法
我們可以來寫程式 , 寫出來程式功能並不像一般的程式語言 ( 如 C
語言 ) 那麼多 , 然而 , 在很多情況下就夠用了 . 這部份也就是整
篇文章的重點 .
(2.3) UNIX 系統上 Shell 的種類
在 DOS 中 , 想必各位一定都知道它的 Shell 為 command.com . 特別
要說明的是 : " Shell 是可以被置換掉的 ! " 所以 , 許多人在 DOS
中 , 常常把他們的 Shell 由 command.com 換成 4dos.com , 以得到
更多的功能 , 這已經是很平常的事了 . 在 UNIX 系統中也是一樣 ,
使用 chsh 這個命令 , 你可以挑你喜歡的 Shell 來使用 . 合法的
Shell 清單被列在 /etc/shells 這個檔案中 .
Shell 的種類可以分為兩個支派 , 一為由 Bourne Shell 衍生而來的
包括了 sh (Bourne Shell) , ksh (Korn Shell) , bash (Bourne
Again Shell) , zsh (Z Shell) ; 另一支派為 C Shell 衍生而來的 ,
包括了 csh , tcsh . 現在許多人作業的環境常常是在 csh 或 tcsh
之下 , 尤其是從 BSD 衍生而來 SunOS 的環境 .
你可以使用 echo $SHELL 來獲知你現在到底使用那一種 Shell . 或
者直接看 /etc/passwd 的最後一個欄位 .
(2.4) Wildcard 的展開及匹配
Wildcard 有些人把它翻譯成 "萬用字元" , 下面所列舉的
項目 , 並不盡然每一種 Shell 都支援 , 各位要自己試試看 .
(2.4.1) * : 代表任意的字串 ( 字串可以是空的 )
% ls
haha haha1 haha2 memo1 memo13 memo2 memo23
% ls haha*
haha haha1 haha2
% ls memo*
memo1 memo13 memo2 memo23
(2.4.2) ? : 代表任意一個字元
% ls
memo1 memo13 memo2 memo23
% ls memo?
memo1 memo2
% ls memo1?
memo13
(2.4.3) []: 代表符合[]中所列舉的字元
% ls
haha haha1 haha2 memo1 memo13 memo2 memo23
% ls memo[12]
memo1 memo2
[a-e]bc 會符合 abc , bbc , cbc , dbc , ebc
(2.4.4) [!]: [] 中的字元 , 若加了 ! , 則表示不符合[]中所列舉的字元
% ls
haha haha1 haha2 memo1 memo13 memo2 memo23
% ls memo[!1]
memo2
[!a-z]bc 會符合不是以小寫英文字母開頭 , 後面接著 bc
的字串 . 如會符合 : Cbc , 6bc 等 ......
(2.4.5) {word1,word2....} : 如 my_{dog,cat,pig} 會符合 my_dog
my_cat , my_pig
(2.4.6) \ : 接在 \ 之後的特殊字元 , 其特殊的意義會被取消
例如 , 有一個檔案 , 它的名字真的叫 *abc , 那我們得用
cat \*abc 才能把 *abc 中的內容顯示出來 .
(2.4.7) 更多關於 Wildcard 的討論 :
(I) glob : 這種 Wildcard 匹配的動作 , 我們稱為 " globbing "
在很多種 Shell 之中都有一個叫作 noglob 的變數 , 可以把
Wildcard 展開匹配的動作關掉 .
% ls *bc
abc bbc cbc dbc
% set noglob <-- C Shell 中設定變數的方法
( 設定變數參考 2.7)
% ls *bc
ls: *bc: No such file or directory
(II) * 不會包含 . 開頭的檔案 , 要符合以 . 為開頭的檔案 , 你
可以試試 .??* 或 .[!\.]* ( For Newer Bourne Shell )
.[^.]* ( For tcsh )
(2.5) 輸出輸入導向
(2.5.1) Standard Input , Standard Output and Standard Error
在某方面來說 , 把資料寫到一個檔案中與寫到終端機並沒有什麼差別
相同的 , 把資料從檔案中讀出與從鍵盤讀出亦沒有什麼差別 .
在 UNIX 系統中 , 標準輸出輸入 ( Standard I/O ) 提供了一些預設
的輸出輸入設備 , 這些就是 stdin , stdout , 以及 stderr .
stdin : 一般說來 , 就是來自你鍵盤的輸入 , 但許多情況下 , 程式
常忽略了 stdin , 而直接在命令列的參數中拿 . 如 :
cat haha . haha 就可以當做 stdin .
stdout : 正常訊息輸出 , 預設是終端機 .
stderr : 錯誤訊息輸出 , 與 stdout 一樣 , 預設是終端機 .
stdin 的檔案描述值 ( file descriptor number ) 是 0 ,
stdout 是 1 , stderr 是 2 .
(2.5.2) 輸出輸入導向
舉一個最簡單的例子 , 我們用 ls -la 可列出目前目錄下所有的
內容 . 然而 , 若我們想要把終端機上的輸出放到一個檔案中 ,
那我們要如何做呢 ? 這時候 , 我們就要把原來終端機上的輸出
(也就是預設的標準輸出)導向至一個檔案 . 我們可以這樣作 :
ls -la > output_file
這個 > 的符號就是導向符號 .
下表是一般經常看到的輸出輸入導向 , prog 就是 program 的意思
而 file 就是一個檔案:
功能 sh,ksh,bash csh,tcsh
--------------------------------------------------------------
把標準輸出導到一個檔案 prog > file prog > file
把標準輸出導到檔案描述值 n prog >&n
把原來輸出至檔案描述值 m prog m>&n
的輸出與輸出至檔案描述值 n
的輸出 , 一起導向至檔案描
述值 n
把標準錯誤輸出導到一個檔案 prog 2> file
把標準輸出關閉 prog >&-
把標準輸出以及標準錯誤輸出 prog > file 2>&1 prog >& file
導到一個檔案
把標準輸出導到 f1 , 把標準 (prog > f1) 2>f2 (prog > f1) >& f2
錯誤輸出導到 f2
--------------------------------------------------------------
把標準輸出導到一個檔案的後面 prog >> file prog >> file
把標準錯誤輸出導到一個檔案的 prog 2>> file
後面
把標準輸出以及標準錯誤輸出導 prog >> file 2>&1 prog >>& file
到一個檔案的後面
--------------------------------------------------------------
把檔案當作標準輸入 prog < file prog < file
從檔案描述值 n 的地方讀取 prog <&n
當作 prog 的輸入
把標準輸入關閉 prog <&-
--------------------------------------------------------------
把 file1 當成 prog 的標準 prog < file1 > file2
輸入 , 並把標準輸出的結果
再導向至 file2
--------------------------------------------------------------
從鍵盤的輸入當作 stdin , 直 prog < file
w > who_log
把 w 這個指令的輸出導到一個叫做 who_log 的檔案
cat file1 > file2
^^^^^^^^^
把 cat file1 的輸出導到一個叫做 file2 的檔案
(II) prog 2> file
先寫一個叫做 test1.c 的 C 語言程式如下 :
#include
void main(void)
{
print("Standard Error Test"); /* 故意寫錯 */
}
接著輸入 cc test1.c <-- compile test1.c
這時我們會看到類似下面的錯誤訊息:
/tmp/cca006131.o(.text+0x1e): undefined reference to `print'
從上面的情況中 , 我們可以看到 , 這個 C 語言程式只有一個錯誤
所以得到的錯誤訊息也不多 . 然而 , 我們若 compile 一個很大的
程式 , 錯誤也許會有數百個也不一定 , 這時候 , 若我們不採用把
錯誤輸出導向到檔案的方法 , 那麼錯誤訊息在終端機上立刻就捲出
去了 , 使用者就無法看的完全 . 比較好的方法是把錯誤訊息導到
一個檔案中再慢慢分析比較好 . 所以這時候我們應該這樣做 :
cc test1.c 2> errmsg
此時 , 原本在終端機上的錯誤輸出就會被導到 errmsg 這個檔案中
還有 , prog 2> /dev/null 可以抑制錯誤訊息的輸出 , 因為它把
標準錯誤輸出導到 /dev/null 這個垃圾桶中 , 這個功能與上面的
prog >&- 有相同的地方 , 本來應該輸出的訊息都被壓抑了 . 只
不過一個是抑制錯誤訊息輸出 , 一個是抑制正常訊息輸出 . 不過
prog 2>&- 就可以達到與 prog 2> /dev/null 相同的效果 .
(III) prog > file 2>&1
我們可以試試下面的命令 :
find / -size +2000k > ~/output 2>&1
接著你可以在你的 Home Directory 下找到 output 這個檔案 , 你
可以看看它的內容 . 你就會發覺 , 有些目錄對你來說是
Permission denied 的 , 這些都是錯誤訊息的輸出 . 你也會看到
有些檔案名 , 這些檔案都是大於 2000k 的 , 屬於正常訊息的輸出
所以 , output 這個檔案的確包含著標準輸出以及標準錯誤輸出的
訊息 .
** 注意 !!! SunOS 上的 find 是以 block 為單位 , 所以 , 假
如你在 SunOS 上操作 , 那請把 +2000k 的 k 去掉 , 如下 :
find / -size +2000 > ~/output 2>&1
(IV) (prog > f1) 2>f2
再一次的 , 我們用上面的命令 , 但改為 :
(find / -size +2000k > ~/normal_out) 2>/tmp/error_out
在 (III) 中 , 我們把正常訊息的輸出與錯誤訊息的輸出都混在
~/output 這個檔案中 . 從某個角度來說 , 這不是很好的作法 ,
我們所希望的 , 是把正常訊息的輸出放在一個檔案 , 而錯誤
訊息的輸出放到另一個檔案 . 此時 , 我們就要採用 (IV) 這個
方法 . 在上面的命令中 , 我們把 find 這個命令的正常訊息輸出
導到 ~/normal_out 這個檔案 , 而錯誤訊息輸出我們就把它導到
/tmp/error_out 這個檔案中 .
(V) 至於 prog >> file , prog 2>> file , prog >> file 2>&1
與 prog > file , prog 2> file , prog > file 2>&1
只是把 > 換成 >> , 而它的差別就只在 : >> 是"附加"的意思
並不像 > 會把原來導向過去的檔案清除 (假如欲導向過去的檔案
已經存在 , 而且 noclobber 變數沒有被設定的話 !當然 , 若原來
檔案不存在 , 那就會製造出新的檔案)
所以 , 試試看下面的命令就可明白 :
ls -la /usr > file1
ls -la /etc > file1
cat file1 <-- 你看到的是 /etc 下的內容
接著 , 再試試
ls -la /usr > file2
ls -la /etc >> file2 <-- 把輸出的結果"附加"在 file2 後
cat file2 <-- 你看到的是 /usr 與 /etc 下所有的內容
(VI) prog < file
有些命令 , 它要處理的內容是要由標準輸入中拿的 . 如 mail 這
個命令 , 若我們在提示符號下鍵入 mail abc@cc.nctu.edu.tw
此時 , 我們就得由鍵盤上輸入一些資料 , 最後按 Ctrl-d 作為結
束 . 此時 , 我們剛剛輸入的資料就會經過 mail 處理寄出電子郵
件 . 然而 , 這樣作的壞處是 , 萬一你打錯了字想修改前面幾列
的內容 , 就變得困難了 . 所們所希望的 , 是信件的內容先用
普通好用的文書編輯器寫好 , 再把寫好的東西寄出去 . 此時 , <
就派上用場 , 我們就把寫好的檔案當作是從標準輸入給與的內容
導向給 prog . 所以 mail abc@cc.nctu.edu.tw < report
report 就相當於從鍵盤上輸入的內容 .
(VII) prog < file1 > file2
這個型式是由兩個導向符號所組成 , 看起來有點複雜 , 其實一點也
不 . 只要你拆成兩部份來看 : prog < file1 > file2
^^^^^^^^^^^^ (i)
^^^^^^^^^^^^^^^^^^^^ (ii)
第 (i) 部份 : 把 file1 當作是 prog 的標準輸入
第 (ii) 部份 : 把第 (i) 部份的輸出結果 , 導向至 file2 中
舉個例子 :
tr 'a-z' 'A-Z' < sour > dest
首先 , tr 'a-z' 'A-Z' < sour 會把 sour 這個檔案中的小寫字元
全部換成大寫字元 , 此時 , 假如沒有後面 > dest 的話 , 我們會在
終端機上看到轉換後的輸出 . 然而 , 我們使用 > dest 再把原來應
該在終端機上的輸出導向到 dest 這個檔案中 . 所以 , dest 就會
是 sour 經過轉換後的內容 .
(VIII) prog << c
這個型式看起來有點奇怪 , 它所作的事就是 : 從標準輸入讀取資料
一直遇到 c 為止 , 然後把這些資料都交給 prog 處理 .
這種動作有個名稱叫 "Here Document"
比如說下面的例子 :
% mail veronica << End_Of_Letter
> this is a test
> but ....
> End_Of_Letter <-- 要單獨一列
本來我們輸入 mail veronica 的話 , 那麼我們就得從鍵盤上輸入
資料 , 直到按下了 Ctrl-d 為止 . 然而 , 我們現在指定 : 一
直從標準輸入讀取資料 , 直到遇見 End_Of_Letter 為止 . 不過
上面的例子並不常見 . 比較常見的是下面這個例子 :
% cat << It_is_OK
> This is Here Document Test
> This Sample is quite typical
> You will see the same style in some Shell Script
> See You
> It_is_OK
如此作可以一次顯示一大段訊息 , 而不必很麻煩的用許多列 echo
來做 .
(2.6) 管線 (Pipes)
管線就是達成 "組合命令" 的方法 , 極端重要 ! 它的型式是這種樣子
prog1 | prog2
其中 , | 就是管線的符號 , 上面的意思是 : 把 prog1 的輸出 , 當
做 prog2 的輸入 . 你也可以想像成 : prog1 及 prog2 是兩臺機器 ,
| 是它們之間的傳送帶 , 經過 prog1 處理過的東西 , 再交給 prog2
去加工 . 舉幾個例子 :
ls -la | more <-- 把 ls -la 的輸出交給 more 去作處理 , 想
當然耳 , more 會把來自 ls -la 的輸出作
分頁處理 .
man ls | wc -l <-- 把 man ls 的輸出交給 wc 去作處理 , wc
使用選項 -l 可計算來自 man ls 的輸出
有多少列 .
當然 , 你也可以組合出兩個以上或更多的管線 :
prog1 | prog2 | prog3
如: who | cut -c1-8 | sort <-- who 的輸出交給 cut 處理 ,
cut -c1-8 會取出每列的第
一到第八個字元 , 然後再把
這些輸出交給 sort 做排序
所以 , " 熟悉每個命令要的輸入是什麼 ? 又會產生什麼輸出 ? "
是有效使用管線的不二法門 ! 各位應該一邊使用管線 , 一邊加強
命令的基礎 , 當你熟悉到了某一程度 , 就可以自己利用管線 , 輸
出輸入導向來 " 創造 " 出新的工具 , UNIX 有趣且重要的地方就
在這裡 !!!
此時 , 我們在 Chap 1 所提的 : " 計算目前工作目錄下有多少普
通的檔案 ? " 假如你對命令熟悉的話 , 你應該可以回答出來了 ,
而且方法太多種了 !
像我可以舉一種方法如下 (不計算以 . 為開頭的隱藏檔) :
ls -l | grep '^-' | wc -l
各位去分析看看 , 為什麼可以計算出來 , 並且去想想看它是如何
工作的 ......
(2.7) 一些環境變數的指定
在 Shell 中 , 有一點很重要的是 , 我們自己可以設定一些變數 ,
其中有些變數可以影響操作環境的行為 , 或者是影響應用程式 .
舉個例子來說好了 , 各位常用的 elm , 它所使用的文書編輯器
就會受到 EDITOR 這個變數的影響 . 假如 EDITOR 的內容是 joe ,
那麼 elm 就會使用 joe 拿來當文書編輯器 , 假如 EDITOR 的內
容是 vi , 則使用 vi . 既然我們知道某些變數可以影響應用程式
甚至整體的操作行為 , 那我們要如何設定這些變數呢 ? 請看下表
sh,ksh,bash csh,tcsh
-----------------------------------------------------------
指定變數 var=value set var value
指定環境整體 export var=value setenv var value
變數
-----------------------------------------------------------
上表中 , var 是變數名 , value 是變數值 ; 還有
上面兩種設定是不一樣的 , 因為 Shell 之中還可以再 invoke 一
個 Subshell , var=value 只能算是設定區域性的變數 , 只在設定
的那個 Shell 之中有效 , 對於 Subshell 就無效了 .
而 export var=value 就不一樣了 , 它對於 Subshell 也有效 .
你可以做下面的實驗 :
% color=blue <-- 把 blue 指定給變數 color ,
% echo $color <-- 顯示變數 color 的內容
blue
% sh <-- 開啟一個 subshell
% echo $color <-- 顯示變數 color 的內容
<-- 沒有變數 color 的內容
%
% export color=blue <-- 把 blue 指定給環境整體變數 color
% echo $color <-- 顯示變數 color 的內容
blue
% sh <-- 開啟一個 subshell
% echo $color <-- 顯示變數 color 的內容
blue <-- 有變數 color 的內容
前面在 Wildcards 有提過一個 noclobber 的變數 , 在屬於 Bourne
Shell 系列的 Shell 中 , 你可以使用 set -o noclobber 或者是
set +o noclobber 來使 noclobber 發生或不發生作用 . 使用 set -o
不加變數名可看到一些變數的值 ( 只能看到那些為 on 或 off ,
對環境會造成影響的變數 ) 其它的變數可用 set 直接看到 .
在 C Shell 系列中 , 直接使用 set noclobber 就可以了 .
至於有那些變數可設定 , 你可以用 man 看看你所使用的 Shell .
Chap 3 -- Shell Programming 中的資料表示
截自目前為止 , 我們都是處於在提示符號後下命令的階段 . 並沒
有 "寫程式" 的感覺 . 在這一章當中 , 我們就要開始介紹 Shell
Programming 中的資料表示方式
(3.1) 什麼是 Shell Script ?
假如你使用 UNIX 已經有一段時間 , 那你一定常常聽到這個名詞 .
既然前面提過 , Shell 本身就是一個 interpreter , 那我們當然
可以在提示符號下 "直接" 寫程式的 ! 可以看看下面的例子 :
% case 3 in
> [1-5]) echo 'haha';;
> [6-10]) echo 'lala';;
> esac
haha
你自己也可以試試看 , 不過你得使用 Bourne Shell 系列的 Shell
好了 , 我們已經看到可以在提示符號下寫程式 , 然而 , 我們並不
喜歡這樣做 . 因為在提示符號下要修改程式很麻煩 , 小的程式也就
算了 , 大的程式我想沒有人會喜歡這種方法 . 所以 , 在一般的情
況下 , 我們可以先用文書編輯器寫好一個程式並存成一個檔案 , 然
後再把這個檔案交給 Shell 去執行 , 而這個檔案就稱為 Shell
Script ! 先給各位看看一個最簡單的 Shell Script :
#!/bin/sh
ps -a | more
上面的兩列內容 , 我把它放在一個檔案中 , 然後直接執行它就可以.
但不要忘記 , 你要對這個檔案有讀以及執行的權限 .
(3.2) Shell Programming 的兩大要素 : 資料及語法
假如你學過高階語言 , 你應該可以了解到 , 其實每種高階語言的概
念都差不多 , 它們只是語法不同 , 也許資料表示的方法也有少許的
差別 . 但是 , 學會了一種再去學別種並不算難 .
Shell Programming 也是一樣 , 它還是有大家熟悉的語法如 if , do
for , while 等等的東西 . 比較特殊的是它的資料型態 .
下面就開始進入 Shell Programming 的資料表示 . 語法請看 Chap 4
(3.3) 註解及 #!/bin/sh
很多程式語言都有其註解格式 , 在一個 Shell Script 中 , 假如我
們看到一個 # 號 , 那從 # 號開始一直到那列的結尾都是註解 .
註解只是提高程式的可閱讀度 , 對 Shell 來說並不會做解譯的動作
舉一個 Shell Script 如下:
#!/bin/sh
#
# Count_bash_user_number : report how many users use bash
#
grep -c '/bin/bash' /etc/passwd # grep -c : print match
# number
這個 Shell Script 中 , 真正作事的只有一列 , 其它 # 後接的文
字都是註解 . 但特別要注意的是 : #!/bin/sh 不是註解 , 雖然它
是以 # 為開頭 , 但是它有特殊的意義 . 在最早的時候因為只有 sh
所以也並不需要 #!/bin/sh , 但是後來 Shell 的種類越來越多 ,
有些人用 csh , 有些人用 tcsh . 這時候我們就要特別的指出 , 這
個 Shell Script 是用那一種 Shell 寫的 . 假如你的 Shell Script
是以 Borune Shell 的語法寫成的 , 那你就得加上 #!/bin/sh , 假
如是以 C Shell 的語法寫成的 , 那你得加上 #!/bin/csh . 這列一
定要加嗎 ? 答案是不盡然 , 如果你目前使用的 Shell 與你寫的
Shell Script 使用相同的語法 , 那可以不加 . 但是我舉一個例子:
假如你用的是 tcsh , 但卻用 sh 的與法來寫 Shell Script , 而沒有
在 Shell Script 的第一列加上 #!/bin/sh , 那麼會發生什麼情形?
很簡單 , tcsh 會以為你寫的 Shell Script 是用 tcsh 的語法寫成
的 , 所以在解譯的過程中就會出現錯誤 . 但是若我們有加 #!/bin/sh
那麼 , tcsh 就會叫用 sh 來解譯你的 Shell Script , 自然就不會
有錯誤產生 .
我的建議是 : "無論如何 , 請明確的在 Shell Script 的第一列指出
這個 Shell Script 是用那一種 Shell 的語法寫成的 !"
(3.4) 變數
(3.4.1) 變數的指定
如同大部份的程式語言一樣 , Shell Programming Language 中也有
變數的存在 , 那我們要如何指定一個變數呢 ? 很簡單 , 如下:
variable=value
舉幾個例子 :
count=1
X11_bin_path=/usr/X11/bin
但在指定變數時 , 有幾點要注意的 : 第一 , 等號的左右兩邊不要
有空格 , 譬如說 , 不可寫成 : color = blue , color= blue
color =blue , 一定得寫成 : color=blue . 第二 , 變數沒有所謂
的 "資料型態" . 當你指定變數時 , 變數只是被當成字元的組合來
看待 . 如上面的例子 : count=1 , count 放的是單純的 1 , 而不
像 C 語言中所謂 "整數型態的 1" 第三 , 變數指定的動作實際上
也可以在提示符號後直接作 . ( 如同 2.7 所提到的 )
(3.4.2) 變數的內容 : $variable
使用 echo $variable 可以顯示變數的內容 .
像剛剛的例子 , count=1 , 那麼 , 使用 echo $count 就會得到 1
使用 echo $X11_bin_path 就會得到 /usr/X11/bin
所以 , 我們可以了解到 : $variable 是變數的內容 !
我現在給一個問題 , 各位練習看看 : 把 var1 的內容設定成 10 ,
把 var2 的內容設定成 20 , 接著把 var2 的內容設定給 var1 .
var1=10
var2=20
var1=$var2
echo $var1
再實際練習一下 , 下面的四列與上面的四列有何不同 ?
var1=10
var2=20
var1=var2
echo $var1
(3.4.3) 變數與 Wildcard
你可以在提示符號下這樣試試 :
list=*
ls $list
上面的兩列 , 首先把 * 指定給 list , 所以 $list 就是 *
那 ls $list 就會變成 ls *
five_char=?????
ls $five_char
上面兩列會把長度為五個字元的檔案列出 .
(3.4.4) ${variable}
有人試了 (3.4.3) 中的例子之後就想到 , 我們可不可以這樣做 :
file=mydoc
cp $file $filenew
把 mydoc 檔案複製一份 , 並把新檔案的名稱後加上 new 變成
mydocnew . 假如你按照上面的方法來作 , 會有錯誤產生 .
這是因為 , $file 固然是 mydoc 沒錯 , 然而 $filenew 卻是
一個內容不知道是什麼的變數 . 所以我們應該這樣做 :
file=mydoc
cp ${file} ${file}new
其中 ${file} 會換成 mydoc , ${file}new 會換成 mydocnew
(3.5) 引號
在 Shell Programming 中 , 很特殊的一點就是引號 , 引號有三種
分別為 ' ' 單引號 , " " 雙引號 , ` ` 反單引號 . 它們分別在
Shell Programming 中扮演不同但都相當重要的角色 . 我們分別敘
述如下 :
(3.5.1) 單引號
單引號最大的用處是 : 使得兩個單引號之間所夾的內容保持不變 .
比如說你想印出 : pig cat dog
那假如你這樣作 : echo pig cat dog
那麼結果仍是 pig cat dog
要改成 : echo 'pig cat dog' 才能達到目地
還有 , 在單引號中的特殊符號也會失去意義 :
% echo '* is all'
* is all
% color=blue
% echo 'Is $color beautiful?'
Is $color beautiful?
% echo '> < >> << [ ] "" & { }'
> < >> << [ ] "" & { }
從上面的例子中 , 我們都可以看到 , 像 * , ? , $variable 等
本來應該會被替換的東西 , 現在都原封不動的照印 .所以 , 單引號
通常拿來印出一段固定的訊息 .
(3.5.2) 雙引號
雙引號的用法與單引號有一些不同 . 我們提過 , 在兩個單引號之間所
夾的內容都會原封不動 . 然而 , 雙引號不同 . 在兩個雙引號之中
的內容 , 有以下幾種會被替換 :
(I) $variable , 以及任何 $ 後有意義的字元 (參考 3.7)
比較以下的不同 :
% color=blue
% echo $color
blue
% echo '$color'
$color
% echo "$color"
blue
% all_list=*
% echo $all_list <-- $all_list 換成 * 號
a.c b.c
% echo '$all_list' <-- 單引號不替換
$all_list
% echo "$all_list" <-- 雙引號中的 $all_list 換成 *
* 變成 echo "*" , 但雙引號中
的 * 號不會再做替換 .
(II) 反斜線 : \
在 (2.4.6) 之中提到 , \ 後的字元 , 其特殊意義會被取消
在上面提到 , $variable 在雙引號之中會被替換 . 假如我
們真的想印出 $ 號 , 那麼就得在 $ 號前加上 \
% number=10
% echo "your value is $number"
your value is 10
% echo "your value is \$number" <-- 取消 $ 之特殊意義
your value is $number
% echo "your value is \"$number\" "
your value is "10"
(III) 反單引號 : ` `
( 參考 3.5.3 )
(3.5.3) 反單引號
反單引號是一個往右撇的引號 . 兩個反單引號所夾的內容會先執行
舉個例子 :
% echo "Your current directory is `pwd` !"
Your current directory is /home/jhhsu
我們說過 , 雙引號之中的內容大多不做替換 , 除了 $variable ,
反斜線 , 反單引號之外 . 上面就是一個很好的例子 . 雙引號
中的 `pwd` 會先執行 , 得到的結果是 /home/jhhsu . 所以 ,
`pwd` 被 /home/jhhsu 所取代 , 全句轉變成 :
echo "Your current directory is /home/jhhsu"
自然就在終端機上印出 Your current directory is /home/jhhsu
再舉一個例子 , 前面在導向時有學過 : mail jhhsu < letter
這樣可以把 letter 這個檔案寄給 jhhsu .
但假如我們想寄信給一大群人呢 ? 實際上你可以 elm 中 alias
的方法 , 不過這有點麻煩 . 我提出一個很直覺的方法 :
mail `cat member` < letter
檔案 member 中每列都寫上收信人的 E-mail address , letter
是信件內容 . 它工作的過程很簡單 , `cat member` 先執行 ,
得到的結果可能是 jhhsu veronica ghguo , 所以全句變為 :
mail jhhsu veronica ghguo < letter
(3.6) 變數的算數運算
在變數的算數運算方面 , 我們主要靠的是 expr 這個指令 . 各位
可以先在提示符號下試試 :
% expr 30 + 20
50
% expr 30 - 20
10
% expr 30 \* 20 <-- * 是乘號 , 但要以反斜線去除特殊意義
600
% expr 30 / 20 <-- 只會取整數部份
1
從上面我們可以看到 , expr 的確可以作整數的四則運算 . 但我們提
過 , 變數是沒有資料型別的 , 所以 expr 只能處理變數內容為可視
為整數的變數 . 如 :
% var1=10
% expr $var1 + 1 <-- 把 var1 加 1
像下面就錯了 :
% var1=10
% var2=20
% expr var1 + var2 <-- var1 及 var2 不是內容 , $var1
expr: non-numeric argument $var2 才是
上面要改成 :
% var1=10
% var2=20
% expr $var1 + $var2
下面是無意義的例子 :
% var1=red
% var2=green
% expr $var1 + $var2 <-- expr 不能處理 red + green
那假如我們要把某一變數的內容加一再放回去呢 ?
如同 Pascal 中的 i:=i+1 , C 語言中的 i=i+1 那要如何作 ?
很簡單 , 如下 :
% i=`expr $i + 1`
等號右邊的反單引號會先執行 , expr 把 i 加 1 然後再把得到
的結果指定給 i
(3.7) 命令列參數的傳遞
設計一個 Shell Script , 取得命令列的引數是相當重要的 . 舉個
例子 , 你想寫一個 Shell Script 叫做 plus . plus 需要兩個參數
然後把這兩個參數相加的結果印出 . 那你應該如何做呢 ? 又有時候
我們要判斷使用者參數的個數是不是正確 , 就像上面 plus , 總得
要有兩個參數 . 這些都要靠參數的特殊表示來達成 .
(3.7.1) $n 是第 n 個參數的內容 , n=0,1,2,3,4,5,6,7,8,9
在 Shell 變數指定中 , 你不能這樣做 : 1=first , 因為數字有
其特殊意義 . 像 $1 就是第 1 個參數的內容 , $2 是第 2 個 , 餘
類推 . 那第 n 個參數的內容有用嗎 ? 就用上面 plus 來說明好了 .
先用文書編輯器寫一個 Shell Script , 並命名為 plus 如下 :
#!/bin/sh
echo `expr $1 + $2`
因為一般人的 umask 值常是 022 , 製造出來的檔案都是沒有執行權限
所以我們得用 chmod u+x plus 來使 plus 有執行的權限 .
接著你可以在提示符號下輸入 :
plus 3 5
那得到的答案就會是 8 .
再舉一個例子 : 我們要計算某個目錄下有多少個檔案 . 而我們希望這
個目錄是由使用者在命令列的參數所給與的 . 那我們可以這樣寫 :
#!/bin/sh
ls $1 | wc -l
以後使用者只要輸入這個 Shell Script 的名稱 , 後面接著某目錄名 ,
那麼就可以計算出有多少檔案 . ( 這個 Shell Script 把目錄也當成
檔案來看待 )
還有 , $0 就是你執行命令的名稱
(3.7.2) $# 代表參數個數
上面的 plus 3 5 , 其中 3 是第一個參數 , 5 是第二個參數 , 我們
把上面的 plus 這個 Shell Script 加以擴充 , 變成 :
#!/bin/sh
echo "There are $# arguments"
echo "The Answer is `expr $1 + $2`"
這個例子利用到我們所目前所學過的不少知識 , 包含 :
雙引號中 , $ 號會被替換 . 又因為我們有 3 5 兩個參數 , 所以 $#
被換成 2 . 因而印出 There are 2 arguments .
雙引號中 , 兩個反單引號之中所夾的東西會先執行 . 所以 ,
`expr $1 + $2` 換成 `expr 3 + 5` 最後得到結果 8 . 因而印出
The Answer is 8 .
講到這裡 , 假如各位看不懂上面的 Shell Script , 那可見對引號
的應用還不太熟 , 我建議重看 (3.5) , 並多加練習 .
我們現在再寫一個 Shell Script , 負責顯示有多少個參數 , 並且
把前三個參數顯示出來 :
#!/bin/sh
echo "There are $# arguments"
echo "argument1 is $1"
echo "argument2 is $2"
echo "argument3 is $3"
拿這個 Shell Script 來試試 :
% argtest red green blue
There are 3 arguments
argument1 is red
argument2 is green
argument3 is blue
% argtest 'a b c'
There are 1 arguments
argument1 is a b c
argument2 is
argument3 is
% argtest "* <> ?"
There are 1 arguments
argument1 is * <> ?
argument2 is
argument3 is
% argtest *
There are 64 arguments
argument1 is a.c
argument2 is c.c
argument3 is e.c
% argtest `cat friend_list`
There are 3 arguments
argument1 is veronica
argument2 is jhhsu
argument3 is ghguo
(3.7.3) $* 代表所有的參數
把剛剛的 Shell Script 再加以擴充如下 :
#!/bin/sh
echo "There are $# arguments"
echo "argument1 is $1"
echo "argument2 is $2"
echo "argument3 is $3"
echo "all arguments : $*"
拿這個 Shell Script 來試試看 :
% argtest a b c
There are 3 arguments
argument1 is a
argument2 is b
argument3 is c
all arguments : a b c
% argtest 'a b c'
There are 1 arguments
argument1 is a b c
argument2 is
argument3 is
all arguments : a b c
% argtest "a b c"
There are 1 arguments
argument1 is a b c
argument2 is
argument3 is
all arguments : a b c
% argtest *
There are 7 arguments
argument1 is haha
argument2 is haha1
argument3 is haha2
all arguments : haha haha1 haha2 memo1 memo13 memo2 memo23
(3.7.4) 參數的移動 shift
前面提過 , $n 是第 n 個參數的內容 , 但我們要特別注意的是 , n
必需小於 10 , 也就是說 , 不可能有 $10 這樣的東西 . 那假如我
們參數的個數很多 , 超過了 10 個 , 而又想取得超過 10 個後的參
數 , 要如何做 ? 答案就在 shift .
shift 是一個單純的命令 , 它把 $n 的內容放到 $n-1 中 . 如 $2
放到 $1 , $3 放到 $2 ...... 但 $1 並不會放到 $0 , 而是永遠的
消失 . 看下面的 Shell Script 及執行結果 :
#!/bin/sh
echo "There are $# arguments"
echo "all argument : $*"
shift
echo "There are $# arguments"
echo "all argument : $*"
shift
echo "There are $# arguments"
echo "all argument : $*"
shift
echo "There are $# arguments"
echo "all argument : $*"
% argtest a b c d e f g h i j
There are 10 arguments
all argument : a b c d e f g h i j
There are 9 arguments
all argument : b c d e f g h i j
There are 8 arguments
all argument : c d e f g h i j
There are 7 arguments
all argument : d e f g h i j
所以從上面的例子得知 , 每 shift 一次 , 我們就可以在 $9 的地方
得到原來的第 10 個參數內容 . 如此一來 , 要取得第 10 個以後的參
數就一點也不困難了 . 還有 , $1 雖然會在 shift 後被丟掉 , 但假
如 $1 以後還要用到的話 , 我們仍然可以在 shift 前將它保留起來 :
.
.
.
temp=$1
shift
.
.
.
Chap 4 -- Shell Programming 中的語法
在 Chap 3 學完之後 , 相信各位已經可以寫一些小的程式 . 但是總
好像少了些什麼 ? 不錯 , 這些可說是每種 Programming Language
中的靈魂 : 語法 ! 語法包含了條件邏輯的判斷 , 迴圈 , 函數呼叫
等 ......有了這些 , Shell Programming Language 才算完整 .
(4.1) if 結構
if [ 測試條件 ]
then
.
.
fi
我們可以很輕易的看到 , if 與 fi 之間就是典型的 if 結構
假如測試條件成立 , 就作 then 與 fi之間所夾的事情 .
還有其它許許多多的流程控制都要用到 "測試條件" , 在繼續
介紹這些流程控制之前 , 我們得先介紹測試條件 .
(4.2) 測試條件
測試條件大體上可分為 (I) 字串測試 (II) 整數測試
(III) 檔案測試
(I) 字串測試 :
string1 = string2 # string1 等於 string2
string1 != string2 # string1 不等於 string2
string # string is not null
-n string # string is not null
-z string # string is null
前面提過 , 變數設定可能像這種樣子 : name=veronica
那假如我們要把 name 與某一字串做相等比較 , 應該這樣做 :
"$name" = veronica
假如要作不相等比較 , 應該這樣做 :
"$name" != veronica
看下面的 Shell Script :
#!/bin/sh
name=veronica
if [ "$name" = veronica ]
then
echo 'equal'
fi
if [ "$name" != veronica ]
then
echo 'not equal'
fi
執行結果 :
% strcmp
equal
分析上面的 Shell Script , 首先 , 把 veronica 指定給 name
此時 $name 就是 veronica . 執行到 if [ "$name" = veronica ]
"$name" 就會換成 veronica , 所以變成 :
if [ veronica = veronica ] <-- 字串相等比較成立
因為比較成立了 , 所以就做 then 至 fi 之中的事 , 也就是顯示
equal .
接著執行 if [ "$name" != veronica ] , 再一次的 , "$name"
被換成 veronica , 所以 : if [ veronica != veronica ]
測試條件不成立 . not equal 將不會被顯示出來 .
至於 string , -z string , -n string 則可以測試是否為空字串
看下面的 Shell Script :
#!/bin/sh
myaddr= # myaddr is null
youaddr=taipei # youraddr is not null
if [ -z "$myaddr" ] # 測試條件成立
then
echo 'my string is null'
fi
if [ -n "$myaddr" ] # 測試條件不成立
then
echo 'my string is not null'
fi
if [ -z "$youaddr" ] # 測試條件不成立
then
echo 'your string is null'
fi
if [ -n "$youaddr" ] # 測試條件成立
then
echo 'your string is not null'
fi
執行結果 :
% isstrnull
my string is null
your string is not null
(II) 整數測試
我們曾提過 , 變數並沒有所謂的資料型別 , 那為什麼會有整數
測試呢 ? 這是因為 , 整數測試中 , 變數的內容被當成整數來看
待 . 如 : money=30 , 一般情況下 , money 是放著 30 這個字串
但在整數測試中 , money 被視為整數的 30 .
整數的測試如下 :
int1 -eq int2 # int1 = int2 等於
int1 -ge int2 # int1 >= int2 大於等於
int1 -gt int2 # int1 > int2 大於
int1 -le int2 # int1 <= int2 小於等於
int1 -lt int2 # int1 < int2 小於
int1 -ne int2 # int1 != int2 不等於
我們舉一個例子 :
#!/bin/sh
if [ "$#" -eq 2 ] # 假如參數個數等於 2
then
echo 'The number of arguments is two'
fi
執行結果 :
% digi
% digi a b
The number of arguments is two
% digi a
% digi "abc" 'def'
The number of arguments is two
整數的比較常用在整個 Shell Script 的開頭 , 尤其是那些須要
參數的 Shell Script . 就以前 plus 這個 Shell Script 而言
plus 需要兩個參數 , 那我們便可以在 Shell Script 的一開頭
就檢查使用者所給的參數個數是不是兩個 , 假如是 , 才做兩個
參數相加的動作 , 若參數個數不等於兩個 , 就顯示訊息給使用
者 , 並且脫離 , 不繼續做 , 整個 Shell Script 如下 :
#!/bin/sh
if [ "$#" -ne 2 ] # 假如參數個數不等於 2
then
echo 'plus needs two arguments'
exit # 脫離整個 Shell Script
fi
echo `expr $1 + $2` # 顯示參數 1 加參數 2
# 的值
執行結果 :
% plus2
plus needs two arguments
% plus2 3 5
8
(III) 檔案測試
我們在 Shell Script 中也可以做檔案測試 :
-d file # 檔案是否為一個目錄
-f file # 檔案是否為一個普通檔案
-r file # 檔案是否為可讀
-s file # 檔案長度是否不是 0
-w file # 檔案是否為可寫入
-x file # 檔案是否為可執行
若我們要測試 /etc/passwd 是否存在 , 並且是普通檔案的話 :
if [ -f /etc/passwd ] # 因為 /etc/passwd 存在 , 而且
# 它是一個普通檔案 , 所以測試
# 會成立
if [ -x /etc/passwd ] # 因為 /etc/passwd 不是可執行
# 檔 , 所以測試不會成立
(4.3) 命令測試
實際上 , 命令也可以當作測試的條件 , 這是因為 , 我們每執行
完一個命令 , 總會有傳回值 , 這個傳回值假如是 0 , 則代表
命令執行成功 , 假如為非 0 的值 , 則代表命令執行失敗 .
看看下面的 Shell Script :
#!/bin/sh
if w | grep "^jhhsu" >&-
then
echo $?
fi
if w | grep "^veronica" > /dev/null
then
echo 'veronica is online'
fi
這個 Shell Script 值得好好研究 , 首先 , $? 代表上個命令的
傳回值 . 假如在這個系統內 jhhsu 正在線上 , 而 veronica 並
不在線上 . 那兩次 echo $? 的結果將會是 0 與 1 , 分別代表
成功與不成功 . 還有 , 假如 if 所測試的是命令 , 那並不需要
[ ] 號 . 前面為了方便 , 以及看起來有結構化 , 我把 if 的結構
寫成 :
if [ 測試條件 ]
then
.
.
fi
但實際上 , 原來的結構應該是這種樣子 :
if test_command
then
.
.
fi
也就是說 , if 後接的必須是一個測試命令 , 而不是 [ 測試條件 ]
所以 , 我們得用 test 這個命令來執行我們的測試條件 . 就像下面
if test "$1" -gt 10
但是 , test 測試條件 也可直接換成 [ 測試條件 ] , 所以我就寫
成 if [ 測試條件 ]
下面左右兩邊的 Shell Script 是完全相等的 :
|
#!/bin/sh | #!/bin/sh
|
if [ "$#" -ne 2 ] | if test "$#" -ne 2
then | then
echo 'needs two arguments' | echo 'needs two arguments'
exit | exit
fi | fi
|
echo `expr $1 + $2` | echo `expr $1 + $2`
|
>&- 與 /dev/null 可參考 (2.5.2) , grep 可參考附錄二
(4.4) 組合測試條件的邏輯運算子
各位在看完 (4.3) 後 , 若你原本對管線 , 導向等等的課題有一定
的認識 , 那你已經可以寫出頗為有用的 Shell Script 了 .
在 (4.4) , 我們還要介紹三種邏輯運算子 not , and 與 or
(I) ! 代表反面的邏輯 : not
也許你已經發現了 , 在 (4.2) 中 , 不管是字串的測試或者是整數
的測試 , 都有反面的邏輯存在 :
string1 = string2 # 字串1 與字串2 相等
string1 != string2 # 字串1 與字串2 不相等
int1 -gt int2 # 整數1 大於 整數2
int1 -le int2 # 整數1 小於等於 整數2
但在檔案的測試方面 , 卻沒有反面的邏輯 . 此時 , 我們就要靠
! 來達成反面的測試 , 如 :
if [ -f /etc/passwd ] # /etc/passwd 是普通檔案嗎 ?
if [ ! -f /etc/passwd ] # /etc/passwd 不是普通檔案嗎 ?
當然 , 在字串或整數比較方面 , 你也可以不用原來就已經有的運
算子 , 而改用 ! 以代表反面的邏輯 . 請看下面的 Shell Script
#!/bin/sh
if [ ! "$1" -gt 10 ] # 假如第一個參數的值不大於 10
then
echo 'argument is less than or equal to 10'
fi
執行結果 :
% unary 3
argument is less than or equal to 10
% unary 11
(II) -a 代表邏輯上的 and
講到目前為止 , 我們的比較都僅限於一個表示式 , 像某一變數的
內容是否等於一個字串 , 或是某一變數的內容是否大於一個整數
那假如我們想做如此的測試 : " 一個數字是否大於 10 而且小於
100 , 也就是介於 10 到 100 之間 " 那我們就得用 -a 來達成 .
請看下面的 Shell Script :
#!/bin/sh
if [ "$1" -gt 10 -a "$1" -lt 100 ] # 10 < 參數一 < 100 ?
then
echo '10 < argument < 100'
fi
我們可以把上面 [ ] 中的測試條件看成 :
第一個參數大於10 且 第一個參數小於100
有些人會想 : 我是不是應該用括號來確保運算優先權的順序 , 其實
可以不必 , 因為 -a 比 -gt 或 -lt 的優先權低 , 也就是說 ,
"$1" -gt 10 與 "$1" -lt 100 會分別先運算完後才做 and 的動作 .
假如你有"優良"的寫程式習慣 , 那你仍然可以加上括號 , 只是 ,
要以反斜線來去除括號的特殊意義 , 如下 :
if [ \( "$1" -gt 10 \) -a \( "$1" -lt 100 \) ]
(III) -o 代表邏輯上的 or
與 and 不同的 , or 只要有一項成立就可以了 . 比如說 :
if [ "$1" -le 10 -o "$1" -ge 90 ]
表示測試第一個參數是否符合小於等於10 或 符合大於 90 其中之一
的敘述 . 只要符合其中任一句敘述 , 條件就成立 .
在比較複雜的邏輯中 , or 與 and 有同時存在的可能 . 此時 , 到底
誰的優先權比較高呢 ? 答案是 and , 舉個例子如下 :
if [ "$1" -le 10 -o "$1" -gt 50 -a "$1" -lt 100 ]
^^^^^^^^^^^^^^^^^^^^^^^^^^^
這部份先做
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
再做這部份
假如你想改變 and 與 or 之間的優先權 , 請使用括號 .
(4.5) 更多的 if 流程控制
當我們學完測試條件之後 , 流程控制就很簡單了 , 在 (4.1)
中 , 我們已經看過了 if 的結構 , 現在我們來看看其它的結構
所有的 if 結構都以 fi 做結尾 , 各位可以把 if 至 fi 看成
一個區塊 .
(I) if test_command
then
.
.
fi
這個結構 (4.1) 已提過 , 不再贅述
(II) if test_command
then
.
.
else
.
.
fi
這個結構很簡單 , 假如 test_command 成立 , 那就做 then
到 else 之間的事 ; 假如 test_command 不成立 , 那就做
else 到 fi 之間的事 . 下面是一個簡單的例子 .
#!/bin/sh
if [ "$1" -ge 0 ]
then
echo 'positive'
else
echo 'negative'
fi
執行結果 :
% mytest 10 # 10 大於 0
positive
% mytest -5 # -5 小於 0
negative
(III) if test_command1
then
.
.
elif test_command2
then
.
.
.
elif test_commandn
then
.
.
else
.
.
fi
上面的結構也很簡單 , 假如 test_command1 成立 , 就做
then 與 elif test_command2 之間的事 . 假如 test_command1
不成立 , 就測試 test_command2 看成不成立 , 假如不成立
就繼續測試下去 . 假如所有的測試都不成立 , 就會執行 else
與 fi 之間的事 . 下面是一個典型的 Shell Script :
#!/bin/sh
time=`date | cut -c12-13`
if [ "$time" -gt 0 -a "$time" -lt 12 ]
then
echo 'Good Morning'
elif [ "$time" -ge 12 -a "$time" -lt 17 ]
then
echo 'Good Afternoon'
else
echo 'Good Evening'
fi
首先 , date | cut -c12-13 先執行 , date 輸出目前時間 ,
但我們並不要全部的輸出 , 只要第12及第13個字元 , 也就是
目前是幾點 , 所以我們把 date 的輸出以管線交給 cut 這個
命令去剪下第12及第13個字元 , 然後把這兩個字元設給 time
接著進入條件測試 . 一開始先測試是否零點到十二點之間 ,
假如是 , 就顯示 Good Morning , 並脫離 if 結構 . 假如不
在零點到十二點之間 , 那就看看是不是在十二點到十七點之間
假如是 , 就顯示 Good Afternoon , 並脫離 if 結構 , 假如
又不是在十二點到十七點之間 , 就顯示 Good Evening
(4.6) case 結構
case value in
pattern_1) .
.
.;;
pattern_2)
.
.
.;;
...
pattern_n) .
.
.;;
esac
case 結構是以 case 為開頭 , 以 esac 為結束 . 其中 , 我們
能自由指定的 , 包含 value , pattern 以及若符合 pattern 則
作那些事 . case 的工作原理很簡單 , 它把 value 的值與下面的
pattern 做比較 , 假如遇到符合的 , 就做 pattern 之後的內容
直到遇見一個 ;; 號為止 . 各位一定要注意 , 每個 pattern 結束
都一定要有一個 ;; 號 , 否則會有錯誤 .
value 就是一個變數 , pattern 是一個符合的樣版 . 先看一個
Shell Script , 這個 Shell Script 從參數得到一個數字 , 並把
這個數字轉成相對應的英文 :
#!/bin/sh
if [ "$#" -ne 1 ]
then
echo 'Usage : transnum'
exit
fi
case "$1" in
0) echo 'zero';;
1) echo 'one';;
2) echo 'two';;
3) echo 'three';;
4) echo 'four';;
5) echo 'five';;
6) echo 'six';;
7) echo 'seven';;
8) echo 'eight';;
9) echo 'nine';;
*) echo 'must a single digit';;
esac
執行結果 :
% transnum 4
four
% transnum 18
must a single digit
% transnum veronica
must a single digit
在上面的 Shell Script 中 , value 是第一個參數 , 而 pattern
的值分別為 0,1,2,3,4,5,6,7,8,9,* . 所以 , 當執行到 case 這
個結構時 , 第一個參數就與下面一個個 pattern 做比較 , 直到
遇見符合的 pattern , 就做 pattern 後面的事 . 比較特殊的是 ,
pattern 若是 * 號 , 則表示都不符合所要做的事 . 像上面 , 我
們只接受一位數字 , 假如你給與的是兩位數字以上 , 或是字串 ;
既然都不符合 0 到 9 的 pattern , 那就會顯示一段錯誤訊息 .
還有 , pattern 可以比較複雜 , 如下面的格式都是可以的 :
[a-z] <-- 符合小寫字元
[0-9] <-- 符合 0,1,2,3,4,5,6,7,8,9
? <-- 符合任何一個字元
你也可以把兩個 pattern 以 | 做邏輯的 or , 看下面的例子
#!/bin/sh
if [ "$#" -ne 1 ]
then
echo 'need a animal name'
fi
case "$1" in
ant | cat ) echo 'I hate this';;
bird | dog ) echo 'I like this';;
esac
執行結果 :
% testani ant
I hate this
% testani bird
I like this
% testani cat
I hate this
% testani dog
I like this
(4.7) while 結構
while test_command
do
.
.
done
while 結構相當簡單 , 它可說是一種迴圈的型式 , 只要 test_command
成立 , 就會一直做 do 與 done 之間所夾的事 . 下面的 Shell Script
會顯示 0 到 100
#!/bin/sh
var=0 # 把 var 的初始值設為 0
while [ "$var" -le 100 ] # 當 $var 小於等於 100
do
echo -n "$var" # echo -n 為顯示但不換行
var=`expr $var + 1` # 把 var 的值加 1
done
假如我們想無條件作無窮迴圈 , 那你可以看看下面的 Shell Script ,
它會在終端機上從 0 開始印 , 直到你按下 Ctrl-c 為止
#!/bin/sh
var=0
while test 0 # 注意 , test_command 為 test 0
do
echo -n "$var"
var=`expr $var + 1`
done
本來在我們的想法中 , test 後應該是接一個測試條件 , 就像 :
test "$var" -lt 100
test 0 是什麼意思 ? 其實很簡單 , 測試條件假如成立 , 那就
會是 0 , 假如不成立 , 那就會是 1 . 所以 , 像 $var 若小於 100
那麼 , test "$var" -lt 100 就會被換成 test 0 .
所以 , 我們所看到的 :
while test 0 <-- 恆成立 , 所以為一無窮迴圈
do
.
.
done
(4.8) until 結構
until test_command
do
.
.
done
與 while 不同的 , until 後面接的 test_command 要不成立才會
一直做 do 與 done 之間夾的事 , 一但 test_command 成立了 ,
整個 until 結構就結束 .
舉上面的顯示 0 到 100 的例子 , 假如我們要把原來的 while 結構
改成 until 結構 , 那我們的想法必然是 : 一直加 1 , 直到大於
100 為止 . 那整個 Shell Script 如下 :
#!/bin/sh
var=0
until [ "$var" -gt 100 ] # 直到大於 100 為止
do
echo -n "$var"
var=`expr $var + 1`
done
讓我們好好比較 while 與 until :
|
#!/bin/sh | #!/bin/sh
|
var=0 | var=0
|
while [ "$var" -le 100 ] | until [ "$var" -gt 100 ]
do | do
echo -n "$var" | echo -n "$var"
var=`expr $var + 1` | var=`expr $var + 1`
done | done
|
做同樣一件事 , 但是它們的測試條件卻剛好相反 . while 是小於等於
100 , 而 until 卻是大於 100 . 從這裡 , 我們看到了 while 與
until 的互補性 .
until 結構有用的地方是在於 : 它可以用來等待某事件的發生 , 看下
面的 Shell Script :
#!/bin/sh
if [ "$#" -ne 1 ] # 假如參數個數不等於 1
then
echo 'Usage: scanuser username'
exit
fi
until who | grep "^$1" > /dev/null
do
sleep 60
done
echo "$1 has logged on"
上面是一個頗為有用的 Shell Script , 我相信各位可以看得懂前幾列
特別要解釋的是 until 的結構 . 在這個 Shell Script 中 , 我們
想做的事是 : 每 60 秒檢查一次 , 看看某 user 是不是在系統中 .
until who | grep "^$1" > /dev/null
上面這列看起來很複雜 , 其實只要你熟悉命令及導向,管線 , 那實在
沒有什麼神秘的 . 我們知道 , until test_command 是只要
test_command 不成立 , 就一直做 do 與 done 之間的事 .
who | grep "^$1" > /dev/null
把 who 的輸出交給 grep 去抓出看看有沒有第一個參數的人名 , 並
把應該正常輸出的訊息丟給 /dev/null 這個垃圾筒 .
假如沒有第一個參數的人名 , 就執行 sleep 60 ( sleep 60 是暫
停 60 秒的意思 ) , 假如有第一個參數的人名 , 就會脫離 until
結構 , 並且顯示那個人已經簽入系統 .
我們可以把這個 Shell Script 以背景執行 , 你會發覺它真的有用 .
(4.9) for 結構
for var in word1 word2 ..... wordn
do
.
.
done
請各位注意 , Shell Programming 中的 for 與一般程式語言的 for
有很大的不同 . 我們能指定的包含 var , word1 , word2 ......
( word1 word2 .... wordn 為了方便 , 我把它稱為 word list )
以及 do 與 done 之間所做的事 . 我們還是先看一個例子 :
#!/bin/sh
for i in 1 2 peter bob
do
echo "$i"
done
執行結果 :
% fortest
1
2
peter
bob
我們所看到的是 , for 每執行一次 , 就把後面的資料指定給 var
像第一次執行時 , 1 被指定給 i , 第二次執行時 , 2 被指定給 i
第三次執行時 , peter 被指定給 i , 第四次執行時 , bob 被指定
給 i . 好了 , 現在我們把焦點放在 word1 word2 ..... wordn 上
它們是不是有什麼變化呢 ? 在這裡 , 我要提出幾種 :
(I) 使用命令製作出 word list
各位還記得 (3.5.3) 中 mail 給很多人的例子嗎 ?
mail `cat member` < letter
我們知道 , `cat member` 會被換成像 jhhsu veronica peter bob
的型式 , 那不是剛好符合我們的 word list 嗎 ? 因此 , 我把
它以 for 來改寫 :
#!/bin/sh
for person in `cat member`
do
mail $person < letter
done
所以各位可以看到 , 只要命令執行結果會產生 word list , 那怕
是只有一個 word 也好 , 我們都可以把它放在 in 的後面 , 並以
反單引號括起來 . 在這部份 , 運用的巧妙就得看你對命令的熟悉
程度而定了 , 並且可以再用管線組合出更複雜的命令 .
(II) 把參數當成 word list
在這裡要介紹一個新的符號 $@ , 它與 $* 類似 , 都是代表所有
的參數 . 不同的是 , $@ 會把每個參數加上雙引號 . 所以 , 我
們可以想成 : $* 代表 $1 $2 $3 .... 而 $@ 代表 "$1" "$2" "$3"
比較下面兩個 Shell Script :
|
#!/bin/sh | #!/bin/sh
|
for arg in $* | for arg in $@
do | do
echo $arg | echo $arg
done | done
|
|
執行結果 : | 執行結果 :
|
% arg1 'a b' c | % arg2 'a b' c
a | a b
b | c
c |
下面還是以寄信做例子 , 但此時 , 收信人我們不寫在檔案中 , 而
直接由命令列中的參數指定 :
#!/bin/sh
for person in $@
do
mail $person < letter
done
Chap 5 -- 讀取資料 , 呼叫函數 , 偵錯
(5.1) read 用來讀取資料
截至目前為止 , 我們變數的來源有兩個地方 , 一是來自參數 , 一是
來自直接在 Shell Script 中直接指定 . 那變數能不能在 Shell
Script 執行時才讀取呢 ? 此時就要用 read . read 的格式為 :
read variable
它會把讀取到的資料放到 variable 中 . 舉一個小小的例子 :
#!/bin/sh
echo -n 'Input number1 --> '
read number1
echo -n 'Input number2 --> '
read number2
echo `expr $number1 + $number2`
執行結果 :
% plus3
Input number1 --> 5
Input number2 --> 10
15
上面這個 Shell Script 可以由使用者任意輸入兩個變數 , read
分別把它們放到 number1 及 number2 , 最後再把 number1 及
number2 加起來 .
我再舉一個例子 , 你可以用 read 來製做出類似選單的東西 :
#!/bin/sh
cat << End_of_menu
------------------------
| |
| You have 4 choice : |
| |
| 1. plus + |
| |
| 2. minus - |
| |
| 3. multiple * |
| |
| 4. divide / |
| |
------------------------
End_of_menu
echo -n 'Input your choice --> '
read choice
echo -n 'Input number1 -->'
read number1
echo -n 'Input number2 -->'
read number2
case $choice in
1) echo `expr $number1 + $number2`;;
2) echo `expr $number1 - $number2`;;
3) echo `expr $number1 \* $number2`;;
4) echo `expr $number1 / $number2`;;
esac
上面是一個選單式的計算機 , 它可以選擇你要作的運算 , 並讀取
兩個數字來當運算元 . 這個 Shell Script 融合了我們在 (2.5.2)
中的 cat << c 的觀念 , 特別要注意的是 , 在上面的 Shell Script
中 , End_of_menu 一定要獨立一列 , 而且一定要在那列的開頭 .
接著 , read choice 把我們要做的四則運算種類 ( 1 代表加 , 2
代表減 , 3 代表乘 , 4 代表除 ) 放在 choice 中 . 再來 , 讀入
兩個數字來當運算元 . 最後 , 進入一個 case 的結構 . 在 case
中根據 choice 的值來做不同的運算 .
(5.2) 呼叫函數
在程式寫作的型態上 , 結構性的程式的確比雜亂無章的程式要好 .
因此 , 使用函數變成了一項不可或缺的技巧 , 而且可以把整個
程式拆成數個部份分別寫作 . 在 Bourne Shell Programming
中 , 我們也可以使用函數 , 函數的格式如下 :
function_name()
{
command
command
.
.
}
我們把上面加減乘除的例子以函數的形式再寫一遍 :
#!/bin/sh
plus()
{
echo 'Plus Function'
echo `expr $number1 + $number2`
}
minus()
{
echo 'Minus Function'
echo `expr $number1 - $number2`
}
multi()
{
echo 'Multiple Function'
echo `expr $number1 \* $number2`
}
div()
{
echo 'Divide Function'
echo `expr $number1 / $number2`
}
main_select()
{
cat << ' End_of_menu'
------------------------
| |
| You have 4 choice : |
| |
| 1. plus + |
| |
| 2. minus - |
| |
| 3. multiple * |
| |
| 4. divide / |
| |
------------------------
End_of_menu
echo -n 'Input your choice --> '
read choice
echo -n 'Input number1 -->'
read number1
echo -n 'Input number2 -->'
read number2
case $choice in
1)plus;;
2)minus;;
3)multi;;
4)div;;
*)echo 'Invalid choice'
exit;;
esac
}
main_select # 程式從這裡開始
使用這種函數寫作有幾點要注意 :
(I) 下面的情形 , 左邊可以正確的執行 , 右邊卻不行
a_func() | a_func
{ |
b_func | a_func()
} | {
| b_func
b_func() | }
{ |
| b_func()
} | {
|
a_func | }
(II) 命令列下參數的傳遞必須先把這些參數指定給另一個變數
然後才能正確的工作 . 看下面的例子 :
#!/bin/sh
main()
{
echo $1 # error ?
echo $2 # error ?
}
main
上面的例子看起來沒錯 , 但實際上並不能 work , 你可能要
改成下面的樣子 :
#!/bin/sh
main()
{
echo $number1
echo $number2
}
number1=$1
number2=$2
main
(5.3) Shell Script 的偵錯
使用 sh -x shell_script_name 可以對一個 Shell Script 做
偵錯的動作 . 假如你的 Shell Script 不能正常的執行 , 或者是
執行的結果不符合你所預料的 . 出錯的地方可能為下列幾項之一 :
(I) 請檢查 Shell Script 是否有可讀及可執行權限 .
(II) 請注意 , Shell Programming Language 不是完全自由語法
就像 if [ "$#" -ne 1 ] 就不能寫成 if ["$#" -ne 1]
也不能寫成 if[ "$#" -ne 1 ] , 這些要特別注意 . 假如
執行時有錯誤訊息 , 你要詳加檢查程式的語法結構 . 該
空格的地方要有空格 .
(III) 要了解 , $variable 才是變數的內容 , 你很有可能把
variable 當成變數的內容 .
(IV) 還有 , 你可能拼錯字了 , 這是很常見的 .
(V) 有些情況下 , 變數中的內容並不是你預期的 , 以致造成
錯誤 . 此時你可以在程式中可疑的地方適時的加上一些
偵錯點 , 把這些變數內容顯示出來看看 .
(V) 最後 , 邏輯上的錯誤是很難找到的 , 你可能也要注意 .
附錄一 : Shell Summary
-------------------------------------------------------------------
## 變數的指定 :
variable=value
此時 , variable 是被當成字串看待 .
如 : NNTPSERVER=news.csie.nctu.edu.tw
其中 , NNTPSERVER 是 variable ; news.csie.nctu.edu.tw 是 value
## 變數的內容 :
$variable
## 對 shell script 作偵錯執行的動作 :
sh -x [your_shell_script]
如 : sh -x search_string
其中 , search_string 這個 shell script
必需有 read 及 execute 的權限
## shell script 中的特殊符號 :
$# 參數個數
$n 第 n 個參數
$* 所有的參數
$@ 與 $* 一樣 , 除了每個參數都加上 " "
$? 上一個命令傳回的值
$$ 目前此 shell 的 pid // 當你在 script 中要 creat 一個檔 , 但不希望
// 這個檔案名是固定時 ( 尤其是有兩個人同時使用
// 這個 shell script ) 總不能 creat 的檔名取成
// 一樣 , 這時候 , $$ 就可派上用場
## 對整數作比較的運算子 :
int1 -eq int2 // int1 = int2
int1 -ge int2 // int1 >= int2
int1 -gt int2 // int1 > int2
int1 -le int2 // int1 <= int2
int1 -lt int2 // int1 < int2
int1 -ne int2 // int1 != int2
## 對字串作比較的運算子 :
string1 =string2
string1 !=string2
string // string is not null
-n string // string is not null
-z string // string is null
## 對檔案作測試的運算子 :
-d file // if file is a directory
-f file // if file is a ordinary file
-r file // if file is readonly
-s file // if file has nonzore length
-w file // if file is writeable
-x file // if file is excuteable
## 邏輯運算子 :
! // negation
-a // logic AND
-o // logic OR
## 讀取使用者輸入的資料 :
read variable
## 迴圈 , 反覆執行 :
(1) while :
while test_command
do
command
command
.
.
done
(2) until :
until test_command
do
command
command
.
.
done
(3) for :
for var in word1 word2 ..... wordn
do
command
command
.
.
done
## 選擇性條件 :
(1) case :
case value in
pattern_1) command
command
.
.
command;;
pattern_2) command
.
.
command;;
...
pattern_n) command
.
.
command;;
esac
(2) if :
型式一 :
if test_command
then
command
command
.
.
fi
型式二 :
if test_command
then
command
command
.
.
else
command
command
fi
型式三 :
if test_command1
then
command
.
.
elif test_command2
then
command
.
.
elif test_command3
then
command
.
.
else
command
.
.
fi
## special two command logic :
cmd1 && cmd2 // cmd2 is excuted when cmd1 is true (return 0)
cmd1 || cmd2 // cmd2 is excuted when cmd1 is false
Example :
---------------------- 第一個參數與字串比較 -----------------
#!/bin/sh
echo $1
if [ "$1" = 'y' ]
then
echo 'Yeah! You are right!'
fi
if [ "$1" = 'n' ]
then
echo 'Ooop! You are wrong!'
fi
執行結果 :
%scom y
y
Yeah! You are right!
%scom n
n
Ooop! You are wrong!
---------------------- Del_From_Inode -----------------------
#!/bin/sh
if [ "$#" -ne 1 ]
then
echo 'Usage : del_from_inode'
exit 1
fi
find . -inum "$1" -ok rm '{}' \;
---------------------- 字串比較的例子 -----------------------
#!/bin/sh
if [ "$#" -lt 1 ]
then
echo 'again!'
exit 1
fi
if [ "$1" = 'lala' ]
then
echo 'lala is haha'
else
echo 'unknown!!!'
fi
執行結果 :
%para3
again!
%para3 lala
lala is haha
%para3 dada
unknown!!!
--------------- 計算某一目錄底下有多少檔案或目錄 ------------
#!/bin/sh
if [ "$#" -ne 2 ]
then
echo 'Usage : count_report '
exit
fi
case "$2" in
f)
file=0
for f in `find $1 -depth 1 -type f`
do
file=`expr ${file} + 1`
done
echo "Total files is ${file}"
;;
d)
directory=0
for f in `find $1 -depth 1 -type d`
do
directory=`expr ${directory} + 1`
done
directory=`expr ${directory} - 1`
echo "Total directories is ${directory}"
;;
esac
---------- 一個比較長的例子 ( 可執行但功能並不完整 :p ) -----------
#!/bin/sh
COMPILER=cc
pause_until_enter()
{
echo
echo 'Press enter to continue ......'
read enter_key
}
exit_shell_script()
{
sleep 1
clear
exit
}
error_handle()
{
if [ $? -ne 0 ]
then
echo "***** Some error occur , Please check ~/$$.err *****"
echo -n "Display ~/$$.err now ? (y/n) --> "
read error_select
if [ $error_select = 'y' ]
then
cat ~/$$.err | more
pause_until_enter
case $main_select in
1) modify_now;;
esac
fi
fi
}
modify_now()
{
echo -n 'Modify your source code now ? (y/n) --> '
read modify_now
if [ $modify_now = 'y' ]
then
$EDITOR $source_code_name
fi
}
read_source_code_name()
{
echo -n 'Enter source codes name --> '
read source_code_name
}
read_out_exe_file_name()
{
echo -n 'Input executable filename , Press enter for a.out --> '
read out_exe_file_name
if [ "$out_exe_file_name" = '' ]
then
echo 'Use default name : a.out'
fi
}
read_library_name()
{
echo -n 'Enter your library name --> '
read library_name
}
read_obj_name()
{
echo -n 'Enter your obj name --> '
read obj_name
}
build_exe()
{
read_source_code_name
read_out_exe_file_name
echo -n 'Link library? (y/n) --> '
read link_lib_choice
if [ "$link_lib_choice" = 'y' ]
then
echo -n 'Enter library name --> '
read lib_name
else
lib_name=
fi
echo -n 'Creat symbol table for debugger? (y/n) --> '
read debug_choice
if [ "$debug_choice" = 'y' ]
then
$COMPILER -g -o ${outfile}.out $source_code_name $lib_name > ~/$$.err 2>&1
error_handle
else
$COMPILER -o ${outfile}.out $source_code_name $lib_name > ~/$$.err 2>&1
error_handle
fi
}
compile_only()
{
read_source_code_name
$COMPILER -c $source_code_name
}
list_lib_content()
{
read_library_name
ar t $library_name | more
pause_until_enter
}
add_obj_into_lib()
{
read_library_name
read_obj_name
if [ -f $library_name ]
then
ar q $library_name $obj_name
fi
}
read_config()
{
sed -n 1,1p comenv.conf
pause_until_enter
}
main_menu()
{
clear
cat << ' main_menu_end'
Main Menu
------------------------------
1. Compile Source Code
2. Library Maintain
3. Execute File
4. Configuration
5. Exit
main_menu_end
echo -n 'Enter your choice --> '
read main_select
case $main_select in
0);;
1) compile_menu;;
2) maintain_menu;;
3) execute_menu;;
4) config_menu;;
5) exit_shell_script;;
*) main_menu;;
esac
}
compile_menu()
{
clear
cat << ' compile_menu_end'
Compile Menu
------------------------------
1. Build Excutable File
2. Compile Only
3. Back To Main Menu
4. Exit
compile_menu_end
echo -n 'Enter your choice --> '
read compile_select
case $compile_select in
1) build_exe;;
2) compile_only;;
3) main_menu;;
4) exit_shell_script;;
*) compile_menu;;
esac
}
maintain_menu()
{
clear
cat << ' maintain_menu_end'
Library Maintain
------------------------------
1. List library content
2. Add objfile into library
3. Remove objfile from library
4. Back to Main Menu
5. Exit
maintain_menu_end
echo -n 'Enter your choice --> '
read maintain_select
case $maintain_select in
1) list_lib_content;;
2) add_obj_into_lib;;
3) remove_obj_from_lib;;
4) main_menu;;
5) exit_shell_script;;
esac
}
execute_menu()
{
echo -n 'Enter execute file name --> '
read execute_file_name
$execute_file_name
pause_until_enter
}
config_menu()
{
clear
cat << ' config_menu_end'
Configuration Menu
------------------------------
1.View Configuration File
2.Change Configuration
config_menu_end
echo -n 'Enter your choice --> '
read config_select
case $config_select in
1) read_config;;
2);;
esac
}
while test 0
do
main_menu
done
附錄二 : grep 簡介
-------------------------------------------------------------------
grep -- 從檔案每一列中找出特定的樣板格式
grep [options] regular_expressions [files]
options :
-b 印出符合的那一列是在整個檔案中的第幾個 byte .
-c 只印出共有多少列符合 regular_expressions .
-h 當我們從許多檔案中找尋適合的條件時 , 假如找到了 , 就會印出是在
那個檔案中找到的 . 然而 , 假如我們加了 -h 這個 option . 那麼 ,
輸出的結果就不會告訴我們 , 是在那個檔案中找到符合的樣板 .
-i 忽略 regular_expressions 中大小寫的差別 .
-l 只印出在那個檔案中有找到 , 而不印出檔案中符合的樣版 .
-n 印出符合的樣板是在檔案中的第幾列 .
-s 抑制錯誤訊息 , 像不存在的檔案啦 , 或者是不能讀的檔案 .
-v 印出不符合 regular_expressions 的那些列 .
example :
從 test1 這個檔案中 , 找出含有 app 的列 :
% grep 'app' test1
從 passwd 這個檔案中找出到底有多少人使用 tcsh :
% grep -c '/bin/tcsh' /etc/passwd
從目前的目錄下 , 找出任一列開頭具有 #include 的檔案 :
% grep -l '^#include' *
從 test2 這個檔案中 , 找出那些不含 class 的列 :
% grep -v 'class' test2
列出不含 pattern 的檔案 :
% grep -c pattern files | grep :0 | cut -d":" -f1
regular expressions 正規表示式
-------------------------------------------------------------
. 可符合任意一個字元 ( 包含空白字元 )
[] 可符合方括弧中列舉的字元
[^ regular_expressions] 不符合 regular_expression
* 與零個或更多個 * 前的字元吻合
.* 符合零或更多個字元
^regular_expressions 符合在每一列開頭的 regular_expressions
下面是 regular expressions 的一些例子
regular expressions match
---------------------------------------------------------------
ring ring , spring , ringing , stringing
^^^^ ^^^^ ^^^^ ^^^^
.alk walk , talk , alk , skialk
^^^^ ^^^^ ^^^^ ^^^^
[bB]ill bill , Bill , biller
^^^^ ^^^^ ^^^^
number[5-9] number7 , number90
^^^^^^^ ^^^^^^^
[^ a-z] a21 , c1 , 123
^^ ^ ^^^
ab* acc , ab , abb , abc
^^^ ^^ ^^^ ^^^
ab.* ab , abb , abbc
^^ ^^^ ^^^^
^T 符合以 T 為開頭的每一列 ; This line
^
參考資料 :
----------------------------------------------------------------
Shell :
1. Title: Unix Shell Programming
Authors: Stephen Kochan and Patrick Wood
Publisher: Hayden
Edition: 1990
ISBN: 0-672-48448-X
2. Title: The Unix C Shell Field Guide
Authors: Gail Anderson and Paul Anderson
Publisher: Prentice Hall
Edition: 1986
ISBN: 0-13-937468-X
General Unix Texts :
1. Title: Unix Power Tools
Authors: Jerry Peek, Tim O'Reilly and Mike Loukides (and other
Publisher: O'Reilly / Bantam contributors)
Edition: 1993
ISBN: 0-553-35402-7
2. Title: Unix in a Nutshell
Authors: Daniel Gilly and O'Reilly staff
Publisher: O'Reilly
Edition: 2nd ed. 1992 (for System V and Solaris 2)
ISBN: 1-56592-001-5
Programming :
1. Title: Advanced Programming in The Unix Environment
Author: Richard Stevens
Publisher: Addison-Wesley
Edition: 1992
ISBN: 0-201-56317-7
Other :
1. UNIX FAQ
如何聯絡作者 :
地址 : 交通大學十舍315R
E-mail : jhhsu@csie.nctu.edu.tw
u8217017@cc.nctu.edu.tw
假如您發現文章中有任何錯誤之處 , 請通知作者 .
還有 , 讀者的建議與批評也非常的歡迎 .
(0.1) 讀者需要那些基礎 ?
這篇文章的主題是 UNIX Shell Programming . 閱讀這篇文章須要一點
基礎 , 包括 : 對 UNIX 的命令要有一點認識 , 尤其是常用 , 重要的
命令 . 此外 , 你也必需會在 UNIX 上使用 editor , 像 vi , joe 或
emacs 等 ...... 當然 , 假如你有寫程式的經驗 , 那麼 UNIX Shell
Programming 對你來說不過是反掌折枝罷了 .
(0.2) 使用那一種 Shell ? Why ?
既然 Shell 有很多種 , 我們所要介紹是最早由 AT&T Bell Labs 的
Stephen Bourne 所寫的標準 Shell , 也就是所謂的 Bourne Shell .
在一些情況下 , 也會順便提一下 C Shell 相關的東西 .
既然現在已經有了更新更好的 Shell , 那我們為什麼還要介紹 Bourne
Shell 呢? 這是因為 , 在 UNIX Shell Programming 中 , 有許多都
還是用 Bourne Shell 的語法寫成的 , 尤其像 /etc/rc.X ( SunOS )
而一般使用者或許用 C Shell 的增強版 tcsh 或 bash ( Bourne Again
Shell ) 這些比較新的 Shell . 不過沒有關係 , 只要系統中有 sh
( Bourne Shell ) 這個直譯器存在 , 那麼 , 我們都可以用 Bourne
Shell 的語法來寫程式 . ( 沒有 sh 的系統 , 是很難想像的 ...... )
還有一點 , 用 sh 來寫程式實際上比用 csh 要好 , 網路上已經有
討論過了 , 很多書也都提到用 csh 寫程式的缺點 , 並建議不要用
csh 來寫程式 . 我個人沒有什麼意見 , sh 與 csh 就好比程式語言
中的 Pascal 與 C (只是比喻) , 想必學過這兩種語言的人都知道 ,
只要學會了其中一種 , 另一種也不算太難學 , sh 與 csh 也是一樣 .
況且寫程式能真正作事就可以 , 不需要一下子用 sh , 一下子用 csh
來寫程式 .
*************************************************************
注意 !!!! 所有的例子都要在 Bourne Shell 系列的環境下操作
建議先看看 2.3 , 才不會搞了半天 , 才發覺原來你是用 C Shell
系列的 Shell (Shell 的種類及系列可參考 2.3)
此外 , 文中的一些例子都是在 Linux 下操作 , 在 SunOS 或 HP
上 , 命令也許有少許的不同 . 當你在演練時 , 假如不能正確的
執行 , 你應該看看指令的 manual . 像 SunOS 的 find 與 Linux
的 find 就有差異 . 大部份的例子都可以在各種不同的平臺上執行 .
*************************************************************
(0.3) 本篇文章的架構
本來想用 html 來寫的 , 這樣似乎比較好 , 但是覺得有點麻煩 , 所
以還是用平常的格式來寫 , 各位假如用 joe , 那麼當看到 (參考 x.y)
時 , 你還是可以用 ctrl-k f 來尋找 x.y 的標題 ; 在 vi 下也可以用
/x.y 來尋找 , 這樣也許也可以達到 hypertext 的效果吧 ...... :)
Chap 1 -- 簡介
Chap 2 -- 基本知識
Chap 3 -- Shell Programming 中的資料表示
Chap 4 -- Shell Programming 中的語法
Chap 5 -- 讀取資料及程式偵錯
附錄一 -- Shell Summary 及 一些 Shell Script 的例子
附錄二 -- grep 簡介及 regular expressions
參考資料 -- 介紹一些相關的書及資料
(0.4) 假如你已經對 Bourne Shell Programming 很熟 ......
那也許附錄一 Shell Summary 會對你有複習及快速參考的好處 !
還有 , 參考資料中的書目 , 都是值得一看的書 , 假如你還沒看過 ,
那我想你可以參考看看 . 當然 , man 及 grep 的配合 , 也常常
可以助你一臂之力 .
Chap 1 -- 簡介
無疑的 , UNIX 是一個功能強大的作業系統 , 它多人多工特性 , 網路
的支援 , 多采多姿的 X Windows System , 都相當的吸引人 . 除此之外
UNIX 還有一項重要的特性 , 就是它提供了數以百計的命令 .
每個人都知道 , DOS 下面也不過二三十個命令 , 這二三十個命令可能
一兩天就可以學的很好了 ; 但 UNIX 作業系統則不然 , 數百個命令就算
都學會了 , 若我們不知道如何去 "組合這些命令" , 那我們將失去學習
UNIX 的最大樂趣 !!
有人會覺得疑惑 : "組合命令"會有趣嗎 ? 什麼叫作 "組合命令" ?
這就是我們所要學習的 . 現在就舉一個最簡單的例子而言 : 請計算目前
工作目錄下共有多少個普通檔案 ( Regular File ) ?
當你遇到這樣的問題 , 你要如何解決 ? 有人會說 : " 我用 ls -l 列
出目前工作目錄下所有的檔案 , 然後慢慢的數 ...... " , 假如你用了
這樣的方法 , 而目前工作目錄下又有數百個檔案 , 那麼 , 我真的很敬
佩你 "愚公移山" 的精神 . 還有些人比較 "暴力" , 他說 : " 這個嘛
很簡單 , 我寫一個 C 語言程式 , 用 opendir , lstat 等函數來計算
某一目錄下有多少個普通檔案 ...... " 假如你要這樣做 , 那我當然不
反對 , 常練習寫程式也是不錯的 . 然而 , 就為了這麼小小的一個問題
而寫一段 C 語言的程式 , 未免太小題大作 . 實際上 , 解答的關鍵就
在 : " 你是不是會從數百個 UNIX 的命令中挑出適合的命令 , 然後把
這些命令組合起來 , 以達到我們所要的目的 !! " . " UNIX 的每個命令
就好比各種不同的 IC , 每種 IC 都有它特殊的作用 , 藉由不同 IC 的
組合 . 我們可以達到我們所要的功能 " , " 要達到目的的方法 , 常常
不是只有一種 ......" 上面這幾句話相當重要 , 各位現在也許還不清
楚 . 不過上面幾句話所提的 , 的確就是 UNIX 頗為吸引人的地方之一 .
除了命令之外 , Shell 提供了一些類似一般程式語言的語法 , 我們可
以利用這些語法來寫程式 . 而這點也正是本篇文章的最主要內容 .
Chap 2 -- 基本知識
(2.1) 什麼是 Shell ?
我們可以把 UNIX 切成兩方面來看 , 一部份是系統的核心 (kernel) ,
另一部份就是系統中的應用程式 . 舉凡我們常用的一些命令 , 像
ls , find 與 Shell 一樣 , 都可被歸類為應用程式 . 所以 , Shell
的地位與其它的應用程式並沒有什麼差別 .
我們可以看看簽入 UNIX 作業系統的過程 : 當我們輸入 username 之
後 , getty 就結束 , 取而代之的是 login . 在我們輸入正確的
password 後 , 系統就依照 /etc/passwd 中的記錄 , 為使用者啟動
相對應的 Shell . 在這個時候 , 使用者就會得到一個提示符號 .
接著我們就可以下達一些命令 .
-- getty --> login --> shell --> logout --
^ |
|------------------------------------------
Shell 有另一個名稱叫做 "命令列直譯器" (command interpreter)
從這個名稱當中 , 我們就可以了解到 Shell 實際上為命令列的翻譯官
當我們在提示符號後輸入一長串的命令 , 並按下 Enter 之後 , Shell
就將我們所輸入的東西 , 做一個適當的分析及處理 .
(2.2) Shell 負責的事
對系統做交談性的使用 . 包含了輸出輸入導向 ( 參考 2.5 ) ,
管線 ( 參考 2.6 ) Wildcard 的展開及匹配 ( 參考 2.4 ) , 另外
還有 Job Control( 從 SVR4 開始) , 變數環境設定?. 解譯命令 ....
還有一點很重要 , Shell 有它自己內建的程式語言 , 利用它的語法
我們可以來寫程式 , 寫出來程式功能並不像一般的程式語言 ( 如 C
語言 ) 那麼多 , 然而 , 在很多情況下就夠用了 . 這部份也就是整
篇文章的重點 .
(2.3) UNIX 系統上 Shell 的種類
在 DOS 中 , 想必各位一定都知道它的 Shell 為 command.com . 特別
要說明的是 : " Shell 是可以被置換掉的 ! " 所以 , 許多人在 DOS
中 , 常常把他們的 Shell 由 command.com 換成 4dos.com , 以得到
更多的功能 , 這已經是很平常的事了 . 在 UNIX 系統中也是一樣 ,
使用 chsh 這個命令 , 你可以挑你喜歡的 Shell 來使用 . 合法的
Shell 清單被列在 /etc/shells 這個檔案中 .
Shell 的種類可以分為兩個支派 , 一為由 Bourne Shell 衍生而來的
包括了 sh (Bourne Shell) , ksh (Korn Shell) , bash (Bourne
Again Shell) , zsh (Z Shell) ; 另一支派為 C Shell 衍生而來的 ,
包括了 csh , tcsh . 現在許多人作業的環境常常是在 csh 或 tcsh
之下 , 尤其是從 BSD 衍生而來 SunOS 的環境 .
你可以使用 echo $SHELL 來獲知你現在到底使用那一種 Shell . 或
者直接看 /etc/passwd 的最後一個欄位 .
(2.4) Wildcard 的展開及匹配
Wildcard 有些人把它翻譯成 "萬用字元" , 下面所列舉的
項目 , 並不盡然每一種 Shell 都支援 , 各位要自己試試看 .
(2.4.1) * : 代表任意的字串 ( 字串可以是空的 )
% ls
haha haha1 haha2 memo1 memo13 memo2 memo23
% ls haha*
haha haha1 haha2
% ls memo*
memo1 memo13 memo2 memo23
(2.4.2) ? : 代表任意一個字元
% ls
memo1 memo13 memo2 memo23
% ls memo?
memo1 memo2
% ls memo1?
memo13
(2.4.3) []: 代表符合[]中所列舉的字元
% ls
haha haha1 haha2 memo1 memo13 memo2 memo23
% ls memo[12]
memo1 memo2
[a-e]bc 會符合 abc , bbc , cbc , dbc , ebc
(2.4.4) [!]: [] 中的字元 , 若加了 ! , 則表示不符合[]中所列舉的字元
% ls
haha haha1 haha2 memo1 memo13 memo2 memo23
% ls memo[!1]
memo2
[!a-z]bc 會符合不是以小寫英文字母開頭 , 後面接著 bc
的字串 . 如會符合 : Cbc , 6bc 等 ......
(2.4.5) {word1,word2....} : 如 my_{dog,cat,pig} 會符合 my_dog
my_cat , my_pig
(2.4.6) \ : 接在 \ 之後的特殊字元 , 其特殊的意義會被取消
例如 , 有一個檔案 , 它的名字真的叫 *abc , 那我們得用
cat \*abc 才能把 *abc 中的內容顯示出來 .
(2.4.7) 更多關於 Wildcard 的討論 :
(I) glob : 這種 Wildcard 匹配的動作 , 我們稱為 " globbing "
在很多種 Shell 之中都有一個叫作 noglob 的變數 , 可以把
Wildcard 展開匹配的動作關掉 .
% ls *bc
abc bbc cbc dbc
% set noglob <-- C Shell 中設定變數的方法
( 設定變數參考 2.7)
% ls *bc
ls: *bc: No such file or directory
(II) * 不會包含 . 開頭的檔案 , 要符合以 . 為開頭的檔案 , 你
可以試試 .??* 或 .[!\.]* ( For Newer Bourne Shell )
.[^.]* ( For tcsh )
(2.5) 輸出輸入導向
(2.5.1) Standard Input , Standard Output and Standard Error
在某方面來說 , 把資料寫到一個檔案中與寫到終端機並沒有什麼差別
相同的 , 把資料從檔案中讀出與從鍵盤讀出亦沒有什麼差別 .
在 UNIX 系統中 , 標準輸出輸入 ( Standard I/O ) 提供了一些預設
的輸出輸入設備 , 這些就是 stdin , stdout , 以及 stderr .
stdin : 一般說來 , 就是來自你鍵盤的輸入 , 但許多情況下 , 程式
常忽略了 stdin , 而直接在命令列的參數中拿 . 如 :
cat haha . haha 就可以當做 stdin .
stdout : 正常訊息輸出 , 預設是終端機 .
stderr : 錯誤訊息輸出 , 與 stdout 一樣 , 預設是終端機 .
stdin 的檔案描述值 ( file descriptor number ) 是 0 ,
stdout 是 1 , stderr 是 2 .
(2.5.2) 輸出輸入導向
舉一個最簡單的例子 , 我們用 ls -la 可列出目前目錄下所有的
內容 . 然而 , 若我們想要把終端機上的輸出放到一個檔案中 ,
那我們要如何做呢 ? 這時候 , 我們就要把原來終端機上的輸出
(也就是預設的標準輸出)導向至一個檔案 . 我們可以這樣作 :
ls -la > output_file
這個 > 的符號就是導向符號 .
下表是一般經常看到的輸出輸入導向 , prog 就是 program 的意思
而 file 就是一個檔案:
功能 sh,ksh,bash csh,tcsh
--------------------------------------------------------------
把標準輸出導到一個檔案 prog > file prog > file
把標準輸出導到檔案描述值 n prog >&n
把原來輸出至檔案描述值 m prog m>&n
的輸出與輸出至檔案描述值 n
的輸出 , 一起導向至檔案描
述值 n
把標準錯誤輸出導到一個檔案 prog 2> file
把標準輸出關閉 prog >&-
把標準輸出以及標準錯誤輸出 prog > file 2>&1 prog >& file
導到一個檔案
把標準輸出導到 f1 , 把標準 (prog > f1) 2>f2 (prog > f1) >& f2
錯誤輸出導到 f2
--------------------------------------------------------------
把標準輸出導到一個檔案的後面 prog >> file prog >> file
把標準錯誤輸出導到一個檔案的 prog 2>> file
後面
把標準輸出以及標準錯誤輸出導 prog >> file 2>&1 prog >>& file
到一個檔案的後面
--------------------------------------------------------------
把檔案當作標準輸入 prog < file prog < file
從檔案描述值 n 的地方讀取 prog <&n
當作 prog 的輸入
把標準輸入關閉 prog <&-
--------------------------------------------------------------
把 file1 當成 prog 的標準 prog < file1 > file2
輸入 , 並把標準輸出的結果
再導向至 file2
--------------------------------------------------------------
從鍵盤的輸入當作 stdin , 直 prog <
w > who_log
把 w 這個指令的輸出導到一個叫做 who_log 的檔案
cat file1 > file2
^^^^^^^^^
把 cat file1 的輸出導到一個叫做 file2 的檔案
(II) prog 2> file
先寫一個叫做 test1.c 的 C 語言程式如下 :
#include
void main(void)
{
print("Standard Error Test"); /* 故意寫錯 */
}
接著輸入 cc test1.c <-- compile test1.c
這時我們會看到類似下面的錯誤訊息:
/tmp/cca006131.o(.text+0x1e): undefined reference to `print'
從上面的情況中 , 我們可以看到 , 這個 C 語言程式只有一個錯誤
所以得到的錯誤訊息也不多 . 然而 , 我們若 compile 一個很大的
程式 , 錯誤也許會有數百個也不一定 , 這時候 , 若我們不採用把
錯誤輸出導向到檔案的方法 , 那麼錯誤訊息在終端機上立刻就捲出
去了 , 使用者就無法看的完全 . 比較好的方法是把錯誤訊息導到
一個檔案中再慢慢分析比較好 . 所以這時候我們應該這樣做 :
cc test1.c 2> errmsg
此時 , 原本在終端機上的錯誤輸出就會被導到 errmsg 這個檔案中
還有 , prog 2> /dev/null 可以抑制錯誤訊息的輸出 , 因為它把
標準錯誤輸出導到 /dev/null 這個垃圾桶中 , 這個功能與上面的
prog >&- 有相同的地方 , 本來應該輸出的訊息都被壓抑了 . 只
不過一個是抑制錯誤訊息輸出 , 一個是抑制正常訊息輸出 . 不過
prog 2>&- 就可以達到與 prog 2> /dev/null 相同的效果 .
(III) prog > file 2>&1
我們可以試試下面的命令 :
find / -size +2000k > ~/output 2>&1
接著你可以在你的 Home Directory 下找到 output 這個檔案 , 你
可以看看它的內容 . 你就會發覺 , 有些目錄對你來說是
Permission denied 的 , 這些都是錯誤訊息的輸出 . 你也會看到
有些檔案名 , 這些檔案都是大於 2000k 的 , 屬於正常訊息的輸出
所以 , output 這個檔案的確包含著標準輸出以及標準錯誤輸出的
訊息 .
** 注意 !!! SunOS 上的 find 是以 block 為單位 , 所以 , 假
如你在 SunOS 上操作 , 那請把 +2000k 的 k 去掉 , 如下 :
find / -size +2000 > ~/output 2>&1
(IV) (prog > f1) 2>f2
再一次的 , 我們用上面的命令 , 但改為 :
(find / -size +2000k > ~/normal_out) 2>/tmp/error_out
在 (III) 中 , 我們把正常訊息的輸出與錯誤訊息的輸出都混在
~/output 這個檔案中 . 從某個角度來說 , 這不是很好的作法 ,
我們所希望的 , 是把正常訊息的輸出放在一個檔案 , 而錯誤
訊息的輸出放到另一個檔案 . 此時 , 我們就要採用 (IV) 這個
方法 . 在上面的命令中 , 我們把 find 這個命令的正常訊息輸出
導到 ~/normal_out 這個檔案 , 而錯誤訊息輸出我們就把它導到
/tmp/error_out 這個檔案中 .
(V) 至於 prog >> file , prog 2>> file , prog >> file 2>&1
與 prog > file , prog 2> file , prog > file 2>&1
只是把 > 換成 >> , 而它的差別就只在 : >> 是"附加"的意思
並不像 > 會把原來導向過去的檔案清除 (假如欲導向過去的檔案
已經存在 , 而且 noclobber 變數沒有被設定的話 !當然 , 若原來
檔案不存在 , 那就會製造出新的檔案)
所以 , 試試看下面的命令就可明白 :
ls -la /usr > file1
ls -la /etc > file1
cat file1 <-- 你看到的是 /etc 下的內容
接著 , 再試試
ls -la /usr > file2
ls -la /etc >> file2 <-- 把輸出的結果"附加"在 file2 後
cat file2 <-- 你看到的是 /usr 與 /etc 下所有的內容
(VI) prog < file
有些命令 , 它要處理的內容是要由標準輸入中拿的 . 如 mail 這
個命令 , 若我們在提示符號下鍵入 mail abc@cc.nctu.edu.tw
此時 , 我們就得由鍵盤上輸入一些資料 , 最後按 Ctrl-d 作為結
束 . 此時 , 我們剛剛輸入的資料就會經過 mail 處理寄出電子郵
件 . 然而 , 這樣作的壞處是 , 萬一你打錯了字想修改前面幾列
的內容 , 就變得困難了 . 所們所希望的 , 是信件的內容先用
普通好用的文書編輯器寫好 , 再把寫好的東西寄出去 . 此時 , <
就派上用場 , 我們就把寫好的檔案當作是從標準輸入給與的內容
導向給 prog . 所以 mail abc@cc.nctu.edu.tw < report
report 就相當於從鍵盤上輸入的內容 .
(VII) prog < file1 > file2
這個型式是由兩個導向符號所組成 , 看起來有點複雜 , 其實一點也
不 . 只要你拆成兩部份來看 : prog < file1 > file2
^^^^^^^^^^^^ (i)
^^^^^^^^^^^^^^^^^^^^ (ii)
第 (i) 部份 : 把 file1 當作是 prog 的標準輸入
第 (ii) 部份 : 把第 (i) 部份的輸出結果 , 導向至 file2 中
舉個例子 :
tr 'a-z' 'A-Z' < sour > dest
首先 , tr 'a-z' 'A-Z' < sour 會把 sour 這個檔案中的小寫字元
全部換成大寫字元 , 此時 , 假如沒有後面 > dest 的話 , 我們會在
終端機上看到轉換後的輸出 . 然而 , 我們使用 > dest 再把原來應
該在終端機上的輸出導向到 dest 這個檔案中 . 所以 , dest 就會
是 sour 經過轉換後的內容 .
(VIII) prog << c
這個型式看起來有點奇怪 , 它所作的事就是 : 從標準輸入讀取資料
一直遇到 c 為止 , 然後把這些資料都交給 prog 處理 .
這種動作有個名稱叫 "Here Document"
比如說下面的例子 :
% mail veronica << End_Of_Letter
> this is a test
> but ....
> End_Of_Letter <-- 要單獨一列
本來我們輸入 mail veronica 的話 , 那麼我們就得從鍵盤上輸入
資料 , 直到按下了 Ctrl-d 為止 . 然而 , 我們現在指定 : 一
直從標準輸入讀取資料 , 直到遇見 End_Of_Letter 為止 . 不過
上面的例子並不常見 . 比較常見的是下面這個例子 :
% cat << It_is_OK
> This is Here Document Test
> This Sample is quite typical
> You will see the same style in some Shell Script
> See You
> It_is_OK
如此作可以一次顯示一大段訊息 , 而不必很麻煩的用許多列 echo
來做 .
(2.6) 管線 (Pipes)
管線就是達成 "組合命令" 的方法 , 極端重要 ! 它的型式是這種樣子
prog1 | prog2
其中 , | 就是管線的符號 , 上面的意思是 : 把 prog1 的輸出 , 當
做 prog2 的輸入 . 你也可以想像成 : prog1 及 prog2 是兩臺機器 ,
| 是它們之間的傳送帶 , 經過 prog1 處理過的東西 , 再交給 prog2
去加工 . 舉幾個例子 :
ls -la | more <-- 把 ls -la 的輸出交給 more 去作處理 , 想
當然耳 , more 會把來自 ls -la 的輸出作
分頁處理 .
man ls | wc -l <-- 把 man ls 的輸出交給 wc 去作處理 , wc
使用選項 -l 可計算來自 man ls 的輸出
有多少列 .
當然 , 你也可以組合出兩個以上或更多的管線 :
prog1 | prog2 | prog3
如: who | cut -c1-8 | sort <-- who 的輸出交給 cut 處理 ,
cut -c1-8 會取出每列的第
一到第八個字元 , 然後再把
這些輸出交給 sort 做排序
所以 , " 熟悉每個命令要的輸入是什麼 ? 又會產生什麼輸出 ? "
是有效使用管線的不二法門 ! 各位應該一邊使用管線 , 一邊加強
命令的基礎 , 當你熟悉到了某一程度 , 就可以自己利用管線 , 輸
出輸入導向來 " 創造 " 出新的工具 , UNIX 有趣且重要的地方就
在這裡 !!!
此時 , 我們在 Chap 1 所提的 : " 計算目前工作目錄下有多少普
通的檔案 ? " 假如你對命令熟悉的話 , 你應該可以回答出來了 ,
而且方法太多種了 !
像我可以舉一種方法如下 (不計算以 . 為開頭的隱藏檔) :
ls -l | grep '^-' | wc -l
各位去分析看看 , 為什麼可以計算出來 , 並且去想想看它是如何
工作的 ......
(2.7) 一些環境變數的指定
在 Shell 中 , 有一點很重要的是 , 我們自己可以設定一些變數 ,
其中有些變數可以影響操作環境的行為 , 或者是影響應用程式 .
舉個例子來說好了 , 各位常用的 elm , 它所使用的文書編輯器
就會受到 EDITOR 這個變數的影響 . 假如 EDITOR 的內容是 joe ,
那麼 elm 就會使用 joe 拿來當文書編輯器 , 假如 EDITOR 的內
容是 vi , 則使用 vi . 既然我們知道某些變數可以影響應用程式
甚至整體的操作行為 , 那我們要如何設定這些變數呢 ? 請看下表
sh,ksh,bash csh,tcsh
-----------------------------------------------------------
指定變數 var=value set var value
指定環境整體 export var=value setenv var value
變數
-----------------------------------------------------------
上表中 , var 是變數名 , value 是變數值 ; 還有
上面兩種設定是不一樣的 , 因為 Shell 之中還可以再 invoke 一
個 Subshell , var=value 只能算是設定區域性的變數 , 只在設定
的那個 Shell 之中有效 , 對於 Subshell 就無效了 .
而 export var=value 就不一樣了 , 它對於 Subshell 也有效 .
你可以做下面的實驗 :
% color=blue <-- 把 blue 指定給變數 color ,
% echo $color <-- 顯示變數 color 的內容
blue
% sh <-- 開啟一個 subshell
% echo $color <-- 顯示變數 color 的內容
<-- 沒有變數 color 的內容
%
% export color=blue <-- 把 blue 指定給環境整體變數 color
% echo $color <-- 顯示變數 color 的內容
blue
% sh <-- 開啟一個 subshell
% echo $color <-- 顯示變數 color 的內容
blue <-- 有變數 color 的內容
前面在 Wildcards 有提過一個 noclobber 的變數 , 在屬於 Bourne
Shell 系列的 Shell 中 , 你可以使用 set -o noclobber 或者是
set +o noclobber 來使 noclobber 發生或不發生作用 . 使用 set -o
不加變數名可看到一些變數的值 ( 只能看到那些為 on 或 off ,
對環境會造成影響的變數 ) 其它的變數可用 set 直接看到 .
在 C Shell 系列中 , 直接使用 set noclobber 就可以了 .
至於有那些變數可設定 , 你可以用 man 看看你所使用的 Shell .
Chap 3 -- Shell Programming 中的資料表示
截自目前為止 , 我們都是處於在提示符號後下命令的階段 . 並沒
有 "寫程式" 的感覺 . 在這一章當中 , 我們就要開始介紹 Shell
Programming 中的資料表示方式
(3.1) 什麼是 Shell Script ?
假如你使用 UNIX 已經有一段時間 , 那你一定常常聽到這個名詞 .
既然前面提過 , Shell 本身就是一個 interpreter , 那我們當然
可以在提示符號下 "直接" 寫程式的 ! 可以看看下面的例子 :
% case 3 in
> [1-5]) echo 'haha';;
> [6-10]) echo 'lala';;
> esac
haha
你自己也可以試試看 , 不過你得使用 Bourne Shell 系列的 Shell
好了 , 我們已經看到可以在提示符號下寫程式 , 然而 , 我們並不
喜歡這樣做 . 因為在提示符號下要修改程式很麻煩 , 小的程式也就
算了 , 大的程式我想沒有人會喜歡這種方法 . 所以 , 在一般的情
況下 , 我們可以先用文書編輯器寫好一個程式並存成一個檔案 , 然
後再把這個檔案交給 Shell 去執行 , 而這個檔案就稱為 Shell
Script ! 先給各位看看一個最簡單的 Shell Script :
#!/bin/sh
ps -a | more
上面的兩列內容 , 我把它放在一個檔案中 , 然後直接執行它就可以.
但不要忘記 , 你要對這個檔案有讀以及執行的權限 .
(3.2) Shell Programming 的兩大要素 : 資料及語法
假如你學過高階語言 , 你應該可以了解到 , 其實每種高階語言的概
念都差不多 , 它們只是語法不同 , 也許資料表示的方法也有少許的
差別 . 但是 , 學會了一種再去學別種並不算難 .
Shell Programming 也是一樣 , 它還是有大家熟悉的語法如 if , do
for , while 等等的東西 . 比較特殊的是它的資料型態 .
下面就開始進入 Shell Programming 的資料表示 . 語法請看 Chap 4
(3.3) 註解及 #!/bin/sh
很多程式語言都有其註解格式 , 在一個 Shell Script 中 , 假如我
們看到一個 # 號 , 那從 # 號開始一直到那列的結尾都是註解 .
註解只是提高程式的可閱讀度 , 對 Shell 來說並不會做解譯的動作
舉一個 Shell Script 如下:
#!/bin/sh
#
# Count_bash_user_number : report how many users use bash
#
grep -c '/bin/bash' /etc/passwd # grep -c : print match
# number
這個 Shell Script 中 , 真正作事的只有一列 , 其它 # 後接的文
字都是註解 . 但特別要注意的是 : #!/bin/sh 不是註解 , 雖然它
是以 # 為開頭 , 但是它有特殊的意義 . 在最早的時候因為只有 sh
所以也並不需要 #!/bin/sh , 但是後來 Shell 的種類越來越多 ,
有些人用 csh , 有些人用 tcsh . 這時候我們就要特別的指出 , 這
個 Shell Script 是用那一種 Shell 寫的 . 假如你的 Shell Script
是以 Borune Shell 的語法寫成的 , 那你就得加上 #!/bin/sh , 假
如是以 C Shell 的語法寫成的 , 那你得加上 #!/bin/csh . 這列一
定要加嗎 ? 答案是不盡然 , 如果你目前使用的 Shell 與你寫的
Shell Script 使用相同的語法 , 那可以不加 . 但是我舉一個例子:
假如你用的是 tcsh , 但卻用 sh 的與法來寫 Shell Script , 而沒有
在 Shell Script 的第一列加上 #!/bin/sh , 那麼會發生什麼情形?
很簡單 , tcsh 會以為你寫的 Shell Script 是用 tcsh 的語法寫成
的 , 所以在解譯的過程中就會出現錯誤 . 但是若我們有加 #!/bin/sh
那麼 , tcsh 就會叫用 sh 來解譯你的 Shell Script , 自然就不會
有錯誤產生 .
我的建議是 : "無論如何 , 請明確的在 Shell Script 的第一列指出
這個 Shell Script 是用那一種 Shell 的語法寫成的 !"
(3.4) 變數
(3.4.1) 變數的指定
如同大部份的程式語言一樣 , Shell Programming Language 中也有
變數的存在 , 那我們要如何指定一個變數呢 ? 很簡單 , 如下:
variable=value
舉幾個例子 :
count=1
X11_bin_path=/usr/X11/bin
但在指定變數時 , 有幾點要注意的 : 第一 , 等號的左右兩邊不要
有空格 , 譬如說 , 不可寫成 : color = blue , color= blue
color =blue , 一定得寫成 : color=blue . 第二 , 變數沒有所謂
的 "資料型態" . 當你指定變數時 , 變數只是被當成字元的組合來
看待 . 如上面的例子 : count=1 , count 放的是單純的 1 , 而不
像 C 語言中所謂 "整數型態的 1" 第三 , 變數指定的動作實際上
也可以在提示符號後直接作 . ( 如同 2.7 所提到的 )
(3.4.2) 變數的內容 : $variable
使用 echo $variable 可以顯示變數的內容 .
像剛剛的例子 , count=1 , 那麼 , 使用 echo $count 就會得到 1
使用 echo $X11_bin_path 就會得到 /usr/X11/bin
所以 , 我們可以了解到 : $variable 是變數的內容 !
我現在給一個問題 , 各位練習看看 : 把 var1 的內容設定成 10 ,
把 var2 的內容設定成 20 , 接著把 var2 的內容設定給 var1 .
var1=10
var2=20
var1=$var2
echo $var1
再實際練習一下 , 下面的四列與上面的四列有何不同 ?
var1=10
var2=20
var1=var2
echo $var1
(3.4.3) 變數與 Wildcard
你可以在提示符號下這樣試試 :
list=*
ls $list
上面的兩列 , 首先把 * 指定給 list , 所以 $list 就是 *
那 ls $list 就會變成 ls *
five_char=?????
ls $five_char
上面兩列會把長度為五個字元的檔案列出 .
(3.4.4) ${variable}
有人試了 (3.4.3) 中的例子之後就想到 , 我們可不可以這樣做 :
file=mydoc
cp $file $filenew
把 mydoc 檔案複製一份 , 並把新檔案的名稱後加上 new 變成
mydocnew . 假如你按照上面的方法來作 , 會有錯誤產生 .
這是因為 , $file 固然是 mydoc 沒錯 , 然而 $filenew 卻是
一個內容不知道是什麼的變數 . 所以我們應該這樣做 :
file=mydoc
cp ${file} ${file}new
其中 ${file} 會換成 mydoc , ${file}new 會換成 mydocnew
(3.5) 引號
在 Shell Programming 中 , 很特殊的一點就是引號 , 引號有三種
分別為 ' ' 單引號 , " " 雙引號 , ` ` 反單引號 . 它們分別在
Shell Programming 中扮演不同但都相當重要的角色 . 我們分別敘
述如下 :
(3.5.1) 單引號
單引號最大的用處是 : 使得兩個單引號之間所夾的內容保持不變 .
比如說你想印出 : pig cat dog
那假如你這樣作 : echo pig cat dog
那麼結果仍是 pig cat dog
要改成 : echo 'pig cat dog' 才能達到目地
還有 , 在單引號中的特殊符號也會失去意義 :
% echo '* is all'
* is all
% color=blue
% echo 'Is $color beautiful?'
Is $color beautiful?
% echo '> < >> << [ ] "" & { }'
> < >> << [ ] "" & { }
從上面的例子中 , 我們都可以看到 , 像 * , ? , $variable 等
本來應該會被替換的東西 , 現在都原封不動的照印 .所以 , 單引號
通常拿來印出一段固定的訊息 .
(3.5.2) 雙引號
雙引號的用法與單引號有一些不同 . 我們提過 , 在兩個單引號之間所
夾的內容都會原封不動 . 然而 , 雙引號不同 . 在兩個雙引號之中
的內容 , 有以下幾種會被替換 :
(I) $variable , 以及任何 $ 後有意義的字元 (參考 3.7)
比較以下的不同 :
% color=blue
% echo $color
blue
% echo '$color'
$color
% echo "$color"
blue
% all_list=*
% echo $all_list <-- $all_list 換成 * 號
a.c b.c
% echo '$all_list' <-- 單引號不替換
$all_list
% echo "$all_list" <-- 雙引號中的 $all_list 換成 *
* 變成 echo "*" , 但雙引號中
的 * 號不會再做替換 .
(II) 反斜線 : \
在 (2.4.6) 之中提到 , \ 後的字元 , 其特殊意義會被取消
在上面提到 , $variable 在雙引號之中會被替換 . 假如我
們真的想印出 $ 號 , 那麼就得在 $ 號前加上 \
% number=10
% echo "your value is $number"
your value is 10
% echo "your value is \$number" <-- 取消 $ 之特殊意義
your value is $number
% echo "your value is \"$number\" "
your value is "10"
(III) 反單引號 : ` `
( 參考 3.5.3 )
(3.5.3) 反單引號
反單引號是一個往右撇的引號 . 兩個反單引號所夾的內容會先執行
舉個例子 :
% echo "Your current directory is `pwd` !"
Your current directory is /home/jhhsu
我們說過 , 雙引號之中的內容大多不做替換 , 除了 $variable ,
反斜線 , 反單引號之外 . 上面就是一個很好的例子 . 雙引號
中的 `pwd` 會先執行 , 得到的結果是 /home/jhhsu . 所以 ,
`pwd` 被 /home/jhhsu 所取代 , 全句轉變成 :
echo "Your current directory is /home/jhhsu"
自然就在終端機上印出 Your current directory is /home/jhhsu
再舉一個例子 , 前面在導向時有學過 : mail jhhsu < letter
這樣可以把 letter 這個檔案寄給 jhhsu .
但假如我們想寄信給一大群人呢 ? 實際上你可以 elm 中 alias
的方法 , 不過這有點麻煩 . 我提出一個很直覺的方法 :
mail `cat member` < letter
檔案 member 中每列都寫上收信人的 E-mail address , letter
是信件內容 . 它工作的過程很簡單 , `cat member` 先執行 ,
得到的結果可能是 jhhsu veronica ghguo , 所以全句變為 :
mail jhhsu veronica ghguo < letter
(3.6) 變數的算數運算
在變數的算數運算方面 , 我們主要靠的是 expr 這個指令 . 各位
可以先在提示符號下試試 :
% expr 30 + 20
50
% expr 30 - 20
10
% expr 30 \* 20 <-- * 是乘號 , 但要以反斜線去除特殊意義
600
% expr 30 / 20 <-- 只會取整數部份
1
從上面我們可以看到 , expr 的確可以作整數的四則運算 . 但我們提
過 , 變數是沒有資料型別的 , 所以 expr 只能處理變數內容為可視
為整數的變數 . 如 :
% var1=10
% expr $var1 + 1 <-- 把 var1 加 1
像下面就錯了 :
% var1=10
% var2=20
% expr var1 + var2 <-- var1 及 var2 不是內容 , $var1
expr: non-numeric argument $var2 才是
上面要改成 :
% var1=10
% var2=20
% expr $var1 + $var2
下面是無意義的例子 :
% var1=red
% var2=green
% expr $var1 + $var2 <-- expr 不能處理 red + green
那假如我們要把某一變數的內容加一再放回去呢 ?
如同 Pascal 中的 i:=i+1 , C 語言中的 i=i+1 那要如何作 ?
很簡單 , 如下 :
% i=`expr $i + 1`
等號右邊的反單引號會先執行 , expr 把 i 加 1 然後再把得到
的結果指定給 i
(3.7) 命令列參數的傳遞
設計一個 Shell Script , 取得命令列的引數是相當重要的 . 舉個
例子 , 你想寫一個 Shell Script 叫做 plus . plus 需要兩個參數
然後把這兩個參數相加的結果印出 . 那你應該如何做呢 ? 又有時候
我們要判斷使用者參數的個數是不是正確 , 就像上面 plus , 總得
要有兩個參數 . 這些都要靠參數的特殊表示來達成 .
(3.7.1) $n 是第 n 個參數的內容 , n=0,1,2,3,4,5,6,7,8,9
在 Shell 變數指定中 , 你不能這樣做 : 1=first , 因為數字有
其特殊意義 . 像 $1 就是第 1 個參數的內容 , $2 是第 2 個 , 餘
類推 . 那第 n 個參數的內容有用嗎 ? 就用上面 plus 來說明好了 .
先用文書編輯器寫一個 Shell Script , 並命名為 plus 如下 :
#!/bin/sh
echo `expr $1 + $2`
因為一般人的 umask 值常是 022 , 製造出來的檔案都是沒有執行權限
所以我們得用 chmod u+x plus 來使 plus 有執行的權限 .
接著你可以在提示符號下輸入 :
plus 3 5
那得到的答案就會是 8 .
再舉一個例子 : 我們要計算某個目錄下有多少個檔案 . 而我們希望這
個目錄是由使用者在命令列的參數所給與的 . 那我們可以這樣寫 :
#!/bin/sh
ls $1 | wc -l
以後使用者只要輸入這個 Shell Script 的名稱 , 後面接著某目錄名 ,
那麼就可以計算出有多少檔案 . ( 這個 Shell Script 把目錄也當成
檔案來看待 )
還有 , $0 就是你執行命令的名稱
(3.7.2) $# 代表參數個數
上面的 plus 3 5 , 其中 3 是第一個參數 , 5 是第二個參數 , 我們
把上面的 plus 這個 Shell Script 加以擴充 , 變成 :
#!/bin/sh
echo "There are $# arguments"
echo "The Answer is `expr $1 + $2`"
這個例子利用到我們所目前所學過的不少知識 , 包含 :
雙引號中 , $ 號會被替換 . 又因為我們有 3 5 兩個參數 , 所以 $#
被換成 2 . 因而印出 There are 2 arguments .
雙引號中 , 兩個反單引號之中所夾的東西會先執行 . 所以 ,
`expr $1 + $2` 換成 `expr 3 + 5` 最後得到結果 8 . 因而印出
The Answer is 8 .
講到這裡 , 假如各位看不懂上面的 Shell Script , 那可見對引號
的應用還不太熟 , 我建議重看 (3.5) , 並多加練習 .
我們現在再寫一個 Shell Script , 負責顯示有多少個參數 , 並且
把前三個參數顯示出來 :
#!/bin/sh
echo "There are $# arguments"
echo "argument1 is $1"
echo "argument2 is $2"
echo "argument3 is $3"
拿這個 Shell Script 來試試 :
% argtest red green blue
There are 3 arguments
argument1 is red
argument2 is green
argument3 is blue
% argtest 'a b c'
There are 1 arguments
argument1 is a b c
argument2 is
argument3 is
% argtest "* <> ?"
There are 1 arguments
argument1 is * <> ?
argument2 is
argument3 is
% argtest *
There are 64 arguments
argument1 is a.c
argument2 is c.c
argument3 is e.c
% argtest `cat friend_list`
There are 3 arguments
argument1 is veronica
argument2 is jhhsu
argument3 is ghguo
(3.7.3) $* 代表所有的參數
把剛剛的 Shell Script 再加以擴充如下 :
#!/bin/sh
echo "There are $# arguments"
echo "argument1 is $1"
echo "argument2 is $2"
echo "argument3 is $3"
echo "all arguments : $*"
拿這個 Shell Script 來試試看 :
% argtest a b c
There are 3 arguments
argument1 is a
argument2 is b
argument3 is c
all arguments : a b c
% argtest 'a b c'
There are 1 arguments
argument1 is a b c
argument2 is
argument3 is
all arguments : a b c
% argtest "a b c"
There are 1 arguments
argument1 is a b c
argument2 is
argument3 is
all arguments : a b c
% argtest *
There are 7 arguments
argument1 is haha
argument2 is haha1
argument3 is haha2
all arguments : haha haha1 haha2 memo1 memo13 memo2 memo23
(3.7.4) 參數的移動 shift
前面提過 , $n 是第 n 個參數的內容 , 但我們要特別注意的是 , n
必需小於 10 , 也就是說 , 不可能有 $10 這樣的東西 . 那假如我
們參數的個數很多 , 超過了 10 個 , 而又想取得超過 10 個後的參
數 , 要如何做 ? 答案就在 shift .
shift 是一個單純的命令 , 它把 $n 的內容放到 $n-1 中 . 如 $2
放到 $1 , $3 放到 $2 ...... 但 $1 並不會放到 $0 , 而是永遠的
消失 . 看下面的 Shell Script 及執行結果 :
#!/bin/sh
echo "There are $# arguments"
echo "all argument : $*"
shift
echo "There are $# arguments"
echo "all argument : $*"
shift
echo "There are $# arguments"
echo "all argument : $*"
shift
echo "There are $# arguments"
echo "all argument : $*"
% argtest a b c d e f g h i j
There are 10 arguments
all argument : a b c d e f g h i j
There are 9 arguments
all argument : b c d e f g h i j
There are 8 arguments
all argument : c d e f g h i j
There are 7 arguments
all argument : d e f g h i j
所以從上面的例子得知 , 每 shift 一次 , 我們就可以在 $9 的地方
得到原來的第 10 個參數內容 . 如此一來 , 要取得第 10 個以後的參
數就一點也不困難了 . 還有 , $1 雖然會在 shift 後被丟掉 , 但假
如 $1 以後還要用到的話 , 我們仍然可以在 shift 前將它保留起來 :
.
.
.
temp=$1
shift
.
.
.
Chap 4 -- Shell Programming 中的語法
在 Chap 3 學完之後 , 相信各位已經可以寫一些小的程式 . 但是總
好像少了些什麼 ? 不錯 , 這些可說是每種 Programming Language
中的靈魂 : 語法 ! 語法包含了條件邏輯的判斷 , 迴圈 , 函數呼叫
等 ......有了這些 , Shell Programming Language 才算完整 .
(4.1) if 結構
if [ 測試條件 ]
then
.
.
fi
我們可以很輕易的看到 , if 與 fi 之間就是典型的 if 結構
假如測試條件成立 , 就作 then 與 fi之間所夾的事情 .
還有其它許許多多的流程控制都要用到 "測試條件" , 在繼續
介紹這些流程控制之前 , 我們得先介紹測試條件 .
(4.2) 測試條件
測試條件大體上可分為 (I) 字串測試 (II) 整數測試
(III) 檔案測試
(I) 字串測試 :
string1 = string2 # string1 等於 string2
string1 != string2 # string1 不等於 string2
string # string is not null
-n string # string is not null
-z string # string is null
前面提過 , 變數設定可能像這種樣子 : name=veronica
那假如我們要把 name 與某一字串做相等比較 , 應該這樣做 :
"$name" = veronica
假如要作不相等比較 , 應該這樣做 :
"$name" != veronica
看下面的 Shell Script :
#!/bin/sh
name=veronica
if [ "$name" = veronica ]
then
echo 'equal'
fi
if [ "$name" != veronica ]
then
echo 'not equal'
fi
執行結果 :
% strcmp
equal
分析上面的 Shell Script , 首先 , 把 veronica 指定給 name
此時 $name 就是 veronica . 執行到 if [ "$name" = veronica ]
"$name" 就會換成 veronica , 所以變成 :
if [ veronica = veronica ] <-- 字串相等比較成立
因為比較成立了 , 所以就做 then 至 fi 之中的事 , 也就是顯示
equal .
接著執行 if [ "$name" != veronica ] , 再一次的 , "$name"
被換成 veronica , 所以 : if [ veronica != veronica ]
測試條件不成立 . not equal 將不會被顯示出來 .
至於 string , -z string , -n string 則可以測試是否為空字串
看下面的 Shell Script :
#!/bin/sh
myaddr= # myaddr is null
youaddr=taipei # youraddr is not null
if [ -z "$myaddr" ] # 測試條件成立
then
echo 'my string is null'
fi
if [ -n "$myaddr" ] # 測試條件不成立
then
echo 'my string is not null'
fi
if [ -z "$youaddr" ] # 測試條件不成立
then
echo 'your string is null'
fi
if [ -n "$youaddr" ] # 測試條件成立
then
echo 'your string is not null'
fi
執行結果 :
% isstrnull
my string is null
your string is not null
(II) 整數測試
我們曾提過 , 變數並沒有所謂的資料型別 , 那為什麼會有整數
測試呢 ? 這是因為 , 整數測試中 , 變數的內容被當成整數來看
待 . 如 : money=30 , 一般情況下 , money 是放著 30 這個字串
但在整數測試中 , money 被視為整數的 30 .
整數的測試如下 :
int1 -eq int2 # int1 = int2 等於
int1 -ge int2 # int1 >= int2 大於等於
int1 -gt int2 # int1 > int2 大於
int1 -le int2 # int1 <= int2 小於等於
int1 -lt int2 # int1 < int2 小於
int1 -ne int2 # int1 != int2 不等於
我們舉一個例子 :
#!/bin/sh
if [ "$#" -eq 2 ] # 假如參數個數等於 2
then
echo 'The number of arguments is two'
fi
執行結果 :
% digi
% digi a b
The number of arguments is two
% digi a
% digi "abc" 'def'
The number of arguments is two
整數的比較常用在整個 Shell Script 的開頭 , 尤其是那些須要
參數的 Shell Script . 就以前 plus 這個 Shell Script 而言
plus 需要兩個參數 , 那我們便可以在 Shell Script 的一開頭
就檢查使用者所給的參數個數是不是兩個 , 假如是 , 才做兩個
參數相加的動作 , 若參數個數不等於兩個 , 就顯示訊息給使用
者 , 並且脫離 , 不繼續做 , 整個 Shell Script 如下 :
#!/bin/sh
if [ "$#" -ne 2 ] # 假如參數個數不等於 2
then
echo 'plus needs two arguments'
exit # 脫離整個 Shell Script
fi
echo `expr $1 + $2` # 顯示參數 1 加參數 2
# 的值
執行結果 :
% plus2
plus needs two arguments
% plus2 3 5
8
(III) 檔案測試
我們在 Shell Script 中也可以做檔案測試 :
-d file # 檔案是否為一個目錄
-f file # 檔案是否為一個普通檔案
-r file # 檔案是否為可讀
-s file # 檔案長度是否不是 0
-w file # 檔案是否為可寫入
-x file # 檔案是否為可執行
若我們要測試 /etc/passwd 是否存在 , 並且是普通檔案的話 :
if [ -f /etc/passwd ] # 因為 /etc/passwd 存在 , 而且
# 它是一個普通檔案 , 所以測試
# 會成立
if [ -x /etc/passwd ] # 因為 /etc/passwd 不是可執行
# 檔 , 所以測試不會成立
(4.3) 命令測試
實際上 , 命令也可以當作測試的條件 , 這是因為 , 我們每執行
完一個命令 , 總會有傳回值 , 這個傳回值假如是 0 , 則代表
命令執行成功 , 假如為非 0 的值 , 則代表命令執行失敗 .
看看下面的 Shell Script :
#!/bin/sh
if w | grep "^jhhsu" >&-
then
echo $?
fi
if w | grep "^veronica" > /dev/null
then
echo 'veronica is online'
fi
這個 Shell Script 值得好好研究 , 首先 , $? 代表上個命令的
傳回值 . 假如在這個系統內 jhhsu 正在線上 , 而 veronica 並
不在線上 . 那兩次 echo $? 的結果將會是 0 與 1 , 分別代表
成功與不成功 . 還有 , 假如 if 所測試的是命令 , 那並不需要
[ ] 號 . 前面為了方便 , 以及看起來有結構化 , 我把 if 的結構
寫成 :
if [ 測試條件 ]
then
.
.
fi
但實際上 , 原來的結構應該是這種樣子 :
if test_command
then
.
.
fi
也就是說 , if 後接的必須是一個測試命令 , 而不是 [ 測試條件 ]
所以 , 我們得用 test 這個命令來執行我們的測試條件 . 就像下面
if test "$1" -gt 10
但是 , test 測試條件 也可直接換成 [ 測試條件 ] , 所以我就寫
成 if [ 測試條件 ]
下面左右兩邊的 Shell Script 是完全相等的 :
|
#!/bin/sh | #!/bin/sh
|
if [ "$#" -ne 2 ] | if test "$#" -ne 2
then | then
echo 'needs two arguments' | echo 'needs two arguments'
exit | exit
fi | fi
|
echo `expr $1 + $2` | echo `expr $1 + $2`
|
>&- 與 /dev/null 可參考 (2.5.2) , grep 可參考附錄二
(4.4) 組合測試條件的邏輯運算子
各位在看完 (4.3) 後 , 若你原本對管線 , 導向等等的課題有一定
的認識 , 那你已經可以寫出頗為有用的 Shell Script 了 .
在 (4.4) , 我們還要介紹三種邏輯運算子 not , and 與 or
(I) ! 代表反面的邏輯 : not
也許你已經發現了 , 在 (4.2) 中 , 不管是字串的測試或者是整數
的測試 , 都有反面的邏輯存在 :
string1 = string2 # 字串1 與字串2 相等
string1 != string2 # 字串1 與字串2 不相等
int1 -gt int2 # 整數1 大於 整數2
int1 -le int2 # 整數1 小於等於 整數2
但在檔案的測試方面 , 卻沒有反面的邏輯 . 此時 , 我們就要靠
! 來達成反面的測試 , 如 :
if [ -f /etc/passwd ] # /etc/passwd 是普通檔案嗎 ?
if [ ! -f /etc/passwd ] # /etc/passwd 不是普通檔案嗎 ?
當然 , 在字串或整數比較方面 , 你也可以不用原來就已經有的運
算子 , 而改用 ! 以代表反面的邏輯 . 請看下面的 Shell Script
#!/bin/sh
if [ ! "$1" -gt 10 ] # 假如第一個參數的值不大於 10
then
echo 'argument is less than or equal to 10'
fi
執行結果 :
% unary 3
argument is less than or equal to 10
% unary 11
(II) -a 代表邏輯上的 and
講到目前為止 , 我們的比較都僅限於一個表示式 , 像某一變數的
內容是否等於一個字串 , 或是某一變數的內容是否大於一個整數
那假如我們想做如此的測試 : " 一個數字是否大於 10 而且小於
100 , 也就是介於 10 到 100 之間 " 那我們就得用 -a 來達成 .
請看下面的 Shell Script :
#!/bin/sh
if [ "$1" -gt 10 -a "$1" -lt 100 ] # 10 < 參數一 < 100 ?
then
echo '10 < argument < 100'
fi
我們可以把上面 [ ] 中的測試條件看成 :
第一個參數大於10 且 第一個參數小於100
有些人會想 : 我是不是應該用括號來確保運算優先權的順序 , 其實
可以不必 , 因為 -a 比 -gt 或 -lt 的優先權低 , 也就是說 ,
"$1" -gt 10 與 "$1" -lt 100 會分別先運算完後才做 and 的動作 .
假如你有"優良"的寫程式習慣 , 那你仍然可以加上括號 , 只是 ,
要以反斜線來去除括號的特殊意義 , 如下 :
if [ \( "$1" -gt 10 \) -a \( "$1" -lt 100 \) ]
(III) -o 代表邏輯上的 or
與 and 不同的 , or 只要有一項成立就可以了 . 比如說 :
if [ "$1" -le 10 -o "$1" -ge 90 ]
表示測試第一個參數是否符合小於等於10 或 符合大於 90 其中之一
的敘述 . 只要符合其中任一句敘述 , 條件就成立 .
在比較複雜的邏輯中 , or 與 and 有同時存在的可能 . 此時 , 到底
誰的優先權比較高呢 ? 答案是 and , 舉個例子如下 :
if [ "$1" -le 10 -o "$1" -gt 50 -a "$1" -lt 100 ]
^^^^^^^^^^^^^^^^^^^^^^^^^^^
這部份先做
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
再做這部份
假如你想改變 and 與 or 之間的優先權 , 請使用括號 .
(4.5) 更多的 if 流程控制
當我們學完測試條件之後 , 流程控制就很簡單了 , 在 (4.1)
中 , 我們已經看過了 if 的結構 , 現在我們來看看其它的結構
所有的 if 結構都以 fi 做結尾 , 各位可以把 if 至 fi 看成
一個區塊 .
(I) if test_command
then
.
.
fi
這個結構 (4.1) 已提過 , 不再贅述
(II) if test_command
then
.
.
else
.
.
fi
這個結構很簡單 , 假如 test_command 成立 , 那就做 then
到 else 之間的事 ; 假如 test_command 不成立 , 那就做
else 到 fi 之間的事 . 下面是一個簡單的例子 .
#!/bin/sh
if [ "$1" -ge 0 ]
then
echo 'positive'
else
echo 'negative'
fi
執行結果 :
% mytest 10 # 10 大於 0
positive
% mytest -5 # -5 小於 0
negative
(III) if test_command1
then
.
.
elif test_command2
then
.
.
.
elif test_commandn
then
.
.
else
.
.
fi
上面的結構也很簡單 , 假如 test_command1 成立 , 就做
then 與 elif test_command2 之間的事 . 假如 test_command1
不成立 , 就測試 test_command2 看成不成立 , 假如不成立
就繼續測試下去 . 假如所有的測試都不成立 , 就會執行 else
與 fi 之間的事 . 下面是一個典型的 Shell Script :
#!/bin/sh
time=`date | cut -c12-13`
if [ "$time" -gt 0 -a "$time" -lt 12 ]
then
echo 'Good Morning'
elif [ "$time" -ge 12 -a "$time" -lt 17 ]
then
echo 'Good Afternoon'
else
echo 'Good Evening'
fi
首先 , date | cut -c12-13 先執行 , date 輸出目前時間 ,
但我們並不要全部的輸出 , 只要第12及第13個字元 , 也就是
目前是幾點 , 所以我們把 date 的輸出以管線交給 cut 這個
命令去剪下第12及第13個字元 , 然後把這兩個字元設給 time
接著進入條件測試 . 一開始先測試是否零點到十二點之間 ,
假如是 , 就顯示 Good Morning , 並脫離 if 結構 . 假如不
在零點到十二點之間 , 那就看看是不是在十二點到十七點之間
假如是 , 就顯示 Good Afternoon , 並脫離 if 結構 , 假如
又不是在十二點到十七點之間 , 就顯示 Good Evening
(4.6) case 結構
case value in
pattern_1) .
.
.;;
pattern_2)
.
.
.;;
...
pattern_n) .
.
.;;
esac
case 結構是以 case 為開頭 , 以 esac 為結束 . 其中 , 我們
能自由指定的 , 包含 value , pattern 以及若符合 pattern 則
作那些事 . case 的工作原理很簡單 , 它把 value 的值與下面的
pattern 做比較 , 假如遇到符合的 , 就做 pattern 之後的內容
直到遇見一個 ;; 號為止 . 各位一定要注意 , 每個 pattern 結束
都一定要有一個 ;; 號 , 否則會有錯誤 .
value 就是一個變數 , pattern 是一個符合的樣版 . 先看一個
Shell Script , 這個 Shell Script 從參數得到一個數字 , 並把
這個數字轉成相對應的英文 :
#!/bin/sh
if [ "$#" -ne 1 ]
then
echo 'Usage : transnum
exit
fi
case "$1" in
0) echo 'zero';;
1) echo 'one';;
2) echo 'two';;
3) echo 'three';;
4) echo 'four';;
5) echo 'five';;
6) echo 'six';;
7) echo 'seven';;
8) echo 'eight';;
9) echo 'nine';;
*) echo 'must a single digit';;
esac
執行結果 :
% transnum 4
four
% transnum 18
must a single digit
% transnum veronica
must a single digit
在上面的 Shell Script 中 , value 是第一個參數 , 而 pattern
的值分別為 0,1,2,3,4,5,6,7,8,9,* . 所以 , 當執行到 case 這
個結構時 , 第一個參數就與下面一個個 pattern 做比較 , 直到
遇見符合的 pattern , 就做 pattern 後面的事 . 比較特殊的是 ,
pattern 若是 * 號 , 則表示都不符合所要做的事 . 像上面 , 我
們只接受一位數字 , 假如你給與的是兩位數字以上 , 或是字串 ;
既然都不符合 0 到 9 的 pattern , 那就會顯示一段錯誤訊息 .
還有 , pattern 可以比較複雜 , 如下面的格式都是可以的 :
[a-z] <-- 符合小寫字元
[0-9] <-- 符合 0,1,2,3,4,5,6,7,8,9
? <-- 符合任何一個字元
你也可以把兩個 pattern 以 | 做邏輯的 or , 看下面的例子
#!/bin/sh
if [ "$#" -ne 1 ]
then
echo 'need a animal name'
fi
case "$1" in
ant | cat ) echo 'I hate this';;
bird | dog ) echo 'I like this';;
esac
執行結果 :
% testani ant
I hate this
% testani bird
I like this
% testani cat
I hate this
% testani dog
I like this
(4.7) while 結構
while test_command
do
.
.
done
while 結構相當簡單 , 它可說是一種迴圈的型式 , 只要 test_command
成立 , 就會一直做 do 與 done 之間所夾的事 . 下面的 Shell Script
會顯示 0 到 100
#!/bin/sh
var=0 # 把 var 的初始值設為 0
while [ "$var" -le 100 ] # 當 $var 小於等於 100
do
echo -n "$var" # echo -n 為顯示但不換行
var=`expr $var + 1` # 把 var 的值加 1
done
假如我們想無條件作無窮迴圈 , 那你可以看看下面的 Shell Script ,
它會在終端機上從 0 開始印 , 直到你按下 Ctrl-c 為止
#!/bin/sh
var=0
while test 0 # 注意 , test_command 為 test 0
do
echo -n "$var"
var=`expr $var + 1`
done
本來在我們的想法中 , test 後應該是接一個測試條件 , 就像 :
test "$var" -lt 100
test 0 是什麼意思 ? 其實很簡單 , 測試條件假如成立 , 那就
會是 0 , 假如不成立 , 那就會是 1 . 所以 , 像 $var 若小於 100
那麼 , test "$var" -lt 100 就會被換成 test 0 .
所以 , 我們所看到的 :
while test 0 <-- 恆成立 , 所以為一無窮迴圈
do
.
.
done
(4.8) until 結構
until test_command
do
.
.
done
與 while 不同的 , until 後面接的 test_command 要不成立才會
一直做 do 與 done 之間夾的事 , 一但 test_command 成立了 ,
整個 until 結構就結束 .
舉上面的顯示 0 到 100 的例子 , 假如我們要把原來的 while 結構
改成 until 結構 , 那我們的想法必然是 : 一直加 1 , 直到大於
100 為止 . 那整個 Shell Script 如下 :
#!/bin/sh
var=0
until [ "$var" -gt 100 ] # 直到大於 100 為止
do
echo -n "$var"
var=`expr $var + 1`
done
讓我們好好比較 while 與 until :
|
#!/bin/sh | #!/bin/sh
|
var=0 | var=0
|
while [ "$var" -le 100 ] | until [ "$var" -gt 100 ]
do | do
echo -n "$var" | echo -n "$var"
var=`expr $var + 1` | var=`expr $var + 1`
done | done
|
做同樣一件事 , 但是它們的測試條件卻剛好相反 . while 是小於等於
100 , 而 until 卻是大於 100 . 從這裡 , 我們看到了 while 與
until 的互補性 .
until 結構有用的地方是在於 : 它可以用來等待某事件的發生 , 看下
面的 Shell Script :
#!/bin/sh
if [ "$#" -ne 1 ] # 假如參數個數不等於 1
then
echo 'Usage: scanuser username'
exit
fi
until who | grep "^$1" > /dev/null
do
sleep 60
done
echo "$1 has logged on"
上面是一個頗為有用的 Shell Script , 我相信各位可以看得懂前幾列
特別要解釋的是 until 的結構 . 在這個 Shell Script 中 , 我們
想做的事是 : 每 60 秒檢查一次 , 看看某 user 是不是在系統中 .
until who | grep "^$1" > /dev/null
上面這列看起來很複雜 , 其實只要你熟悉命令及導向,管線 , 那實在
沒有什麼神秘的 . 我們知道 , until test_command 是只要
test_command 不成立 , 就一直做 do 與 done 之間的事 .
who | grep "^$1" > /dev/null
把 who 的輸出交給 grep 去抓出看看有沒有第一個參數的人名 , 並
把應該正常輸出的訊息丟給 /dev/null 這個垃圾筒 .
假如沒有第一個參數的人名 , 就執行 sleep 60 ( sleep 60 是暫
停 60 秒的意思 ) , 假如有第一個參數的人名 , 就會脫離 until
結構 , 並且顯示那個人已經簽入系統 .
我們可以把這個 Shell Script 以背景執行 , 你會發覺它真的有用 .
(4.9) for 結構
for var in word1 word2 ..... wordn
do
.
.
done
請各位注意 , Shell Programming 中的 for 與一般程式語言的 for
有很大的不同 . 我們能指定的包含 var , word1 , word2 ......
( word1 word2 .... wordn 為了方便 , 我把它稱為 word list )
以及 do 與 done 之間所做的事 . 我們還是先看一個例子 :
#!/bin/sh
for i in 1 2 peter bob
do
echo "$i"
done
執行結果 :
% fortest
1
2
peter
bob
我們所看到的是 , for 每執行一次 , 就把後面的資料指定給 var
像第一次執行時 , 1 被指定給 i , 第二次執行時 , 2 被指定給 i
第三次執行時 , peter 被指定給 i , 第四次執行時 , bob 被指定
給 i . 好了 , 現在我們把焦點放在 word1 word2 ..... wordn 上
它們是不是有什麼變化呢 ? 在這裡 , 我要提出幾種 :
(I) 使用命令製作出 word list
各位還記得 (3.5.3) 中 mail 給很多人的例子嗎 ?
mail `cat member` < letter
我們知道 , `cat member` 會被換成像 jhhsu veronica peter bob
的型式 , 那不是剛好符合我們的 word list 嗎 ? 因此 , 我把
它以 for 來改寫 :
#!/bin/sh
for person in `cat member`
do
mail $person < letter
done
所以各位可以看到 , 只要命令執行結果會產生 word list , 那怕
是只有一個 word 也好 , 我們都可以把它放在 in 的後面 , 並以
反單引號括起來 . 在這部份 , 運用的巧妙就得看你對命令的熟悉
程度而定了 , 並且可以再用管線組合出更複雜的命令 .
(II) 把參數當成 word list
在這裡要介紹一個新的符號 $@ , 它與 $* 類似 , 都是代表所有
的參數 . 不同的是 , $@ 會把每個參數加上雙引號 . 所以 , 我
們可以想成 : $* 代表 $1 $2 $3 .... 而 $@ 代表 "$1" "$2" "$3"
比較下面兩個 Shell Script :
|
#!/bin/sh | #!/bin/sh
|
for arg in $* | for arg in $@
do | do
echo $arg | echo $arg
done | done
|
|
執行結果 : | 執行結果 :
|
% arg1 'a b' c | % arg2 'a b' c
a | a b
b | c
c |
下面還是以寄信做例子 , 但此時 , 收信人我們不寫在檔案中 , 而
直接由命令列中的參數指定 :
#!/bin/sh
for person in $@
do
mail $person < letter
done
Chap 5 -- 讀取資料 , 呼叫函數 , 偵錯
(5.1) read 用來讀取資料
截至目前為止 , 我們變數的來源有兩個地方 , 一是來自參數 , 一是
來自直接在 Shell Script 中直接指定 . 那變數能不能在 Shell
Script 執行時才讀取呢 ? 此時就要用 read . read 的格式為 :
read variable
它會把讀取到的資料放到 variable 中 . 舉一個小小的例子 :
#!/bin/sh
echo -n 'Input number1 --> '
read number1
echo -n 'Input number2 --> '
read number2
echo `expr $number1 + $number2`
執行結果 :
% plus3
Input number1 --> 5
Input number2 --> 10
15
上面這個 Shell Script 可以由使用者任意輸入兩個變數 , read
分別把它們放到 number1 及 number2 , 最後再把 number1 及
number2 加起來 .
我再舉一個例子 , 你可以用 read 來製做出類似選單的東西 :
#!/bin/sh
cat << End_of_menu
------------------------
| |
| You have 4 choice : |
| |
| 1. plus + |
| |
| 2. minus - |
| |
| 3. multiple * |
| |
| 4. divide / |
| |
------------------------
End_of_menu
echo -n 'Input your choice --> '
read choice
echo -n 'Input number1 -->'
read number1
echo -n 'Input number2 -->'
read number2
case $choice in
1) echo `expr $number1 + $number2`;;
2) echo `expr $number1 - $number2`;;
3) echo `expr $number1 \* $number2`;;
4) echo `expr $number1 / $number2`;;
esac
上面是一個選單式的計算機 , 它可以選擇你要作的運算 , 並讀取
兩個數字來當運算元 . 這個 Shell Script 融合了我們在 (2.5.2)
中的 cat << c 的觀念 , 特別要注意的是 , 在上面的 Shell Script
中 , End_of_menu 一定要獨立一列 , 而且一定要在那列的開頭 .
接著 , read choice 把我們要做的四則運算種類 ( 1 代表加 , 2
代表減 , 3 代表乘 , 4 代表除 ) 放在 choice 中 . 再來 , 讀入
兩個數字來當運算元 . 最後 , 進入一個 case 的結構 . 在 case
中根據 choice 的值來做不同的運算 .
(5.2) 呼叫函數
在程式寫作的型態上 , 結構性的程式的確比雜亂無章的程式要好 .
因此 , 使用函數變成了一項不可或缺的技巧 , 而且可以把整個
程式拆成數個部份分別寫作 . 在 Bourne Shell Programming
中 , 我們也可以使用函數 , 函數的格式如下 :
function_name()
{
command
command
.
.
}
我們把上面加減乘除的例子以函數的形式再寫一遍 :
#!/bin/sh
plus()
{
echo 'Plus Function'
echo `expr $number1 + $number2`
}
minus()
{
echo 'Minus Function'
echo `expr $number1 - $number2`
}
multi()
{
echo 'Multiple Function'
echo `expr $number1 \* $number2`
}
div()
{
echo 'Divide Function'
echo `expr $number1 / $number2`
}
main_select()
{
cat << ' End_of_menu'
------------------------
| |
| You have 4 choice : |
| |
| 1. plus + |
| |
| 2. minus - |
| |
| 3. multiple * |
| |
| 4. divide / |
| |
------------------------
End_of_menu
echo -n 'Input your choice --> '
read choice
echo -n 'Input number1 -->'
read number1
echo -n 'Input number2 -->'
read number2
case $choice in
1)plus;;
2)minus;;
3)multi;;
4)div;;
*)echo 'Invalid choice'
exit;;
esac
}
main_select # 程式從這裡開始
使用這種函數寫作有幾點要注意 :
(I) 下面的情形 , 左邊可以正確的執行 , 右邊卻不行
a_func() | a_func
{ |
b_func | a_func()
} | {
| b_func
b_func() | }
{ |
| b_func()
} | {
|
a_func | }
(II) 命令列下參數的傳遞必須先把這些參數指定給另一個變數
然後才能正確的工作 . 看下面的例子 :
#!/bin/sh
main()
{
echo $1 # error ?
echo $2 # error ?
}
main
上面的例子看起來沒錯 , 但實際上並不能 work , 你可能要
改成下面的樣子 :
#!/bin/sh
main()
{
echo $number1
echo $number2
}
number1=$1
number2=$2
main
(5.3) Shell Script 的偵錯
使用 sh -x shell_script_name 可以對一個 Shell Script 做
偵錯的動作 . 假如你的 Shell Script 不能正常的執行 , 或者是
執行的結果不符合你所預料的 . 出錯的地方可能為下列幾項之一 :
(I) 請檢查 Shell Script 是否有可讀及可執行權限 .
(II) 請注意 , Shell Programming Language 不是完全自由語法
就像 if [ "$#" -ne 1 ] 就不能寫成 if ["$#" -ne 1]
也不能寫成 if[ "$#" -ne 1 ] , 這些要特別注意 . 假如
執行時有錯誤訊息 , 你要詳加檢查程式的語法結構 . 該
空格的地方要有空格 .
(III) 要了解 , $variable 才是變數的內容 , 你很有可能把
variable 當成變數的內容 .
(IV) 還有 , 你可能拼錯字了 , 這是很常見的 .
(V) 有些情況下 , 變數中的內容並不是你預期的 , 以致造成
錯誤 . 此時你可以在程式中可疑的地方適時的加上一些
偵錯點 , 把這些變數內容顯示出來看看 .
(V) 最後 , 邏輯上的錯誤是很難找到的 , 你可能也要注意 .
附錄一 : Shell Summary
-------------------------------------------------------------------
## 變數的指定 :
variable=value
此時 , variable 是被當成字串看待 .
如 : NNTPSERVER=news.csie.nctu.edu.tw
其中 , NNTPSERVER 是 variable ; news.csie.nctu.edu.tw 是 value
## 變數的內容 :
$variable
## 對 shell script 作偵錯執行的動作 :
sh -x [your_shell_script]
如 : sh -x search_string
其中 , search_string 這個 shell script
必需有 read 及 execute 的權限
## shell script 中的特殊符號 :
$# 參數個數
$n 第 n 個參數
$* 所有的參數
$@ 與 $* 一樣 , 除了每個參數都加上 " "
$? 上一個命令傳回的值
$$ 目前此 shell 的 pid // 當你在 script 中要 creat 一個檔 , 但不希望
// 這個檔案名是固定時 ( 尤其是有兩個人同時使用
// 這個 shell script ) 總不能 creat 的檔名取成
// 一樣 , 這時候 , $$ 就可派上用場
## 對整數作比較的運算子 :
int1 -eq int2 // int1 = int2
int1 -ge int2 // int1 >= int2
int1 -gt int2 // int1 > int2
int1 -le int2 // int1 <= int2
int1 -lt int2 // int1 < int2
int1 -ne int2 // int1 != int2
## 對字串作比較的運算子 :
string1 =string2
string1 !=string2
string // string is not null
-n string // string is not null
-z string // string is null
## 對檔案作測試的運算子 :
-d file // if file is a directory
-f file // if file is a ordinary file
-r file // if file is readonly
-s file // if file has nonzore length
-w file // if file is writeable
-x file // if file is excuteable
## 邏輯運算子 :
! // negation
-a // logic AND
-o // logic OR
## 讀取使用者輸入的資料 :
read variable
## 迴圈 , 反覆執行 :
(1) while :
while test_command
do
command
command
.
.
done
(2) until :
until test_command
do
command
command
.
.
done
(3) for :
for var in word1 word2 ..... wordn
do
command
command
.
.
done
## 選擇性條件 :
(1) case :
case value in
pattern_1) command
command
.
.
command;;
pattern_2) command
.
.
command;;
...
pattern_n) command
.
.
command;;
esac
(2) if :
型式一 :
if test_command
then
command
command
.
.
fi
型式二 :
if test_command
then
command
command
.
.
else
command
command
fi
型式三 :
if test_command1
then
command
.
.
elif test_command2
then
command
.
.
elif test_command3
then
command
.
.
else
command
.
.
fi
## special two command logic :
cmd1 && cmd2 // cmd2 is excuted when cmd1 is true (return 0)
cmd1 || cmd2 // cmd2 is excuted when cmd1 is false
Example :
---------------------- 第一個參數與字串比較 -----------------
#!/bin/sh
echo $1
if [ "$1" = 'y' ]
then
echo 'Yeah! You are right!'
fi
if [ "$1" = 'n' ]
then
echo 'Ooop! You are wrong!'
fi
執行結果 :
%scom y
y
Yeah! You are right!
%scom n
n
Ooop! You are wrong!
---------------------- Del_From_Inode -----------------------
#!/bin/sh
if [ "$#" -ne 1 ]
then
echo 'Usage : del_from_inode
exit 1
fi
find . -inum "$1" -ok rm '{}' \;
---------------------- 字串比較的例子 -----------------------
#!/bin/sh
if [ "$#" -lt 1 ]
then
echo 'again!'
exit 1
fi
if [ "$1" = 'lala' ]
then
echo 'lala is haha'
else
echo 'unknown!!!'
fi
執行結果 :
%para3
again!
%para3 lala
lala is haha
%para3 dada
unknown!!!
--------------- 計算某一目錄底下有多少檔案或目錄 ------------
#!/bin/sh
if [ "$#" -ne 2 ]
then
echo 'Usage : count_report
exit
fi
case "$2" in
f)
file=0
for f in `find $1 -depth 1 -type f`
do
file=`expr ${file} + 1`
done
echo "Total files is ${file}"
;;
d)
directory=0
for f in `find $1 -depth 1 -type d`
do
directory=`expr ${directory} + 1`
done
directory=`expr ${directory} - 1`
echo "Total directories is ${directory}"
;;
esac
---------- 一個比較長的例子 ( 可執行但功能並不完整 :p ) -----------
#!/bin/sh
COMPILER=cc
pause_until_enter()
{
echo
echo 'Press enter to continue ......'
read enter_key
}
exit_shell_script()
{
sleep 1
clear
exit
}
error_handle()
{
if [ $? -ne 0 ]
then
echo "***** Some error occur , Please check ~/$$.err *****"
echo -n "Display ~/$$.err now ? (y/n) --> "
read error_select
if [ $error_select = 'y' ]
then
cat ~/$$.err | more
pause_until_enter
case $main_select in
1) modify_now;;
esac
fi
fi
}
modify_now()
{
echo -n 'Modify your source code now ? (y/n) --> '
read modify_now
if [ $modify_now = 'y' ]
then
$EDITOR $source_code_name
fi
}
read_source_code_name()
{
echo -n 'Enter source codes name --> '
read source_code_name
}
read_out_exe_file_name()
{
echo -n 'Input executable filename , Press enter for a.out --> '
read out_exe_file_name
if [ "$out_exe_file_name" = '' ]
then
echo 'Use default name : a.out'
fi
}
read_library_name()
{
echo -n 'Enter your library name --> '
read library_name
}
read_obj_name()
{
echo -n 'Enter your obj name --> '
read obj_name
}
build_exe()
{
read_source_code_name
read_out_exe_file_name
echo -n 'Link library? (y/n) --> '
read link_lib_choice
if [ "$link_lib_choice" = 'y' ]
then
echo -n 'Enter library name --> '
read lib_name
else
lib_name=
fi
echo -n 'Creat symbol table for debugger? (y/n) --> '
read debug_choice
if [ "$debug_choice" = 'y' ]
then
$COMPILER -g -o ${outfile}.out $source_code_name $lib_name > ~/$$.err 2>&1
error_handle
else
$COMPILER -o ${outfile}.out $source_code_name $lib_name > ~/$$.err 2>&1
error_handle
fi
}
compile_only()
{
read_source_code_name
$COMPILER -c $source_code_name
}
list_lib_content()
{
read_library_name
ar t $library_name | more
pause_until_enter
}
add_obj_into_lib()
{
read_library_name
read_obj_name
if [ -f $library_name ]
then
ar q $library_name $obj_name
fi
}
read_config()
{
sed -n 1,1p comenv.conf
pause_until_enter
}
main_menu()
{
clear
cat << ' main_menu_end'
Main Menu
------------------------------
1. Compile Source Code
2. Library Maintain
3. Execute File
4. Configuration
5. Exit
main_menu_end
echo -n 'Enter your choice --> '
read main_select
case $main_select in
0);;
1) compile_menu;;
2) maintain_menu;;
3) execute_menu;;
4) config_menu;;
5) exit_shell_script;;
*) main_menu;;
esac
}
compile_menu()
{
clear
cat << ' compile_menu_end'
Compile Menu
------------------------------
1. Build Excutable File
2. Compile Only
3. Back To Main Menu
4. Exit
compile_menu_end
echo -n 'Enter your choice --> '
read compile_select
case $compile_select in
1) build_exe;;
2) compile_only;;
3) main_menu;;
4) exit_shell_script;;
*) compile_menu;;
esac
}
maintain_menu()
{
clear
cat << ' maintain_menu_end'
Library Maintain
------------------------------
1. List library content
2. Add objfile into library
3. Remove objfile from library
4. Back to Main Menu
5. Exit
maintain_menu_end
echo -n 'Enter your choice --> '
read maintain_select
case $maintain_select in
1) list_lib_content;;
2) add_obj_into_lib;;
3) remove_obj_from_lib;;
4) main_menu;;
5) exit_shell_script;;
esac
}
execute_menu()
{
echo -n 'Enter execute file name --> '
read execute_file_name
$execute_file_name
pause_until_enter
}
config_menu()
{
clear
cat << ' config_menu_end'
Configuration Menu
------------------------------
1.View Configuration File
2.Change Configuration
config_menu_end
echo -n 'Enter your choice --> '
read config_select
case $config_select in
1) read_config;;
2);;
esac
}
while test 0
do
main_menu
done
附錄二 : grep 簡介
-------------------------------------------------------------------
grep -- 從檔案每一列中找出特定的樣板格式
grep [options] regular_expressions [files]
options :
-b 印出符合的那一列是在整個檔案中的第幾個 byte .
-c 只印出共有多少列符合 regular_expressions .
-h 當我們從許多檔案中找尋適合的條件時 , 假如找到了 , 就會印出是在
那個檔案中找到的 . 然而 , 假如我們加了 -h 這個 option . 那麼 ,
輸出的結果就不會告訴我們 , 是在那個檔案中找到符合的樣板 .
-i 忽略 regular_expressions 中大小寫的差別 .
-l 只印出在那個檔案中有找到 , 而不印出檔案中符合的樣版 .
-n 印出符合的樣板是在檔案中的第幾列 .
-s 抑制錯誤訊息 , 像不存在的檔案啦 , 或者是不能讀的檔案 .
-v 印出不符合 regular_expressions 的那些列 .
example :
從 test1 這個檔案中 , 找出含有 app 的列 :
% grep 'app' test1
從 passwd 這個檔案中找出到底有多少人使用 tcsh :
% grep -c '/bin/tcsh' /etc/passwd
從目前的目錄下 , 找出任一列開頭具有 #include 的檔案 :
% grep -l '^#include' *
從 test2 這個檔案中 , 找出那些不含 class 的列 :
% grep -v 'class' test2
列出不含 pattern 的檔案 :
% grep -c pattern files | grep :0 | cut -d":" -f1
regular expressions 正規表示式
-------------------------------------------------------------
. 可符合任意一個字元 ( 包含空白字元 )
[] 可符合方括弧中列舉的字元
[^ regular_expressions] 不符合 regular_expression
* 與零個或更多個 * 前的字元吻合
.* 符合零或更多個字元
^regular_expressions 符合在每一列開頭的 regular_expressions
下面是 regular expressions 的一些例子
regular expressions match
---------------------------------------------------------------
ring ring , spring , ringing , stringing
^^^^ ^^^^ ^^^^ ^^^^
.alk walk , talk , alk , skialk
^^^^ ^^^^ ^^^^ ^^^^
[bB]ill bill , Bill , biller
^^^^ ^^^^ ^^^^
number[5-9] number7 , number90
^^^^^^^ ^^^^^^^
[^ a-z] a21 , c1 , 123
^^ ^ ^^^
ab* acc , ab , abb , abc
^^^ ^^ ^^^ ^^^
ab.* ab , abb , abbc
^^ ^^^ ^^^^
^T 符合以 T 為開頭的每一列 ; This line
^
參考資料 :
----------------------------------------------------------------
Shell :
1. Title: Unix Shell Programming
Authors: Stephen Kochan and Patrick Wood
Publisher: Hayden
Edition: 1990
ISBN: 0-672-48448-X
2. Title: The Unix C Shell Field Guide
Authors: Gail Anderson and Paul Anderson
Publisher: Prentice Hall
Edition: 1986
ISBN: 0-13-937468-X
General Unix Texts :
1. Title: Unix Power Tools
Authors: Jerry Peek, Tim O'Reilly and Mike Loukides (and other
Publisher: O'Reilly / Bantam contributors)
Edition: 1993
ISBN: 0-553-35402-7
2. Title: Unix in a Nutshell
Authors: Daniel Gilly and O'Reilly staff
Publisher: O'Reilly
Edition: 2nd ed. 1992 (for System V and Solaris 2)
ISBN: 1-56592-001-5
Programming :
1. Title: Advanced Programming in The Unix Environment
Author: Richard Stevens
Publisher: Addison-Wesley
Edition: 1992
ISBN: 0-201-56317-7
Other :
1. UNIX FAQ
如何聯絡作者 :
地址 : 交通大學十舍315R
E-mail : jhhsu@csie.nctu.edu.tw
u8217017@cc.nctu.edu.tw
假如您發現文章中有任何錯誤之處 , 請通知作者 .
還有 , 讀者的建議與批評也非常的歡迎 .


