http://www.freebsd.org.hk/html/other/shell1.html
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 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 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 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 file2
ls -la /etc >> file2 file2
這個型式是由兩個導向符號所組成 , 看起來有點複雜 , 其實一點也
不 . 只要你拆成兩部份來看 : prog file2
^^^^^^^^^^^^ (i)
^^^^^^^^^^^^^^^^^^^^ (ii)
第 (i) 部份 : 把 file1 當作是 prog 的標準輸入
第 (ii) 部份 : 把第 (i) 部份的輸出結果 , 導向至 file2 中
舉個例子 :
tr 'a-z' 'A-Z' dest
首先 , tr 'a-z' 'A-Z' dest 的話 , 我們會在
終端機上看到轉換後的輸出 . 然而 , 我們使用 > dest 再把原來應
該在終端機上的輸出導向到 dest 這個檔案中 . 所以 , dest 就會
是 sour 經過轉換後的內容 .
(VIII) prog this is a test
> but ....
> End_Of_Letter 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 [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 '> > > ?"
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 ] = int2 大於等於
int1 -gt int2 # int1 > int2 大於
int1 -le int2 # int1 &-
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 '
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] /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` '
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 '
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 '
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 '
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 '
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 '
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 '
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 '
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
假如您發現文章中有任何錯誤之處 , 請通知作者 .
還有 , 讀者的建議與批評也非常的歡迎 .
本文来自ChinaUnix博客,如果查看原文请点:http://blog.chinaunix.net/u/12857/showart_230865.html
|