shell學習資料:shell十三問

SHELL十三問
2008125163340
    
   
SHELL十三問之五:var=value?export 前後差在哪?
文章整理: 文章來源: 網絡
這次讓我們暫時丟開command line,先來了解一下bash變量(variable)吧...
所謂的變量,就是就是利用一個特定的"名稱"(name)來存取一段可以變化的"值"(value)。
*設定(set)*
在bash中,你可以用"="來設定或重新定義變量的內容:
        name=value
在設定變量的時侯,得遵守如下規則:
        *等號左右兩邊不能使用區隔符號(IFS),也應避免使用shell的保留字符(meta charactor)。
        *變量名稱不能使用$符號。
        *變量名稱的第一個字母不能是數字(number)。
        *變量名稱長度不可超過256個字母。
        *變量名稱及變量值之大小寫是有區別的(case sensitive)。
如下是一些變量設定時常見的錯誤:
        A= B       :不能有IFS
        1A=B       :不能以數字開頭
        $A=B       :名稱不能有$
        a=B       :這跟a=b是不同的
如下則是可以接受的設定:
        A=" B"       :IFS被關閉了(請參考前面的quoting章節)
        A1=B       :並非以數字開頭
        A=$B       :$可用在變量值內
        This_Is_A_Long_Name=b       :可用_連接較長的名稱或值,且大小寫有別。
*變量替換(substitution)*
Shell之所以強大,其中的一個因素是它可以在命令行中對變量作替換(substitution)處理。
在命令行中使用者可以使用$符號加上變量名稱(除了在用=號定義變量名稱之外),
將變量值給替換出來,然後再重新組建命令行。
比方:
        $ A=ls
        $ B=la
        $ C=/tmp
        $ $A -$B $C
(注意:以上命令行的第一個$是shell prompt,並不在命令行之內。)
必需強調的是,我們所提的變量替換,只發生在command line上面。(是的,讓我們再回到command line吧�u)
仔細分析最後那行command line,不難發現在被執行之前(在輸入CR字符之前),
$符號會對每一個變量作替換處理(將變量值替換出來再重組命令行),最後會得出如下命令行:
        ls -la /tmp
還記得第二章我請大家"務必理解"的那兩句嗎?若你忘了,那我這裏再重貼一遍:
若從技術細節來看,shell會依據IFS(Internal Field Seperator)將command line所輸入的文字給拆解爲"字段"(word)。
然後再針對特殊字符(meta)先作處理,最後再重組整行command line。
這裏的$就是command line中最經典的meta之一了,就是作變量替換的�u
在日常的shell操作中,我們常會使用echo命令來查看特定變量的值,例如:
        $ echo $A -$B $C
我們已學過,echo命令只單純將其argument送至"標準輸出"(STDOUT,通常是我們的熒幕)。
所以上面的命令會在熒幕上得到如下結果:
http://www.818198.com  Page 1
SHELL十三問
        ls -la /tmp
這是由於echo命令在執行時,會先將$A(ls)、$B(la)、跟$C(/tmp)給替換出來的結果。
利用shell對變量的替換處理能力,我們在設定變量時就更爲靈活了:
        A=B
        B=$A
這樣,B的變量值就可繼承A變量"當時"的變量值了。
不過,不要以"數學羅輯"來套用變量的設定,比方說:
        A=B
        B=C
這樣並不會讓A的變量值變成C。
上面是單純定義了兩個不同名稱的變量:A與  B,它們的值分別是B與  C。再如:
        A=B
        B=$A
        A=C
同樣也不會讓B的值換成C。
 
若變量被重複定義的話,則原有舊值將被新值所取代。(這不正是"可變的量"嗎?  ^_^)
當我們在設定變量的時侯,請記着這點:
        *用一個名稱儲存一個數值
僅此而已。
此外,我們也可利用命令行的變量替換能力來"擴充"(append)變量值:
        A=B:C:D
        A=$A:E
這樣,第一行我們設定A的值爲"B:C:D",然後,第二行再將值擴充爲"A:B:C:E"。
上面的擴充範例,我們使用區隔符號( : )來達到擴充目的,
要是沒有區隔符號的話,如下是有問題的:
        A=BCD
        A=$AE
因爲第二次是將A的值繼承$AE的提換結果,而非$A再加E�u
要解決此問題,我們可用更嚴謹的替換處理:
        A=BCD
        A=${A}E
上例中,我們使用{}將變量名稱的範圍給明確定義出來,
如此一來,我們就可以將A的變量值從BCD給擴充爲BCDE。
(提示:關於${name}事實上還可做到更多的變量處理能力,這些均屬於比較進階的變量處理,現階段暫時不介紹了,請大
家自行參考數據。如CU的貼子:
http://www.chinaunix.net/forum/viewtopic.php?t=201843)
* export *
嚴格來說,我們在當前shell中所定義的變量,均屬於"本地變量"(local variable),
只有經過export命令的"輸出"處理,才能成爲環境變量(environment variable):
        $ A=B
        $ export A
或:
        $ export A=B
經過export輸出處理之後,變量A就能成爲一個環境變量供其後的命令使用。
在使用export  的時侯,請別忘記shell在命令行對變量的"替換"(substitution)處理,
比方說:
        $ A=B
http://www.818198.com  Page 2
SHELL十三問
        $ B=C
        $ export $A
上面的命令並未將A輸出爲環境變量,而是將B作輸出,
這是因爲在這個命令行中,$A會首先被提換出B然後再"塞回"作export的參數。
要理解這個export,事實上需要從process的角度來理解才能透徹。
我將於下一章爲大家說明process的觀念,敬請留意。
*取消變量*
要取消一個變量,在bash中可使用unset命令來處理:
        unset A
與export一樣,unset命令行也同樣會作變量替換(這其實就是shell的功能之一),
因此:
        $ A=B
        $ B=C
        $ unset $A
事實上所取消的變量是B而不是A。
此外,變量一旦經過unset取消之後,其結果是將整個變量拿掉,而不僅是取消其變量值。
如下兩行其實是很不一樣的:
        $ A=
        $ unset A
第一行只是將變量A設定爲"空值"(null  value),但第二行則讓變量A不在存在。
雖然用眼睛來看,這兩種變量狀態在如下命令結果中都是一樣的:
        $ A=
        $ echo $A
        $ unset A
        $ echo $A
請學員務必能識別null value與unset的本質區別,這在一些進階的變量處理上是很嚴格的。
比方說:
        $ str=                #設爲null
        $ var=${str=expr}        #定義var
        $ echo $var
      
        $ echo $str
      
        $ unset str        #取消
        $ var=${str=expr}        #定義var
        $ echo $var
        expr
        $ echo $str
        expr
聰明的讀者(yes, you!),稍加思考的話,
應該不難發現爲何同樣的var=${str=expr}在null與unset之下的不同吧?
若你看不出來,那可能是如下原因之一:
a.你太笨了
b.不瞭解  var=${str=expr}       這個進階處理
c.對本篇說明還沒來得及消化吸收
e.我講得不好
不知,你選哪個呢?....  ^_^
 
http://www.818198.com  Page 3
SHELL十三問
再來解釋一下var=${str=expr}:
首先,var=$str這個大家都可理解吧。
而接下來的思考方向是,究竟$str這個變量是如下哪一種情況呢:
1) unset
2) null
3) not null
1)假如是unset,那麼var=${str=expr}的結果將是:
var=expr
str=expr
2)假如是null,那var=${str=expr}的結果是:
var=
str=
3)假如是not null (比方爲xyz ),那var=${str=expr}之結果是:
var=xyz
str=xyz
測試如下:
$ showvar() {
>    var=${str=expr}
>    echo \$var is $var
>    echo \$str is $str
> }
$ unset str
$ showvar
$var is expr
$str is expr
$ str=
$ showvar
$var is
$str is
$ str=xyz
$ showvar
$var is xyz
$str is xyz
 
接下來,再來看看var=${str:=expr}好了:
1) $str爲not set:
var=expr
str=expr
2) $str爲null:
var=expr
str=expr
3) $str爲not null (str=xyz):
var=xyz
str=xyz
測試如下:
$ showvar() {
> var=${str:=expr}
> echo \$var is $var
http://www.818198.com  Page 4
SHELL十三問
> echo \$str is $str
> }
$ unset str
$ showvar
$var is expr
$str is expr
$ str=
$ showvar
$var is expr
$str is expr
$ str=xyz
$ showvar
$var is xyz
$str is xyz
最後比教一下${str=expr}與${str:=expr}:
*兩者在not set與not null都一致
*但當null值時,${str=expr}會將$var與$str都設爲null,但${str:=expr}則設爲expr
 
從這個再延伸出其它模擬,不防請大家"實作"觀查一下有何不同?
var=${str-expr} vs var=${str:-expr}
var=${str+expr} vs var=${str:+expr}
var=${str?expr} vs var=${str:?expr}
 
$ showvar() {
>    var=${str-expr}
>    echo \$var is $var
>    echo \$str is $str
> }
$ unset str
$ showvar
$var is expr
$str is
$ str=
$ showvar
$var is
$str is
$ str=xyz
$ showvar
$var is xyz
$str is xyz
 
$ showvar() {
>    var=${str:-expr}
>    echo \$var is $var
>    echo \$str is $str
> }
$ unset str
$ showvar
http://www.818198.com  Page 5
SHELL十三問
$var is expr
$str is
$ str=
$ showvar
$var is expr
$str is
$ str=xyz
$ showvar
$var is xyz
$str is xyz
 
可以看出來${str-expr}與${str:-expr}
*兩者在$str爲not set與not null都一致
*但當null值時,${str-expr}會將$var與$str都設爲null,但${str:-expr}則將$var設爲expr
 
$ showvar() {
>    var=${str+expr}
>    echo \$var is $var
>    echo \$str is $str
> }
$ unset str
$ showvar
$var is
$str is
$ str=
$ showvar
$var is expr
$str is
$ str=xyz
$ showvar
$var is expr
$str is xyz
$ showvar() {
>    var=${str:+expr}
>    echo \$var is $var
>    echo \$str is $str
> }
$ unset str
$ showvar
$var is
$str is
$ str=
$ showvar
$var is
$str is
$ str=xyz
$ showvar
$var is expr
http://www.818198.com  Page 6
SHELL十三問
$str is xyz
可以看出來${str+expr}與${str:+expr}
*兩者在$str爲not set與not null都一致
*但當null值時,${str+expr}會將$var與$str都設爲expr,但${str:+expr}則將$var設爲null
 
$ showvar() {
>    var=${str?expr}
>    echo \$var is $var
>    echo \$str is $str
> }
$ unset str
$ showvar
expr
$ str=
$ showvar
$var is
$str is
$ str=xyz
$ showvar
$var is xyz
$str is xyz
$ showvar() {
>    var=${str:?expr}
>    echo \$var is $var
>    echo \$str is $str
> }
$ unset str
$ showvar
expr
$ str=
$ showvar
expr
$ str=xyz
$ showvar
$var is xyz
$str is xyz
可以看出來${str?expr}與${str:?expr}
*兩者在$str爲not set與not null都一致
*但當null值時,${str?expr}會將$var與$str都設爲null,但${str:?expr}則將$var設爲expr
綜上所述:
$var=${str=expr}與$var=${str:=expr}
$var=${str-expr}與$var=${str:-expr}
$var=${str+expr}與$var=${str:+expr}
$var=${str?expr}與$var=${str:?expr}
* 兩者在 $str 爲not set 與 not null 都一致
* 但當 null 值時不一致
分別描述如下:
$var=${str:-expr}的功能:
http://www.818198.com  Page 7
SHELL十三問
當str非空的時候var賦值爲str,否則(包括not set和null)將var賦值爲expr
$var=${str-expr}的功能:
當str非空的時候var賦值爲str,爲空的時候var設置爲null,not set的時候將var賦值爲expr
 
$var=${str:=expr}的功能:
當str非空的時候var賦值爲str,否則(包括not set和null)將var和str均賦值爲expr
$var=${str=expr}的功能:
當str非空的時候var賦值爲str,爲空的時候var設置爲null,not set的時候將var和str均賦值爲expr
 
$var=${str:?expr}的功能:
當str非空的時候var賦值爲expr,否則將expr寫入標準錯誤後退出
$var=${str?expr}的功能:
當str非空的時候var賦值爲expr,爲空的時候var設置爲null,not set的時候將expr寫入標準錯誤後退出
 
$var=${str:+expr}的功能:
當str非空的時候var賦值爲expr,否則將不做任何操作
$var=${str+expr}的功能:
當str非空的時候var賦值爲expr,爲空的時候var設置爲expr,not set的時候不做任何操作
 
看來還是帶有冒號的簡單,只有兩種情況:非空和其他,呵呵
[]    
   
SHELL十三問之六:exec 跟 source 差在哪?
文章整理: 文章來源: 網絡
這次先讓我們從CU Shell版的一個實例貼子來談起吧:
例中的提問是:
cd /etc/aa/bb/cc可以執行,但是把這條命令寫入shell時shell不執行!
這是什麼原因呀!
我當時如何回答暫時別去深究,先讓我們瞭解一下進程(process)的觀念好了。
首先,我們所執行的任何程序,都是由父進程(parent process)所產生出來的一個子進程(child process),子進程在結束後,
將返回到父進程去。此一現像在Linux系統中被稱爲 fork。
當子進程被產生的時候,將會從父進程那裏獲得一定的資源分配、及(更重要的是)繼承父進程的環境�u
讓我們回到上一章所談到的"環境變量"吧:
*所謂環境變量其實就是那些會傳給子進程的變量。
簡單而言,"遺傳性"就是區分本地變量與環境變量的決定性指標。
然而,從遺傳的角度來看,我們也不難發現環境變量的另一個重要特徵:
*環境變量只能從父進程到子進程單向繼承。換句話說:在子進程中的環境如何變更,均不會影響父進程的環境。
接下來,再讓我們瞭解一下命令腳本(shell script)的概念。
所謂的shell script講起來很簡單,就是將你平時在shell prompt後所輸入的多行command line依序寫入一個文件去而已。
其中再加上一些條件判斷、互動界面、參數運用、函數調用、等等技巧,得以讓script更加"聰明"的執行,但若撇開這些技
巧不談,我們真的可以簡單的看成script只不過依次執行預先寫好的命令行而已。
再結合以上兩個概念(process + script),那應該就不難理解如下這句話的意思了:
*正常來說,當我們執行一個shell script時,其實是先產生一個sub-shell的子進程,然後sub-shell再去產生命令行的子進程。
然則,那讓我們回到本章開始時所提到的例子再從新思考:
cd /etc/aa/bb/cc可以執行,但是把這條命令寫入shell時shell不執行!
這是什麼原因呀!
我當時的答案是這樣的:
因爲,一般我們跑的shell script是用subshell去執行的。
http://www.818198.com  Page 8
SHELL十三問
從process的觀念來看,是parent process產生一個child process去執行,當child結束後,會返回parent,但parent的環境是不會
因child的改變而改變的。
所謂的環境元數很多,凡舉effective id, variable, workding dir等等...
其中的workding dir ($PWD)正是樓主的疑問所在:
當用subshell來跑script的話,sub shell的$PWD會因爲cd而變更,
但當返回primary shell時,$PWD是不會變更的。
能夠了解問題的原因及其原理是很好的,但是?如何解決問題恐怕是我們更感興趣的�u是吧?^_^
那好,接下來,再讓我們瞭解一下source命令好了。
當你有了fork的概念之後,要理解source就不難:
*所謂source就是讓script在當前shell內執行、而不是產生一個sub-shell來執行。
由於所有執行結果均於當前shell內完成,若script的環境有所改變,當然也會改變當前環境了�u
因此,只要我們要將原本單獨輸入的script命令行變成source命令的參數,就可輕易解決前例提到的問題了。
比方說,原本我們是如此執行  script的:
./my.script
現在改成這樣即可:
source ./my.script
或:
. ./my.script
說到這裏,我想,各位有興趣看看/etc底下的衆多設定文件,應該不難理解它們被定義後,如何讓其它script讀取並繼承了
吧?
若然,日後你有機會寫自己的script,應也不難專門指定一個設定文件以供不同的script一起"共享"了...  ^_^
okay,到這裏,若你搞得懂fork與source的不同,那接下來再接受一個挑戰:
----那exec又與source/fork有何不同呢?
哦...要了解exec或許較爲複雜,尤其扯上File Descriptor的話...
不過,簡單來說:
* exec也是讓script在同一個進程上執行,但是原有進程則被結束了。
也就是簡而言之:原有進程會否終止,就是exec與source/fork的最大差異了。
嗯,光是從理論去理解,或許沒那麼好消化,不如動手"實作+思考"來的印像深刻哦。
下面讓我們寫兩個簡單的script,分別命令爲1.sh及2.sh:
1.sh
#!/bin/sh
A=B
echo "PID for 1.sh before exec/source/fork:$$"
export A
echo "1.sh: \$A is $A"
case $1 in
        exec)
                echo "using exec..."
                exec ./2.sh ;;
        source)
                echo "using source..."
                . ./2.sh ;;
        *)
                echo "using fork by default..."
                ./2.sh ;;
esac
echo "PID for 1.sh after exec/source/fork:$$"
echo "1.sh: \$A is $A"
http://www.818198.com  Page 9
SHELL十三問
2.sh
#!/bin/sh
echo "PID for 2.sh: $$"
echo "2.sh get \$A=$A from 1.sh"
A=C
export A
echo "2.sh: \$A is $A"
然後,分別跑如下參數來觀察結果:
$ ./1.sh fork
PID for 1.sh before exec/source/fork:531
1.sh: $A is B
using fork by default...
PID for 2.sh:532
2.sh get $A=B from 1.sh
2.sh: $A is C
PID for 1.sh after exec/source/fork:531
1.sh: $A is B
 
$ ./1.sh source
PID for 1.sh before exec/source/fork:533
1.sh: $A is B
using source...
PID for 2.sh:533
2.sh get $A=B from 1.sh
2.sh: $A is C
PID for 1.sh after exec/source/fork:533
1.sh: $A is C
 
$ ./1.sh exec
PID for 1.sh before exec/source/fork:537
1.sh: $A is B
using exec...
PID for 2.sh:537
2.sh get $A=B from 1.sh
2.sh: $A is C
############
echo "PID for 1.sh after exec/source/fork:$$"
echo "1.sh: \$A is $A"
已經不會執行了,1.sh的進程已經沒了。
##############
[]    
   
SHELL十三問之七:( ) 與 { } 差在哪?
文章整理: 文章來源: 網絡
先說一下,爲何要用( )或{ }好了。
許多時候,我們在shell操作上,需要在一定條件下一次執行多個命令,也就是說,要麼不執行,要麼就全執行,而不是每
次依序的判斷是否要執行下一個命令。或是,需要從一些命令執行優先次順中得到豁免,如算術的2*(3+4)那樣...
這時候,我們就可引入"命令羣組"(command group)的概念:將多個命令集中處理。
http://www.818198.com  Page 10
SHELL十三問
在shell command line中,一般人或許不太計較( )與{ }這兩對符號的差異,雖然兩者都可將多個命令作羣組化處理,但若從
技術細節上,卻是很不一樣的:
( )將command group置於sub-shell去執行,也稱nested sub-shell。
{ }則是在同一個shell內完成,也稱爲non-named command group。
如果你對上一章的fork與source的概念還記得的話,那就不難理解兩者的差異了。
要是在command group中扯上變量及其它環境的修改,我們可以根據不同的需求來使用( )或{ }。通常而言,若所作的修改
是臨時的,且不想影響原有或以後的設定,那我們就nested sub-shell,反之,則用non-named command group。
是的,光從command line來看,( )與{ }的差別就講完了,夠輕鬆吧~~~  ^_^
然而,若這兩個meta用在其它command meta或領域中(如Regular Expression),還是有很多差別的。只是,我不打算再去說
明瞭,留給讀者自己慢慢發掘好了...
我這裏只想補充一個概念,就是function。
所謂的function,就是用一個名字去命名一個  command group,然後再調用這個名字去執行command group。從non-named
command group來推斷,大概你也可以猜到我要說的是{ }了吧?
在bash中,function的定義方式有兩種:
方式一:
function function_name {
  command1
  command2
  command3
  ....
}
方式二:
fuction_name () {
  command1
  command2
  command3
  ....
}
用哪一種方式無所謂,只是若碰到所定義的名稱與現有的命令或別名(Alias)衝突的話,方式二或許會失敗。
但方式二起碼可以少打function這一串英文字母,對懶人來說(如我),又何樂不爲呢?...  ^_^
function在某一程度來說,也可稱爲"函式",但請不要與傳統編程所使用的函式(library)搞混了,畢竟兩者差異很大。
惟一相同的是,我們都可以隨時用"已定義的名稱"來調用它們...
若我們在shell操作中,需要不斷的重複執行某些命令,我們首先想到的,或許是將命令寫成命令稿(shell script)。不過,我
們也可以寫成function,然後在command line中打上function_name就可當一舨的script來使用了。只是若你在shell中定義的
function,除了可用unset function_name取消外,一旦退出shell,function也跟着取消。
然而,在script中使用function卻有許多好處,除了可以提高整體script的執行效能外(因爲已被加載),還可以節省許多重複
的代碼...
簡單而言,若你會將多個命令寫成script以供調用的話,那,你可以將function看成是script中的script ...  ^_^
而且,透過上一章介紹的source命令,我們可以自行定義許許多多好用的function,再集中寫在特定文件中,然後,在其它
的script中用source將它們加載並反覆執行。
若你是RedHat Linux的使用者,或許,已經猜得出/etc/rc.d/init.d/functions這個文件是作啥用的了~~~  ^_^
[]    
   
SHELL十三問之八:$(( )) 與 $( ) 還有${ } 差在哪?
文章整理: 文章來源: 網絡
我們上一章介紹了( )與{ }的不同,這次讓我們擴展一下,看看更多的變化:$( )與${ }又是啥玩意兒呢?
在bash shell中,$( )與` ` (反引號)都是用來做命令替換用(command substitution)的。
所謂的命令替換與我們第五章學過的變量替換差不多,都是用來重組命令行:
http://www.818198.com  Page 11
SHELL十三問
*完成引號裏的命令行,然後將其結果替換出來,再重組命令行。
例如:
$ echo the last sunday is $(date -d "last sunday" +%Y-%m-%d)
如此便可方便得到上一星期天的日期了...
上例是在linux下,在FreeBSD下應該用下面的:
echo the last Sunday is `date �Cv Sunday +%Y%m%d`
在操作上,用$( )或` `都無所謂,只是我"個人"比較喜歡用$( ),理由是:
1,  ` `很容易與' ' (單引號)搞混亂,尤其對初學者來說。
有時在一些奇怪的字形顯示中,兩種符號是一模一樣的(直豎兩點)。
當然了,有經驗的朋友還是一眼就能分辯兩者。只是,若能更好的避免混亂,又何樂不爲呢?
2,在多層次的複合替換中,` `須要額外的跳脫( \` )處理,而$( )則比較直觀。例如:
這是錯的:
command1 `command2 `command3` `
原本的意圖是要在command2 `command3`先將command3提換出來給command 2處理,然後再將結果傳給command1
`command2 ...`來處理。
然而,真正的結果在命令行中卻是分成了`command2 `與``兩段。
正確的輸入應該如下:
command1 `command2 \`command3\` `
要不然,換成$( )就沒問題了:
command1 $(command2 $(command3))
只要你喜歡,做多少層的替換都沒問題啦~~~  ^_^
不過,$( )並不是沒有弊端的...
首先,` `基本上可用在全部的unix shell中使用,若寫成shell script,其移植性比較高。
而$( )並不見的每一種shell都能使用,我只能跟你說,若你用bash2的話,肯定沒問題... 
接下來,再讓我們看${ }吧...它其實就是用來作變量替換用的啦。
一般情況下,$var與${var}並沒有啥不一樣。
但是用${ }會比較精確的界定變量名稱的範圍,比方說:
$ A=B
$ echo $AB
原本是打算先將$A的結果替換出來,然後再補一個B字母於其後,但在命令行上,真正的結果卻是隻會替換變量名稱爲AB
的值出來...
若使用${ }就沒問題了:
$ echo ${A}B
BB
不過,假如你只看到${ }只能用來界定變量名稱的話,那你就實在太小看bash了�u
有興趣的話,你可先參考一下cu本版的精華文章:
http://www.chinaunix.net/forum/viewtopic.php?t=201843
爲了完整起見,我這裏再用一些例子加以說明${ }的一些特異功能:
假設我們定義了一個變量爲:
file=/dir1/dir2/dir3/my.file.txt
我們可以用${ }分別替換獲得不同的值:
${file#*/}:拿掉第一條/及其左邊的字符串:dir1/dir2/dir3/my.file.txt
${file##*/}:拿掉最後一條/及其左邊的字符串:my.file.txt
${file#*.}:拿掉第一個.  及其左邊的字符串:file.txt
${file##*.}:拿掉最後一個.  及其左邊的字符串:txt
${file%/*}:拿掉最後條/及其右邊的字符串:/dir1/dir2/dir3
${file%%/*}:拿掉第一條/及其右邊的字符串:(空值)
${file%.*}:拿掉最後一個.  及其右邊的字符串:/dir1/dir2/dir3/my.file
http://www.818198.com  Page 12
SHELL十三問
${file%%.*}:拿掉第一個.  及其右邊的字符串:/dir1/dir2/dir3/my
記憶的方法爲:
#是去掉左邊(在鑑盤上#在$之左邊)
%是去掉右邊(在鑑盤上%在$之右邊)
單一符號是最小匹配�r兩個符號是最大匹配。
${file:0:5}:提取最左邊的5個字節:/dir1
${file:5:5}:提取第5個字節右邊的連續5個字節:/dir2
我們也可以對變量值裏的字符串作替換:
${file/dir/path}:將第一個dir替換爲path:/path1/dir2/dir3/my.file.txt
${file//dir/path}:將全部dir替換爲path:/path1/path2/path3/my.file.txt
利用${ }還可針對不同的變量狀態賦值(沒設定、空值、非空值):
${file-my.file.txt}:假如$file沒有設定,則使用my.file.txt作傳回值。(空值及非空值時不作處理)
${file:-my.file.txt}:假如$file沒有設定或爲空值,則使用my.file.txt作傳回值。(非空值時不作處理)
${file+my.file.txt}:假如$file設爲空值或非空值,均使用my.file.txt作傳回值。(沒設定時不作處理)
${file:+my.file.txt}:若$file爲非空值,則使用my.file.txt作傳回值。(沒設定及空值時不作處理)
${file=my.file.txt}:若$file沒設定,則使用my.file.txt作傳回值,同時將$file賦值爲my.file.txt。(空值及非空值時不作處理)
${file:=my.file.txt}:若$file沒設定或爲空值,則使用my.file.txt作傳回值,同時將$file賦值爲my.file.txt。(非空值時不作處理
)
${file?my.file.txt}:若$file沒設定,則將my.file.txt輸出至STDERR。(空值及非空值時不作處理)
${file:?my.file.txt}:若$file沒設定或爲空值,則將my.file.txt輸出至STDERR。(非空值時不作處理)
tips:
以上的理解在於,你一定要分清楚unset與null及non-null這三種賦值狀態.
一般而言, :與null有關,若不帶:的話, null不受影響,若帶:則連null也受影響.
還有哦,${#var}可計算出變量值的長度:
${#file}可得到27,因爲/dir1/dir2/dir3/my.file.txt剛好是27個字節...
接下來,再爲大家介稍一下bash的組數(array)處理方法。
一般而言,A="a b c def"這樣的變量只是將$A替換爲一個單一的字符串,
但是改爲A=(a b c def),則是將$A定義爲組數...
bash的組數替換方法可參考如下方法:
${A[@]}或${A
· }可得到a b c def (全部組數)
${A[0]}可得到a (第一個組數),${A[1]}則爲第二個組數...
${#A[@]}或${#A
· }可得到4 (全部組數數量)
${#A[0]}可得到1 (即第一個組數(a)的長度),${#A[3]}可得到3 (第四個組數(def)的長度)
A[3]=xyz則是將第四個組數重新定義爲xyz ...
諸如此類的....
能夠善用bash的$( )與${ }可大大提高及簡化shell在變量上的處理能力哦
好了,最後爲大家介紹$(( ))的用途吧:它是用來作整數運算的。
在bash中,$(( ))的整數運算符號大致有這些:
+ - * /:分別爲"加、減、乘、除"。
%:餘數運算
& | ^ !:分別爲"AND、OR、XOR、NOT"運算。
例:
$ a=5; b=7; c=2
$ echo $(( a+b*c ))
19
$ echo $(( (a+b)/c ))
http://www.818198.com  Page 13
SHELL十三問
6
SHELL十三問
groff, tbl, eqn
 另一種文本標記和顯示格式化語言是 groff. 這是一個對傳統 UNIX roff/troff 顯示和排
 版包的 GNU 增強版本.Man頁  使用的就是 groff.
 tbl 表處理工具可以認爲是 groff 的一部分, 它的功能就是將表標記轉化到 groff
 命令中.
 eqn 等式處理工具也是 groff 的一部分, 它的功能是將等式標記轉化到 groff 命令中.
Example 12-26 manview: 查看格式化的man頁
################################Start Script#######################################
 1 #!/bin/bash
 2 # manview.sh: 將man頁源文件格式化以方便查看.
 3
 4 #  當你想閱讀man頁的時候, 這個腳本就有用了.
 5 #  它允許你在運行的時候查看
 6 #+ 中間結果.
 7
 8 E_WRONGARGS=65
 9
10 if [ -z "$1" ]
11 then
12   echo "Usage: `basename $0` filename"
13   exit $E_WRONGARGS
14 fi
15
16 # ---------------------------17 groff -Tascii -man $1 | less
18 # 來自於 groff man頁.
19 # ---------------------------20
21 #  如果man業中包括表或者等式,
22 #+ 那麼上邊的代碼就夠嗆了.
23 #  下邊的這行代碼可以解決上邊的這個問題.
24 #
25 #   gtbl < "$1" | geqn -Tlatin1 | groff -Tlatin1 -mtty-char -man
26 #
27 #   Thanks, S.C.
28
29 exit 0
################################End Script#########################################
lex, yacc
 lex 是用於模式匹配的詞彙分析產生程序. 在Linux系統上這個命令已經被 flex 取代了.
 yacc 工具基於一系列的語法規範生成語法分析程序. 在Linux系統上這個命令已經被
 bison 取代了.
注意事項:
[1]  對於 GNU 版本的 tr 命令來說這是唯一一處比那些商業 UNIX 系統上的一般版本合適
  的地方.
12.5 文件與歸檔命令
-------------------歸檔命令
http://www.818198.com  Page 260
SHELL十三問
tar
    標準的 UNIX 歸檔工具. [1] 起初這只是一個 磁帶 歸檔 程序, 而現在這個工具已經被開
 發爲通用打包程序, 它能夠處理所有設備的所有類型的歸檔文件, 包括磁帶設備, 正常文
 件, 甚至是 stdout (參見Example 3-4). GNU 的tar工具現在可以接受不同種類的壓縮過
 濾器, 比如tar czvf archive_name.tar.gz *, 並且可以遞歸的處理歸檔文件, 還可以用
 gzip 壓縮目錄下的所有文件, 除了當前目錄下($PWD)的 點文件 . [2]
 一些有用的 tar 命令選項:
  1.  -c 創建 (一個新的歸檔文件)
  2.  -x 解壓文件 (從存在的歸檔文件中)
  3.  --delete 刪除文件 (從存在的歸檔文件中)
  注意: 這個選項不能用於磁帶類型設備.
  4.  -r 將文件添加到現存的歸檔文件的尾部
  5.  -A 將 tar 文件添加到現存的歸檔文件的尾部
  6.  -t 列出現存的歸檔文件中包含的內容
  7.  -u 更新歸檔文件
  8.  -d 使用指定的文件系統 比較歸檔文件
  9.  -z 用 gzip 壓縮歸檔文件
  (壓縮還是解壓, 依賴於是否組合了 -c 或 -x)選項
 10.  -j 用 bzip2 壓縮歸檔文件
 注意: 如果想從損壞的用 gzip 壓縮過的 tar 文件中取得數據, 那將是很困難的. 所有當
  我們歸檔重要的文件的時候, 一定要保留多個備份.
shar
 Shell 歸檔工具. 存在於 shell 歸檔文件中的所有文件都是未經壓縮的, 並且本質上是一
 個shell 腳本,以 #!/bin/sh 開頭, 並且包含所有必要的解檔命令. Shar 歸檔文件  至今
 還在 Internet 新聞組中使用, 否則的話 shar早就被 tar/gzip 所取代了. unshar 命令
 用來解檔 shar 歸檔文件.
ar
 創建和操作歸檔文件的工具, 主要在對2進制目標文件打包成庫時纔會用到.
rpm
 Red Hat 包管理器, 或者說 rpm 工具提供了一種對源文件或2進制文件進行打包的方法.
 除此之外, 它還包括安裝命令, 並且還檢查包的完整性.
 一個簡單的 rpm -i package_name.rpm  命令對於安裝一個包來說就足夠了, 雖然這個命
 令還有好多其它的選項.
 注意: rpm -qf 列出一個文件屬於那個包.
   bash$ rpm -qf /bin/ls
   coreutils-5.2.1-3
 注意: rpm -qa 將會列出給定系統上所有安裝了的 rpm 包. rpm -qa package_name 命令
  將會列出於給定名字匹配的包.
   bash$ rpm -qa
   redhat-logos-1.1.3-1
   glibc-2.2.4-13
   cracklib-2.7-12
   dosfstools-2.7-1
   gdbm-1.8.0-10
   ksymoops-2.4.1-1
   mktemp-1.5-11
   perl-5.6.0-17
   reiserfs-utils-3.x.0j-2
http://www.818198.com  Page 261
SHELL十三問
   ...
  
  
   bash$ rpm -qa docbook-utils
   docbook-utils-0.6.9-2
  
  
   bash$ rpm -qa docbook | grep docbook
   docbook-dtd31-sgml-1.0-10
   docbook-style-dsssl-1.64-3
   docbook-dtd30-sgml-1.0-10
   docbook-dtd40-sgml-1.0-11
   docbook-utils-pdf-0.6.9-2
   docbook-dtd41-sgml-1.0-10
   docbook-utils-0.6.9-2
cpio
 這個特殊的歸檔拷貝命令(拷貝輸入和輸出)現在已經很少能見到了, 因爲它已經被 tar/gz
 ip 所替代了.現在這個命令只在一些比較特殊的地方還在使用,比如拷貝一個目錄樹.
Example 12-27 使用 cpio 來拷貝一個目錄樹
################################Start Script#######################################
 1 #!/bin/bash
 2
 3 # 使用 'cpio' 拷貝目錄樹.
 4
 5 # 使用 'cpio' 的優點:
 6 #   加速拷貝. 比通過管道使用 'tar' 命令快一些.
 7 #   很適合拷貝一些 'cp' 命令
 8 #+  搞不定的的特殊文件(比如名字叫 pipes 的文件, 等等)
 9
10 ARGS=2
11 E_BADARGS=65
12
13 if [ $# -ne "$ARGS" ]
14 then
15   echo "Usage: `basename $0` source destination"
16   exit $E_BADARGS
17 fi 
18
19 source=$1
20 destination=$2
21
22 find "$source" -depth | cpio -admvp "$destination"
23 #               ^^^^^         ^^^^^
24 # 閱讀 'find' 和 'cpio' 的man 頁來了解這些選項的意義.
25
26
27 # 練習:
28 # -----http://www.818198.com  Page 262
SHELL十三問
29
30 #  添加一些代碼來檢查 'find | cpio' 管道命令的退出碼($?)
31 #+ 並且如果出現錯誤的時候輸出合適的錯誤碼.
32
33 exit 0
################################End Script#########################################
rpm2cpio
 這個命令可以從 rpm 歸檔文件中解出一個 cpio 歸檔文件.
Example 12-28 解包一個 rpm 歸檔文件
################################Start Script#######################################
 1 #!/bin/bash
 2 # de-rpm.sh: 解包一個 'rpm' 歸檔文件
 3
 4 : ${1?"Usage: `basename $0` target-file"}
 5 # 必須指定 'rpm' 歸檔文件名作爲參數.
 6
 7
 8 TEMPFILE=$$.cpio                         # Tempfile 必須是一個"唯一"的名字.
 9                                          # $$ 是這個腳本的進程 ID.
10
11 rpm2cpio < $1 > $TEMPFILE                # 將 rpm 歸檔文件轉換爲 cpio 歸檔文件.
12 cpio --make-directories -F $TEMPFILE -i  # 解包 cpio 歸檔文件.
13 rm -f $TEMPFILE                          # 刪除 cpio 歸檔文件.
14
15 exit 0
16
17 #  練習:
18 #  添加一些代碼來檢查    1) "target-file" 是否存在
19 #+                       2) 這個文件是否是一個 rpm 歸檔文件.
20 #  暗示:                    分析 'file' 命令的輸出.
################################End Script#########################################
壓縮命令
gzip
 標準的 GNU/UNIX 壓縮工具, 取代了比較差的 compress 命令. 相應的解壓命令是gunzip,
  gzip -d 是等價的.
 zcat 過濾器可以將一個 gzip 文件解壓到 stdout, 所以儘可能的使用管道和重定向. 這
 個命令事實上就是一個可以工作於壓縮文件(包括一些的使用老的 compress 工具壓縮的文
 件)的 cat 命令. zcat 命令等價於 gzip -dc.
 注意: 在某些商業的 UNIX 系統上, zcat  與 uncompress -c 等價, 並且不能工作於
  gzip  文件.
 參見 Example 7-7.
bzip2
 用來壓縮的一個可選的工具, 通常比 gzip 命令壓縮率更高(所以更慢), 適用於比較大的
 文件. 相應的解壓命令是 bunzip2.
 注意: 新版本的 tar 命令已經直接支持 bzip2 了.
compress, uncompress
 這是一個老的, 私有的壓縮工具, 一般的商業 UNIX 發行版都會有這個工具. 更有效率的
 gzip 工具早就把這個工具替換掉了. Linux 發行版一般也會包含一個兼容的 compress 命
http://www.818198.com  Page 263
SHELL十三問
 令, 雖然 gunzip 也可以加壓用 compress 工具壓縮的文件.
 注意: znew 命令可以將 compress 壓縮的文件轉換爲 gzip 壓縮的文件.
sq
 另一種壓縮工具, 一個只能工作於排過序的 ASCII 單詞列表的過濾器.這個命令使用過濾
 器標準的調用語法, sq < input-file >  output-file. 速度很快, 但是效率遠不及
 gzip. 相應的解壓命令爲 unsq, 調用方法與 sq 相同.
 注意: sq 的輸出可以通過管道傳遞給 gzip 以便於進一步的壓縮.
zip, unzip
 跨平臺的文件歸檔和壓縮工具, 與 DOS 下的 pkzip.exe 兼容. zip 歸檔文件看起來在互
 聯網上比 tar 包更流行.
unarc, unarj, unrar
 這些 Linux 工具可以用來解檔那些用 DOS 下的 arc.exe, arj.exe, 和 rar.exe 程序進
 行歸檔的文件.
文件信息
file
 確定文件類型的工具. 命令 file file-name 將會用 ascii 文本或數據的形式返回
 file-name 文件的詳細描述. 這個命令會使用 /usr/share/magic, /etc/magic, 或
 /usr/lib/magic 中定義的 魔法數字  來標識包含某種魔法數字的文件, 上邊所舉出的這
 3個文件需要依賴於具體的 Linux/UNIX 發行版.
 -f 選項將會讓 file 命令運行於批處理模式, 也就是說它會分析 -f 後邊所指定的文件,
 從中讀取需要處理的文件列表, 然後依次執行 file 命令. -z 選項, 當對壓縮過的目標文
 件使用時, 將會強制分析壓縮的文件類型.
  bash$ file test.tar.gz
  test.tar.gz: gzip compressed data, deflated, last modified: Sun Sep 16 13:34:51 2001, os: Unix
 
  bash file -z test.tar.gz
  test.tar.gz: GNU tar archive (gzip compressed data, deflated, last modified: Sun Sep 16 13:34:51 2001, os: Unix)
  1 # 在給定的目錄中找出sh和Bash腳本文件:
  2
  3 DIRECTORY=/usr/local/bin
  4 KEYWORD=Bourne
  5 # Bourne 和 Bourne-Again shell 腳本
  6
  7 file $DIRECTORY/* | fgrep $KEYWORD
  8
  9 # 輸出:
 10
 11 # /usr/local/bin/burn-cd:          Bourne-Again shell script text executable
 12 # /usr/local/bin/burnit:           Bourne-Again shell script text executable
 13 # /usr/local/bin/cassette.sh:      Bourne shell script text executable
 14 # /usr/local/bin/copy-cd:          Bourne-Again shell script text executable
 15 # . . .
Example 12-29 從 C 文件中去掉註釋
################################Start Script#######################################
 1 #!/bin/bash
 2 # strip-comment.sh: 去掉C 程序中的註釋 (/* 註釋 */)
 3
 4 E_NOARGS=0
http://www.818198.com  Page 264
SHELL十三問
 5 E_ARGERROR=66
 6 E_WRONG_FILE_TYPE=67
 7
 8 if [ $# -eq "$E_NOARGS" ]
 9 then
10   echo "Usage: `basename $0` C-program-file" >&2 # 將錯誤消息發到 stderr.
11   exit $E_ARGERROR
12 fi 
13
14 # 檢查文件類型是否正確.
15 type=`file $1 | awk '{ print $2, $3, $4, $5 }'`
16 # "file $1" echoe 出文件類型 . . .
17 # 然後 awk 會刪掉第一個域,  就是文件名 . . .
18 # 然後結果將會傳遞到變量 "type" 中.
19 correct_type="ASCII C program text"
20
21 if [ "$type" != "$correct_type" ]
22 then
23   echo
24   echo "This script works on C program files only."
25   echo
26   exit $E_WRONG_FILE_TYPE
27 fi 
28
29
30 # 相當隱祕的 sed 腳本:
31 #--------32 sed '
33 /^\/\*/d
34 /.*\*\//d
35 ' $1
36 #--------37 # 如果你花上幾個小時來學習 sed 語法的話, 上邊這個命令還是很好理解的.
38
39
40 #  如果註釋和代碼在同一行上, 上邊的腳本就不行了.
41 #+ 所以需要添加一些代碼來處理這種情況.
42 #  這是一個很重要的練習.
43
44 #  當然, 上邊的代碼也會刪除帶有 "*/" 的非註釋行 --45 #+ 這也不是一個令人滿意的結果.
46
47 exit 0
48
49
50 # ----------------------------------------------------------------51 # 下邊的代碼不會執行, 因爲上邊已經 'exit 0' 了.
52
http://www.818198.com  Page 265
SHELL十三問
53 # Stephane Chazelas 建議使用下邊的方法:
54
55 usage() {
56   echo "Usage: `basename $0` C-program-file" >&2
57   exit 1
58 }
59
60 WEIRD=`echo -n -e '\377'`   # or WEIRD=$'\377'
61 [[ $# -eq 1 ]] || usage
62 case `file "$1"` in
63   *"C program text"*) sed -e "s%/\*%${WEIRD}%g;s%\*/%${WEIRD}%g" "$1" \
64      | tr '\377\n' '\n\377' \
65      | sed -ne 'p;n' \
66      | tr -d '\n' | tr '\377' '\n';;
67   *) usage;;
68 esac
69
70 #  如果是下列的這些情況, 還是很糟糕:
71 #  printf("/*");
72 #  or
73 #  /*  /* buggy embedded comment */
74 #
75 #  爲了處理上邊所有這些特殊情況(字符串中的註釋, 含有 \", \\" ...
76 #+ 的字符串中的註釋) 唯一的方法還是寫一個 C 分析器
77 #+ (或許可以使用lex 或者 yacc ?).
78
79 exit 0
################################End Script#########################################
which
 which command-xxx 將會給出 "command-xxx" 的完整路徑. 當你想在系統中準確定位一個
 特定的命令或工具的時候, 這個命令就非常有用了.
 $bash which rm
  /usr/bin/rm
whereis
 與上邊的 which 很相似, whereis command-xxx 不只會給出 "command-xxx" 的完整路徑,
  而且還會給出這個命令的 man頁 的完整路徑.
 $bash whereis rm
  rm: /bin/rm /usr/share/man/man1/rm.1.bz2
whatis
 whatis filexxx 將會在 whatis 數據庫中查詢 "filexxx". 當你想確認系統命令和重要的
 配置文件的時候, 這個命令就非常重要了. 可以把這個命令認爲是一個簡單的 man 命令.
 $bash whatis whatis
  whatis               (1)  - search the whatis database for complete words
Example 12-30 Exploring /usr/X11R6/bin
################################Start Script#######################################
 1 #!/bin/bash
 2
 3 # 在 /usr/X11R6/bin 中的所有神祕的2進制文件都是什麼東西?
http://www.818198.com  Page 266
SHELL十三問
 4
 5 DIRECTORY="/usr/X11R6/bin"
 6 # 也試試 "/bin", "/usr/bin", "/usr/local/bin", 等等.
 7
 8 for file in $DIRECTORY/*
 9 do
10   whatis `basename $file`   # 將會 echo 出這個2進制文件的信息.
11 done
12
13 exit 0
14
15 # 你可能希望將這個腳本的輸出重定向, 像這樣:
16 # ./what.sh >>whatis.db
17 # 或者一頁一頁的在 stdout 上查看,
18 # ./what.sh | less
################################End Script#########################################
 參見 Example 10-3.
vdir
 顯示詳細的目錄列表. 與 ls -l 的效果類似.
 這是一個 GNU fileutils.
  bash$ vdir
  total 10
  -rw-r--r--    1 bozo  bozo      4034 Jul 18 22:04 data1.xrolo
  -rw-r--r--    1 bozo  bozo      4602 May 25 13:58 data1.xrolo.bak
  -rw-r--r--    1 bozo  bozo       877 Dec 17  2000 employment.xrolo
 
  bash ls -l
  total 10
  -rw-r--r--    1 bozo  bozo      4034 Jul 18 22:04 data1.xrolo
  -rw-r--r--    1 bozo  bozo      4602 May 25 13:58 data1.xrolo.bak
  -rw-r--r--    1 bozo  bozo       877 Dec 17  2000 employment.xrolo
locate, slocate
 locate 命令將會在預先建立好的檔案數據庫中查詢文件. slocate 命令是 locate 的安全
 版本( locate 命令可能已經被關聯到 slocate 命令上了).
 $bash locate hickson
  /usr/lib/xephem/catalogs/hickson.edb
readlink
 顯示符號連接所指向的文件.
  bash$ readlink /usr/bin/awk
  ../../bin/gawk
strings
 使用 strings 命令在二進制或數據文件中找出可打印字符. 它將在目標文件中列出所有找
 到的可打印字符的序列. 這個命令對於想進行快速查找一個 n 個字符的打印檢查來說是很
 方便的,也可以用來檢查一個未知格式的圖片文件 (strings image-file | more 可能會搜
 索出像 JFIF 這樣的字符串, 那麼這就意味着這個文件是一個 jpeg  格式的圖片文件).
 在腳本中, 你可能會使用 grep 或 sed 命令來分析 strings 命令的輸出. 參見
 Example 10-7  和 Example 10-9.
Example 12-31 一個"改進過"的 strings  命令
http://www.818198.com  Page 267
SHELL十三問
################################Start Script#######################################
 1 #!/bin/bash
 2 # wstrings.sh: "word-strings" (增強的 "strings" 命令)
 3 #
 4 #  這個腳本將會過濾 "strings" 命令的輸出.
 5 #+ 通過排除標準單詞列表的形式檢查來過濾輸出.
 6 #  這將有效的過濾掉無意義的字符,
 7 #+ 並且指揮輸出可以識別的字符.
 8
 9 # ===========================================================
10 #                 腳本參數的標準檢查
11 ARGS=1
12 E_BADARGS=65
13 E_NOFILE=66
14
15 if [ $# -ne $ARGS ]
16 then
17   echo "Usage: `basename $0` filename"
18   exit $E_BADARGS
19 fi
20
21 if [ ! -f "$1" ]                      # 檢查文件是否存在.
22 then
23     echo "File \"$1\" does not exist."
24     exit $E_NOFILE
25 fi
26 # ===========================================================
27
28
29 MINSTRLEN=3                           #  最小的字符串長度.
30 WORDFILE=/usr/share/dict/linux.words  #  字典文件.
31                                       #  也可以指定一個不同的
32                                       #+ 單詞列表文件,
33                                       #+ 但這種文件必須是以每個單詞一行的方式進行保存.
34
35
36 wlist=`strings "$1" | tr A-Z a-z | tr '[:space:]' Z | \
37 tr -cs '[:alpha:]' Z | tr -s '\173-\377' Z | tr Z ' '`
38
39 # 將'strings' 命令的輸出通過管道傳遞到多個 'tr' 命令中.
40 #  "tr A-Z a-z"  全部轉換爲小寫字符.
41 #  "tr '[:space:]'"  轉換空白字符爲多個 Z.
42 #  "tr -cs '[:alpha:]' Z"  將非字母表字符轉換爲多個 Z,
43 #+ 然後去除多個連續的 Z.
44 #  "tr -s '\173-\377' Z"  把所有z後邊的字符都轉換爲 Z.
45 #+ 並且去除多餘重複的Z.(注意173(123 ascii "{")和377(255 ascii 最後一個字符)都是8進制)
46 #+ 這樣處理之後, 我們所有之前需要處理的令我們頭痛的字符
47 #+ 就全都轉換爲字符 Z 了.
http://www.818198.com  Page 268
SHELL十三問
48 #  最後"tr Z ' '" 將把所有的 Z 都轉換爲空格,
49 #+ 這樣我們在下邊循環中用到的變量 wlist 中的內容就全部以空格分隔了.
50
51 #  ****************************************************************
52 #  注意, 我們使用管道來將多個 'tr' 的輸出傳遞到下一個 'tr' 時
53 #+ 每次都使用了不同的參數.
54 #  ****************************************************************
55
56
57 for word in $wlist                    # 重要:
58                                       # $wlist 這裏不能使用雙引號.
59                                       # "$wlist" 不能正常工作.
60                                       # 爲什麼不行?
61 do
62
63   strlen=${#word}                     # 字符串長度.
64   if [ "$strlen" -lt "$MINSTRLEN" ]   # 跳過短的字符串.
65   then
66     continue
67   fi
68
69   grep -Fw $word "$WORDFILE"          #  只匹配整個單詞.
70 #      ^^^                            #  "固定字符串" 和
71                                       #+ "整個單詞" 選項.
72
73 done 
74
75
76 exit $?
################################End Script#########################################
比較命令
diff, patch
 diff: 一個非常靈活的文件比較工具. 這個工具將會以一行接一行的形式來比較目標文件.
  在某些應用中, 比如說比較單詞詞典, 在通過管道將結果傳遞給 diff 命令之前, 使用諸
 如 sort 和 uniq 命令來對文件進行過濾將是非常有用的.diff file-1 file-2 將會輸出
 2個文件不同的行,並會通過符號標識出每個不同行所屬的文件.
 diff 命令的 --side-by-side 選項將會把2個比較中的文件全部輸出, 按照左右分隔的形
 式, 並會把不同的行標記出來. -c 和 -u 選項也會使得 diff 命令的輸出變得容易解釋
 一些.
 還有一些 diff 命令的變種, 比如 sdiff, wdiff, xdiff, 和 mgdiff.
 注意: 如果比較的兩個文件是完全一樣的話, 那麼 diff 命令會返回 0 作爲退出碼, 如果
  不同的話就返回 1 作爲退出碼. 這樣 diff 命令就可以用在 shell 腳本的測試結構
  中了. (見下邊)
 diff 命令的一個重要用法就是產生區別文件, 這個文件將用作 patch 命令的 -e 選項的
 參數, -e 選項接受 ed 或 ex 腳本.
 patch: 靈活的版本工具.給出一個用 diff 命令產生的區別文件, patch 命令可以將一個
 老版本的包更新爲一個新版本的包. 因爲你發佈一個小的區別文件遠比重新發佈一個大的
 軟件包來的容易得多.對於頻繁更新的 Linux 內核來說, 使用補丁包的形式來發布將是一
http://www.818198.com  Page 269
SHELL十三問
 種很好的方法.
    1 patch -p1 <patch-file
    2 # 在'patch-file'中取得所有的修改列表
    3 # 然後把它們應用於其中索引到的文件上.
    4 # 那麼這個包就被更新爲新版本了.
 更新 kernel:
    1 cd /usr/src
    2 gzip -cd patchXX.gz | patch -p0
    3 #  使用'patch'來更新內核源文件.
    4 # 來自於匿名作者(Alan Cox?)的
    5 # Linux 內核文檔 "README".
 注意: diff 命令也可以遞歸的比較目錄下的所有文件(包含子目錄).
   bash$ diff -r ~/notes1 ~/notes2
   Only in /home/bozo/notes1: file02
   Only in /home/bozo/notes1: file03
   Only in /home/bozo/notes2: file04
 注意: 使用 zdiff 來比較 gzip 文件.
diff3
 一個 diff 命令的擴展版本, 可以同時比較3個文件. 如果成功執行那麼這個命令就返回0,
  但是不幸的是這個命令不給出比較結果的信息.
  bash$ diff3 file-1 file-2 file-3
  ====
  1:1c
    This is line 1 of "file-1".
  2:1c
    This is line 1 of "file-2".
  3:1c
    This is line 1 of "file-3"
sdiff
 比較 和/或 編輯2個文件, 將它們合併到一個輸出文件中. 因爲這個命令的交互特性, 所
 以在腳本中很少使用這個命令.
cmp
 cmp 命令是上邊 diff 命令的一個簡單版本. diff  命令會報告兩個文件的不同之處, 而
 cmp 命令僅僅指出那些位置有不同, 而不會顯示不同的具體細節.
 注意: 與 diff 一樣,如果兩個文件相同 cmp  返回0作爲退出碼, 如果不同返回1. 這樣就
  可以用在 shell 腳本的測試結構中了.
Example 12-32 在一個腳本中使用 cmp 來比較2個文件.
################################Start Script#######################################
 1 #!/bin/bash
 2
 3 ARGS=2  # 腳本需要2個參數.
 4 E_BADARGS=65
 5 E_UNREADABLE=66
 6
 7 if [ $# -ne "$ARGS" ]
 8 then
 9   echo "Usage: `basename $0` file1 file2"
10   exit $E_BADARGS
http://www.818198.com  Page 270
SHELL十三問
11 fi
12
13 if [[ ! -r "$1" || ! -r "$2" ]]
14 then
15   echo "Both files to be compared must exist and be readable."
16   exit $E_UNREADABLE
17 fi
18
19 cmp $1 $2 &> /dev/null  # /dev/null 將會禁止 "cmp" 命令的輸出.
20 #   cmp -s $1 $2  與上邊這句結果相同 ("-s" 選項是安靜標誌)
21 #   Thank you  Anders Gustavsson for pointing this out.
22 #
23 # 用 'diff' 命令也可以, 比如,   diff $1 $2 &> /dev/null
24
25 if [ $? -eq 0 ]         # 測試 "cmp" 命令的退出碼.
26 then
27   echo "File \"$1\" is identical to file \"$2\"."
28 else 
29   echo "File \"$1\" differs from file \"$2\"."
30 fi
31
32 exit 0
################################End Script#########################################
 注意: 用 zcmp 處理 gzip 文件.
comm
 多功能的文件比較工具. 使用這個命令之前必須先排序.
 comm -options  first-file  second-file
 comm file-1 file-2 將會輸出3列:
  * 第 1 列 = 只在 file-1 中存在的行
  * 第 2 列 = 只在 file-2 中存在的行
  * 第 2 列 = 兩邊相同的行.
 下列選項可以禁止1列或多列的輸出.
  * -1 禁止顯示第一欄 (譯者: 在 File1 中的行)
  * -2 禁止顯示第二欄 (譯者: 在 File2 中的行)
  * -3 禁止顯示第三欄 (譯者: File1 和 File2 公共的行)
  * -12 禁止第一列和第二列, (就是說選項可以組合).
一般工具
basename
 從文件名中去掉路徑信息, 只打印出文件名. 結構 basename $0 可以讓腳本知道它自己的
 名字, 也就是, 它被調用的名字. 可以用來顯示用法信息, 比如如果你調用腳本的時候缺
 少參數, 可以使用如下語句:
    1 echo "Usage: `basename $0` arg1 arg2 ... argn"
dirname
 從帶路徑的文件名中去掉文件名, 只打印出路徑信息.
 注意: basename 和 dirname  可以操作任意字符串. 參數可以不是一個真正存在的文件,
  甚至可以不是一個文件名.(參見 Example A-7).
Example 12-33 basename 和 dirname
################################Start Script#######################################
http://www.818198.com  Page 271
SHELL十三問
 1 #!/bin/bash
 2
 3 a=/home/bozo/daily-journal.txt
 4
 5 echo "Basename of /home/bozo/daily-journal.txt = `basename $a`"
 6 echo "Dirname of /home/bozo/daily-journal.txt = `dirname $a`"
 7 echo
 8 echo "My own home is `basename ~/`."         # `basename ~` also works.
 9 echo "The home of my home is `dirname ~/`."  # `dirname ~`  also works.
10
11 exit 0
################################End Script#########################################
split, csplit
 將一個文件分割爲幾個小段的工具. 這些命令通常用來將大的文件分割, 並備份到軟盤上,
  或者是爲了切成合適的尺寸用 email 上傳.
 csplit 根據 上下文 來切割文件, 切割的位置將會發生在模式匹配的地方.
sum, cksum, md5sum, sha1sum
 這些都是用來產生 checksum 的工具. checksum 的目的是用來檢驗文件的完整性, 是對文
 件的內容進行數學計算而得到的. 出於安全目的一個腳本可能會有一個 checksum 列表,
 這樣可以確保關鍵系統文件的內容不會被修改或損壞. 對於需要安全性的應用來說, 應該
 使用 md5sum (message digest 5  checksum) 命令, 或者更好的更新的 sha1sum
 (安全 Hash 算法).
  bash$ cksum /boot/vmlinuz
  1670054224 804083 /boot/vmlinuz
 
  bash$ echo -n "Top Secret" | cksum
  3391003827 10
 
 
 
  bash$ md5sum /boot/vmlinuz
  0f43eccea8f09e0a0b2b5cf1dcf333ba  /boot/vmlinuz
 
  bash$ echo -n "Top Secret" | md5sum
  8babc97a6f62a4649716f4df8d61728f  -
 注意: cksum 命令將會顯示目標的尺寸(字節), 目標可以使文件或 stdout.
  md5sum 和 sha1sum 命令在它們收到 stdout 的輸入時候, 顯示一個 dash .
Example 12-34 檢查文件完整性
################################Start Script#######################################
  1 #!/bin/bash
  2 # file-integrity.sh: 檢查一個給定目錄下的文件
  3 #                    是否被改動了.
  4
  5 E_DIR_NOMATCH=70
  6 E_BAD_DBFILE=71
  7
  8 dbfile=File_record.md5
  9 # 存儲記錄的文件名 (數據庫文件).
http://www.818198.com  Page 272
SHELL十三問
 10
 11
 12 set_up_database ()
 13 {
 14   echo ""$directory"" > "$dbfile"
 15   # 把目錄名寫到文件的第一行.
 16   md5sum "$directory"/* >> "$dbfile"
 17   # 在文件中附上  md5 checksums 和 filenames.
 18 }
 19
 20 check_database ()
 21 {
 22   local n=0
 23   local filename
 24   local checksum
 25
 26   # ------------------------------------------- #
 27   #  這個文件檢查其實是不必要的,
 28   #+ 但是能安全一些.
 29
 30   if [ ! -r "$dbfile" ]
 31   then
 32     echo "Unable to read checksum database file!"
 33     exit $E_BAD_DBFILE
 34   fi
 35   # ------------------------------------------- #
 36
 37   while read record[n]
 38   do
 39
 40     directory_checked="${record[0]}"
 41     if [ "$directory_checked" != "$directory" ]
 42     then
 43       echo "Directories do not match up!"
 44       # 換個目錄試一下.
 45       exit $E_DIR_NOMATCH
 46     fi
 47
 48     if [ "$n" -gt 0 ]   # 不是目錄名.
 49     then
 50       filename[n]=$( echo ${record[$n]} | awk '{ print $2 }' )
 51       #  md5sum 向後寫記錄,
 52       #+ 先寫 checksum, 然後寫 filename.
 53       checksum[n]=$( md5sum "${filename[n]}" )
 54
 55
 56       if [ "${record[n]}" = "${checksum[n]}" ]
 57       then
http://www.818198.com  Page 273
SHELL十三問
 58         echo "${filename[n]} unchanged."
 59
 60       elif [ "`basename ${filename[n]}`" != "$dbfile" ]
 61              #  跳過checksum 數據庫文件,
 62              #+ 因爲在每次調用腳本它都會被修改.
 63       #  --- 64       #  這不幸的意味着當我們在 $PWD中運行這個腳本
 65       #+ 時, 修改這個 checksum 數
 66       #+ 據庫文件將不會被檢測出來.
 67       #  練習: 修復這個問題.
 68  then
 69           echo "${filename[n]} : CHECKSUM ERROR!"
 70         # 因爲最後的檢查, 文件已經被修改.
 71       fi
 72
 73       fi
 74
 75
 76
 77     let "n+=1"
 78   done <"$dbfile"       # 從 checksum 數據庫文件中讀.
 79
 80 } 
 81
 82 # =================================================== #
 83 # main ()
 84
 85 if [ -z  "$1" ]
 86 then
 87   directory="$PWD"      #  如果沒制定參數,
 88 else                    #+ 那麼就使用當前的工作目錄.
 89   directory="$1"
 90 fi 
 91
 92 clear                   # 清屏.
 93 echo " Running file integrity check on $directory"
 94 echo
 95
 96 # ------------------------------------------------------------------ #
 97   if [ ! -r "$dbfile" ] # 是否需要建立數據庫文件?
 98   then
 99     echo "Setting up database file, \""$directory"/"$dbfile"\"."; echo
100     set_up_database
101   fi 
102 # ------------------------------------------------------------------ #
103
104 check_database          # 調用主要處理函數.
105
http://www.818198.com  Page 274
SHELL十三問
106 echo
107
108 #  你可能想把這個腳本的輸出重定向到文件中,
109 #+ 尤其在這個目錄中有很多文件的時候.
110
111 exit 0
112
113 #  如果要對數量非常多的文件做完整性檢查,
114 #+ 可以考慮一下 "Tripwire" 包,
115 #+ http://sourceforge.net/projects/tripwire/.
116
################################End Script#########################################
 參見 Example A-19 和 Example 33-14 , 這兩個例子展示了 md5sum 命令的用法.
 注意: 已經有 128-bit md5sum 被破解的報告了,所以現在更安全的 160-bit sha1sum 是
  非常受歡迎的, 並且已經被加入到 checksum 工具包中.
  一些安全顧問認爲即使是 sha1sum 也是會被泄漏的. 所以, 下一個工具是什麼呢?
  -- 512-bit 的 checksum 工具?
   bash$ md5sum testfile
   e181e2c8720c60522c4c4c981108e367  testfile
  
  
   bash$ sha1sum testfile
   5d7425a9c08a66c3177f1e31286fa40986ffc996  testfile
shred
 用隨機字符填充文件, 使得文件無法恢復, 這樣就可以保證文件安全的被刪除. 這個命令
 的效果與 Example 12-55 一樣, 但是使用這個命令是一種更優雅更徹底的方法.
 這是一個 GNU fileutils.
 注意: 即使使用了 shred 命令, 高級的(forensic)辯論技術還是能夠恢復文件的內容.
編碼和解碼
uuencode
 這個工具用來把二進制文件編碼成 ASCII 字符串,這個工具適用於編碼e-mail消息體,或者
 新聞組消息.
uudecode
 這個工具用來把 uuencode 後的 ASCII 字符串恢復爲二進制文件.
Example 12-35 Uudecod 編碼後的文件
################################Start Script#######################################
 1 #!/bin/bash
 2 # 在當前目錄下 uudecode 所有用 uuencode 編碼的文件.
 3
 4 lines=35        # 允許讀頭部的 35 行(範圍很寬).
 5
 6 for File in *   # Test 所有 $PWD 下的文件.
 7 do
 8   search1=`head -$lines $File | grep begin | wc -w`
 9   search2=`tail -$lines $File | grep end | wc -w`
10   #  Uuencode 過的文件在文件開始的地方有個 "begin",
11   #+ 在文件結尾的地方有個 "end".
12   if [ "$search1" -gt 0 ]
http://www.818198.com  Page 275
SHELL十三問
13   then
14     if [ "$search2" -gt 0 ]
15     then
16       echo "uudecoding - $File -"
17       uudecode $File
18     fi 
19   fi
20 done 
21
22 #  小心不要讓這個腳本運行自己,
23 #+ 因爲它也會把自身也認爲是一個 uuencoded 文件,
24 #+ 這都是因爲這個腳本自身也包含 "begin" 和 "end".
25
26 #  練習:
27 #  -----28 #  修改這個腳本, 讓它可以檢查一個新聞組的每個文件,
29 #+ 並且如果下一個沒找的話就跳過.
30
31 exit 0
################################End Script#########################################
 注意: fold -s 命令在處理從 Usenet 新聞組下載下來的長的uudecode 文本消息的時候可
  能會有用(可能在管道中).
mimencode, mmencode
 mimencode 和 mmencode 命令處理多媒體編碼的 email 附件. 雖然 mail 用戶代理
 (比如 pine 或 kmail) 通常情況下都會自動處理, 但是這些特定的工具允許從命令行或
 shell腳本中來手動操作這些附件.
crypt
 這個工具曾經是標準的 UNIX 文件加密工具. [3]  政府由於政策上的動機規定禁止加密軟
 件的輸出, 這樣導致了 crypt 命令從 UNIX 世界消失, 並且在大多數的 Linux 發行版中
 也沒有這個命令. 幸運的是, 程序員們想出了一些替代它的方法, 在這些方法中有作者自
 己的 cruft (參見 Example A-4).
一些雜項工具
mktemp
 使用一個"唯一"的文件名來創建一個 臨時文件  [4]  . 如果不帶參數的在命令行下調用
 這個命令時, 將會在 /tmp 目錄下產生一個零長度的文件.
  bash$ mktemp
  /tmp/tmp.zzsvql3154
    1 PREFIX=filename
    2 tempfile=`mktemp $PREFIX.XXXXXX`
    3 #                        ^^^^^^ 在這個臨時的文件名中
    4 #+                              至少需要6個佔位符.
    5 #  如果沒有指定臨時文件的文件名,
    6 #+ 那麼默認就是 "tmp.XXXXXXXXXX".
    7
    8 echo "tempfile name = $tempfile"
    9 # tempfile name = filename.QA2ZpY
   10 #                 或者一些其他的相似的名字...
   11
http://www.818198.com  Page 276
SHELL十三問
   12 #  使用 600 爲文件權限
   13 #+ 來在當前工作目錄下創建一個這樣的文件.
   14 #  這樣就不需要 "umask 177" 了.
   15 #  但不管怎麼說, 這也是一個好的編程風格.
make
 build 和 compile 二進制包的工具. 當源文件被增加或修改時就會觸發一些操作, 這個工
 具用來控制這些操作.
 make 命令將會檢查 Makefile, makefile 是文件的依賴和操作列表.
install
 特殊目的的文件拷貝命令, 與 cp 命令相似, 但是具有設置拷貝文件的權限和屬性的能力.
  這個命令看起來是爲了安裝軟件包所定製的, 而且就其本身而言, 這個命令經常出現在
 Makefile 中(在 make install : 區中). 在安裝腳本中也會看到這個命令的使用.
dos2unix
 這個工具是由 Benjamin Lin 和其同事編寫的, 目的是將 DOS 格式的文本文件
 (以 CR-LF 爲行結束符) 轉換爲 UNIX 格式 (以 LF 爲行結束符), 反過來也一樣.
ptx
 ptx [targetfile] 命令將會輸出目標文件的序列改變的索引(交叉引用列表). 如果必要的
 話, 這個命令可以在管道中進行更深層次的過濾和格式化.
more, less
 分頁顯示文本文件或 stdout, 一次一屏.可以用來過濾 stdout 的輸出 . . . 或一個腳本
 的輸出.
 more 命令的一個有趣的應用就是測試一個命令序列的執行, 來避免可能發生的糟糕的
 結果.
    1 ls /home/bozo | awk '{print "rm -rf " $1}' | more
    2 #                                            ^^^^
    3   
    4 # 檢測下邊(災難性的)命令行的效果:
    5 #      ls /home/bozo | awk '{print "rm -rf " $1}' | sh
    6 #      推入 shell 中執行 . . .
注意事項:
[1]  在這裏所討論的一個歸檔文件, 只不過是存儲在一個單一位置上的一些相關文件的
  集合.
[2]  tar czvf archive_name.tar.gz *  可以 包含當前工作目錄下的點文件. 這是一個
  未文檔化的 GNU tar 的"特徵".
[3]  這是一個對稱的塊密碼, 過去曾在單系統或本地網絡中用來加密文件, 用來對抗
  "public key" 密碼類, pgp 就是一個衆所周知的例子.
[4]  使用 -d 選項可以創建一個臨時的目錄.
 
12.6 通訊命令
------------- 下邊命令中的某幾個命令你會在 "追蹤垃圾郵件" 練習中找到其用法, 用來進行網絡數
據的轉換和分析.
信息與統計
host
 通過名字或 IP 地址來搜索一個互聯網主機的信息, 使用 DNS.
  bash$ host surfacemail.com
  surfacemail.com. has address 202.92.42.236
ipcalc
http://www.818198.com  Page 277
SHELL十三問
 顯示一個主機 IP 信息. 使用 -h 選項, ipcalc 將會做一個 DNS 的反向查詢, 通過 IP
 地址找到主機(服務器)名.
  bash$ ipcalc -h 202.92.42.236
  HOSTNAME=surfacemail.com
nslookup
 通過 IP 地址在一個主機上做一個互聯網的 "名字服務查詢". 事實上這與 ipcalc -h 或
 dig -x 等價. 這個命令既可以交互運行也可以非交互運行, 換句話說, 就是在腳本中運
 行.
 nslookup 命令據說已經慢慢被"忽視"了, 但是它還是有它的用處.
  bash$ nslookup -sil 66.97.104.180
  nslookup kuhleersparnis.ch
  Server:         135.116.137.2
  Address:        135.116.137.2#53
  Non-authoritative answer:
  Name:   kuhleersparnis.ch
dig
 域信息查詢. 與 nslookup 很相似, dig 在一個主機上做一個互聯網的 "名字服務查詢".
 這個命令既可以交互運行也可以非交互運行, 換句話說, 就是在腳本中運行.
 下邊是一些 dig 命令有趣的選項, +time=N 選項用來設置查詢超時爲 N 秒, +nofail
 選項用來持續查詢服務器直到收到一個響應, -x 選項會做反向地址查詢.
 比較下邊這3個命令的輸出, dig -x , ipcalc -h 和 nslookup.
  bash$ dig -x 81.9.6.2
  ;; Got answer:
  ;; ->>HEADER<<- opcode: QUERY, status: NXDOMAIN, id: 11649
  ;; flags: qr rd ra; QUERY: 1, ANSWER: 0, AUTHORITY: 1, ADDITIONAL: 0
  ;; QUESTION SECTION:
  ;2.6.9.81.in-addr.arpa.         IN      PTR
  ;; AUTHORITY SECTION:
  6.9.81.in-addr.arpa.    3600    IN      SOA     ns.eltel.net. noc.eltel.net.
  2002031705 900 600 86400 3600
  ;; Query time: 537 msec
  ;; SERVER: 135.116.137.2#53(135.116.137.2)
  ;; WHEN: Wed Jun 26 08:35:24 2002
  ;; MSG SIZE  rcvd: 91
Example 12-36 查找濫用的連接來報告垃圾郵件發送者
################################Start Script#######################################
 1 #!/bin/bash
 2 # spam-lookup.sh: 查找濫用的連接來報告垃圾郵件發送者.
 3 # 感謝 Michael Zick.
 4
 5 # 檢查命令行參數.
 6 ARGCOUNT=1
 7 E_WRONGARGS=65
 8 if [ $# -ne "$ARGCOUNT" ]
 9 then
10   echo "Usage: `basename $0` domain-name"
11   exit $E_WRONGARGS
12 fi
http://www.818198.com  Page 278
SHELL十三問
13
14
15 dig +short $1.contacts.abuse.net -c in -t txt
16 # 也試試:
17 #     dig +nssearch $1
18 #     儘量找到 "可信賴的名字服務器" 並且顯示 SOA 記錄.
19
20 # 下邊這句也可以:
21 #     whois -h whois.abuse.net $1
22 #           ^^ ^^^^^^^^^^^^^^^  指定主機.
23 #     使用這個命令也可以查找多個垃圾郵件發送者, 比如:"
24 #     whois -h whois.abuse.net $spamdomain1 $spamdomain2 . . .
25
26
27 #  練習:
28 #  -----29 #  擴展這個腳本的功能,
30 #+ 讓它可以自動發送 e-mail 來通知
31 #+ 需要對此負責的 ISP 的聯繫地址.
32 #  暗示: 使用 "mail" 命令.
33
34 exit $?
35
36 # spam-lookup.sh chinatietong.com
37 #                一個已知的垃圾郵件域.(譯者: 中國鐵通. . .)
38
39 # "[email protected]"
40 # "[email protected]"
41 # "[email protected]"
42
43
44 #  如果想找到這個腳本的一個更詳盡的版本,
45 #+ 請訪問 SpamViz 的主頁, http://www.spamviz.net/index.html.
################################End Script#########################################
Example 12-37 分析一個垃圾郵件域<rojy bug>
################################Start Script#######################################
  1 #! /bin/bash
  2 # is-spammer.sh: 鑑別一個垃圾郵件域
  3
  4 # $Id: is-spammer, v 1.4 2004/09/01 19:37:52 mszick Exp $
  5 # 上邊這行是 RCS ID 信息.
  6 #
  7 #  這是附件中捐獻腳本 is_spammer.bash
  8 #+ 的一個簡單版本.
  9
 10 # is-spammer <domain.name>
 11
 12 # 使用外部程序: 'dig'
http://www.818198.com  Page 279
SHELL十三問
 13 # 測試版本: 9.2.4rc5
 14
 15 # 使用函數.
 16 # 使用 IFS 來分析分配在數組中的字符串.
 17 # 檢查 e-mail 黑名單.
 18
 19 # 使用來自文本體中的 domain.name:
 20 # http://www.good_stuff.spammer.biz/just_ignore_everything_else
 21 #                       ^^^^^^^^^^^
 22 # 或者使用來自任意 e-mail 地址的 domain.name:
 23 # [email protected]
 24 #
 25 # 並將其作爲這個腳本的唯一參數.
 26 #(另: 你的 Inet 連接應該保證連接)
 27 #
 28 # 這樣, 在上邊兩個實例中調用這個腳本:
 29 #       is-spammer.sh spammer.biz
 30
 31
 32 # Whitespace == :Space:Tab:Line Feed:Carriage Return:
 33 WSP_IFS=$'\x20'$'\x09'$'\x0A'$'\x0D'
 34
 35 # No Whitespace == Line Feed:Carriage Return
 36 No_WSP=$'\x0A'$'\x0D'
 37
 38 # 域分隔符爲點分10進制 ip 地址
 39 ADR_IFS=${No_WSP}'.'
 40
 41 # 取得 dns 文本資源記錄.
 42 # get_txt <error_code> <list_query>
 43 get_txt() {
 44
 45     # 分析在"."中分配的 $1.
 46     local -a dns
 47     IFS=$ADR_IFS
 48     dns=( $1 )
 49     IFS=$WSP_IFS
 50     if [ "${dns[0]}" == '127' ]
 51     then
 52         # 查看此處是否有原因.
 53         echo $(dig +short $2 -t txt)
 54     fi
 55 }
 56
 57 # 取得 dns 地址資源記錄.
 58 # chk_adr <rev_dns> <list_server>
 59 chk_adr() {
 60     local reply
http://www.818198.com  Page 280
SHELL十三問
 61     local server
 62     local reason
 63
 64     server=${1}${2}
 65     reply=$( dig +short ${server} )
 66
 67     # 假設應答可能是一個錯誤碼 . . .
 68     if [ ${#reply} -gt 6 ]
 69     then
 70         reason=$(get_txt ${reply} ${server} )
 71         reason=${reason:-${reply}}
 72     fi
 73     echo ${reason:-' not blacklisted.'}
 74 }
 75
 76 # 需要從名字中取得 IP 地址.
 77 echo 'Get address of: '$1
 78 ip_adr=$(dig +short $1)
 79 dns_reply=${ip_adr:-' no answer '}
 80 echo ' Found address: '${dns_reply}
 81
 82 # 一個可用的應答至少是4個數字加上3個點.
 83 if [ ${#ip_adr} -gt 6 ]
 84 then
 85     echo
 86     declare query
 87
 88     # 分析點中的分配.
 89     declare -a dns
 90     IFS=$ADR_IFS
 91     dns=( ${ip_adr} )
 92     IFS=$WSP_IFS
 93
 94     # Reorder octets into dns query order.
 95     rev_dns="${dns[3]}"'.'"${dns[2]}"'.'"${dns[1]}"'.'"${dns[0]}"'.'
 96
 97 # 參見: http://www.spamhaus.org (Conservative, well maintained)
 98     echo -n 'spamhaus.org says: '
 99     echo $(chk_adr ${rev_dns} 'sbl-xbl.spamhaus.org')
100
101 # 參見: http://ordb.org (Open mail relays)
102     echo -n '   ordb.org  says: '
103     echo $(chk_adr ${rev_dns} 'relays.ordb.org')
104
105 # 參見: http://www.spamcop.net/ (你可以在這裏報告 spammer)
106     echo -n ' spamcop.net says: '
107     echo $(chk_adr ${rev_dns} 'bl.spamcop.net')
108
http://www.818198.com  Page 281
SHELL十三問
109 # # # 其他的黑名單操作 # # #
110
111 # 參見: http://cbl.abuseat.org.
112     echo -n ' abuseat.org says: '
113     echo $(chk_adr ${rev_dns} 'cbl.abuseat.org')
114
115 # 參見: http://dsbl.org/usage (Various mail relays)
116     echo
117     echo 'Distributed Server Listings'
118     echo -n '       list.dsbl.org says: '
119     echo $(chk_adr ${rev_dns} 'list.dsbl.org')
120
121     echo -n '   multihop.dsbl.org says: '
122     echo $(chk_adr ${rev_dns} 'multihop.dsbl.org')
123
124     echo -n 'unconfirmed.dsbl.org says: '
125     echo $(chk_adr ${rev_dns} 'unconfirmed.dsbl.org')
126
127 else
128     echo
129     echo 'Could not use that address.'
130 fi
131
132 exit 0
133
134 # 練習:
135 # -----136
137 # 1) 檢查腳本的參數,
138 #    並且如果必要的話使用合適的錯誤消息退出.
139
140 # 2) 檢查調用這個腳本的時候是否在線,
141 #    並且如果必要的話使用合適的錯誤消息退出.
142
143 # 3) Substitute generic variables for "hard-coded" BHL domains.
144
145 # 4) 通過對 'dig' 命令使用 "+time=" 選項
146      來給這個腳本設置一個暫停.
################################End Script#########################################
 想獲得比上邊這個腳本更詳細的版本, 參見 Example A-27.
traceroute
 跟蹤包發送到遠端主機過程中的路由信息. 這個命令在 LAN, WAN, 或者在 Internet 上都
 可以正常工作. 遠端主機可以通過 IP 地址來指定. 這個命令的輸出也可以通過管道中的
 grep 或 sed 命令來過濾.
  bash$ traceroute 81.9.6.2
  traceroute to 81.9.6.2 (81.9.6.2), 30 hops max, 38 byte packets
  1  tc43.xjbnnbrb.com (136.30.178.8)  191.303 ms  179.400 ms  179.767 ms
  2  or0.xjbnnbrb.com (136.30.178.1)  179.536 ms  179.534 ms  169.685 ms
http://www.818198.com  Page 282
SHELL十三問
  3  192.168.11.101 (192.168.11.101)  189.471 ms  189.556 ms *
  ...
ping
 廣播一個 "ICMP ECHO_REQUEST" 包到其他主機上, 既可以是本地網絡也可以使遠端網絡.
 這是一個測試網絡連接的診斷工具, 應該小心使用.
 一個成功的 ping 返回的 退出碼 爲 0. 可以用在腳本的測試語句中.
  bash$ ping localhost
  PING localhost.localdomain (127.0.0.1) from 127.0.0.1 : 56(84) bytes of data.
  64 bytes from localhost.localdomain (127.0.0.1): icmp_seq=0 ttl=255 time=709 usec
  64 bytes from localhost.localdomain (127.0.0.1): icmp_seq=1 ttl=255 time=286 usec
  --- localhost.localdomain ping statistics ---  2 packets transmitted, 2 packets received, 0% packet loss
  round-trip min/avg/max/mdev = 0.286/0.497/0.709/0.212 ms
whois
 執行DNS (域名系統) 查詢lookup. -h 選項允許指定需要查詢的特定的 whois 服務器.
 參見 Example 4-6 和 Example 12-36.
finger
 取得網絡上的用戶信息. 另外這個命令可以顯示一個用戶的~/.plan, ~/.project, 和
 ~/.forward 文件, 如果存在的話.
  bash$ finger
  Login  Name           Tty      Idle  Login Time   Office     Office Phone
  bozo   Bozo Bozeman   tty1        8  Jun 25 16:59
  bozo   Bozo Bozeman   ttyp0          Jun 25 16:59
  bozo   Bozo Bozeman   ttyp1          Jun 25 17:07
 
 
 
  bash$ finger bozo
  Login: bozo                             Name: Bozo Bozeman
  Directory: /home/bozo                   Shell: /bin/bash
  Office: 2355 Clown St., 543-1234
  On since Fri Aug 31 20:13 (MST) on tty1    1 hour 38 minutes idle
  On since Fri Aug 31 20:13 (MST) on pts/0   12 seconds idle
  On since Fri Aug 31 20:13 (MST) on pts/1
  On since Fri Aug 31 20:31 (MST) on pts/2   1 hour 16 minutes idle
  No mail.
  No Plan.
 處於安全上的考慮, 許多網絡都禁用了 finger 以及和它相關的幽靈進程. [1]
chfn
 修改 finger 命令所顯示出來的用戶信息.
vrfy
 驗證一個互聯網的 e-mail 地址.
遠端主機接入
sx, rx
 sx 和 rx 命令使用 xmodem 協議, 設置服務來向遠端主機傳輸文件和接收文件. 這些都
 是通訊安裝包的一般部分, 比如 minicom.
sz, rz
 sz 和 rz 命令使用 zmodem 協議, 設置服務來向遠端主機傳輸文件和接收文件. zmodem
http://www.818198.com  Page 283
SHELL十三問
 協議在某些方面比 xmodem強, 比如使用更快的的傳輸波特率, 並且可以對中斷的文件進
 行續傳.與 sx 一樣 rx, 這些都是通訊安裝包的一般部分.
ftp
 向遠端服務器上傳或下載的工具和協議. 一個ftp會話可以寫到腳本中自動運行. (見
 Example 17-6, Example A-4, 和 Example A-13).
uucp, uux, cu
 uucp: UNIX 到 UNIX 拷貝. 這是一個通訊安裝包, 目的是爲了在 UNIX 服務器之間傳輸
 文件. 使用 shell 腳本來處理 uucp 命令序列是一種有效的方法.
 因爲互聯網和電子郵件的出現, uucp 現在看起來已經很落伍了, 但是這個命令在互聯網
 連接不可用或者不適合使用的地方, 這個命令還是可以完美的運行. uucp 的優點就是它
 的容錯性, 即使有一個服務將拷貝操作中斷了, 那麼當連接恢復的時候, 這個命令還是
 可以在中斷的地方續傳.
 ---
 uux: UNIX 到 UNIX 執行. 在遠端系統上執行一個命令.這個命令是 uucp 包的一部分.
 ---
 cu: Call Up 一個遠端系統並且作爲一個簡單終端進行連接. 這是一個 telnet 的縮減
 版本. 這個命令是 uucp 包的一部分.
telnet
 連接遠端主機的工具和協議.
 注意:telnet 協議本身包含安全漏洞, 因此我們應該適當的避免使用.
wget
 wget 工具使用非交互的形式從 web 或 ftp 站點上取得或下載文件. 在腳本中使用正好.
    1 wget -p http://www.xyz23.com/file01.html
    2 #  The -p or --page-requisite 選項將會使得 wget 取得顯示指定頁時
    3 #+ 所需要的所有文件.(譯者: 比如內嵌圖片和樣式表等).
    4
    5 wget -r ftp://ftp.xyz24.net/~bozo/project_files/ -O $SAVEFILE
    6 #  -r 選項將會遞歸的從指定站點
    7 #+ 上下載所有連接.
Example 12-38 獲得一份股票報價
################################Start Script#######################################
 1 #!/bin/bash
 2 # quote-fetch.sh: 下載一份股票報價.
 3
 4
 5 E_NOPARAMS=66
 6
 7 if [ -z "$1" ]  # 必須指定需要獲取的股票(代號).
 8   then echo "Usage: `basename $0` stock-symbol"
 9   exit $E_NOPARAMS
10 fi
11
12 stock_symbol=$1
13
14 file_suffix=.html
15 # 獲得一個 HTML 文件, 所以要正確命名它.
16 URL='http://finance.yahoo.com/q?s='
17 # Yahoo 金融板塊, 後綴是股票查詢.
http://www.818198.com  Page 284
SHELL十三問
18
19 # -----------------------------------------------------------20 wget -O ${stock_symbol}${file_suffix} "${URL}${stock_symbol}"
21 # -----------------------------------------------------------22
23
24 # 在 http://search.yahoo.com 上查詢相關材料:
25 # -----------------------------------------------------------26 # URL="http://search.yahoo.com/search?fr=ush-news&p=${query}"
27 # wget -O "$savefilename" "${URL}"
28 # -----------------------------------------------------------29 # 保存相關 URL 的列表.
30
31 exit $?
32
33 # 練習:
34 # -----35 #
36 # 1) 添加一個測試來驗證用戶正在線.
37 #    (暗示: 對 "ppp" 或 "connect" 來分析 'ps -ax' 的輸出.
38 #
39 # 2) 修改這個腳本, 讓這個腳本具有獲得本地天氣預報的能力,
40 #+   將用戶的 zip code 作爲參數.
################################End Script#########################################
 參見 Example A-29 和 Example A-30.
lynx
 lynx 是一個網頁瀏覽器, 也是一個文件瀏覽器. 它可以(通過使用 -dump 選項)在腳本中
 使用. 它的作用是可以從 Web 或 ftp 站點上非交互的獲得文件.
    1 lynx -dump http://www.xyz23.com/file01.html >$SAVEFILE
 使用 -traversal 選項, lynx 將從參數中指定的 HTTP URL 開始, 遍歷指定服務器上的
 所有鏈接. 如果與 -crawl 選項一起用的話, 將會把每個輸出的頁面文本都放到一個 log
 文件中.
rlogin
 遠端登陸, 在遠端的主機上開啓一個會話. 這個命令存在安全隱患, 所以要使用 ssh 來
 代替.
rsh
 遠端 shell, 在遠端的主機上執行命令. 這個命令存在安全隱患, 所以要使用 ssh 來代
 替.
rcp
 遠端拷貝, 在網絡上的不同主機間拷貝文件.
rsync
 遠端同步, 在網絡上的不同主機間(同步)更新文件.
  bash$ rsync -a ~/sourcedir/*txt /node1/subdirectory/
Example 12-39 更新 Fedora 4 <rojy bug> 
################################Start Script#######################################
  1 #!/bin/bash
  2 # fc4upd.sh
  3
http://www.818198.com  Page 285
SHELL十三問
  4 # 腳本作者: Frank Wang.
  5 # 本書作者作了少量修改.
  6 # 授權在本書中使用.
  7
  8
  9 #  使用 rsync 命令從鏡像站點上下載 Fedora 4 的更新.
 10 #  爲了節省空間, 如果有多個版本存在的話,
 11 #+ 只下載最新的包.
 12
 13 URL=rsync://distro.ibiblio.org/fedora-linux-core/updates/
 14 # URL=rsync://ftp.kddilabs.jp/fedora/core/updates/
 15 # URL=rsync://rsync.planetmirror.com/fedora-linux-core/updates/
 16
 17 DEST=${1:-/var/www/html/fedora/updates/}
 18 LOG=/tmp/repo-update-$(/bin/date +%Y-%m-%d).txt
 19 PID_FILE=/var/run/${0##*/}.pid
 20
 21 E_RETURN=65        # 某些意想不到的錯誤.
 22
 23
 24 # 一搬 rsync 選項
 25 # -r: 遞歸下載
 26 # -t: 保存時間
 27 # -v: verbose
 28
 29 OPTS="-rtv --delete-excluded --delete-after --partial"
 30
 31 # rsync include 模式
 32 # Leading slash causes absolute path name match.
 33 INCLUDE=(
 34     "/4/i386/kde-i18n-Chinese*"
 35 #   ^                         ^
 36 # 雙引號是必須的, 用來防止file globbing.
 37 )
 38
 39
 40 # rsync exclude 模式
 41 # 使用 "#" 臨時註釋掉一些不需要的包.
 42 EXCLUDE=(
 43     /1
 44     /2
 45     /3
 46     /testing
 47     /4/SRPMS
 48     /4/ppc
 49     /4/x86_64
 50     /4/i386/debug
 51    "/4/i386/kde-i18n-*"
http://www.818198.com  Page 286
SHELL十三問
 52    "/4/i386/openoffice.org-langpack-*"
 53    "/4/i386/*i586.rpm"
 54    "/4/i386/GFS-*"
 55    "/4/i386/cman-*"
 56    "/4/i386/dlm-*"
 57    "/4/i386/gnbd-*"
 58    "/4/i386/kernel-smp*"
 59 #  "/4/i386/kernel-xen*"
 60 #  "/4/i386/xen-*"
 61 )
 62
 63
 64 init () {
 65     # 讓管道命令返回可能的 rsync 錯誤, 比如, 網絡延時(stalled network).
 66     set -o pipefail
 67
 68     TMP=${TMPDIR:-/tmp}/${0##*/}.$$     # 保存精煉的下載列表.
 69     trap "{
 70         rm -f $TMP 2>/dev/null
 71     }" EXIT                             # 刪除存在的臨時文件.
 72 }
 73
 74
 75 check_pid () {
 76 # 檢查進程是否存在.
 77     if [ -s "$PID_FILE" ]; then
 78         echo "PID file exists. Checking ..."
 79         PID=$(/bin/egrep -o "^[[:digit:]]+" $PID_FILE)
 80         if /bin/ps --pid $PID &>/dev/null; then
 81             echo "Process $PID found. ${0##*/} seems to be running!"
 82            /usr/bin/logger -t ${0##*/} \
 83                  "Process $PID found. ${0##*/} seems to be running!"
 84             exit $E_RETURN
 85         fi
 86         echo "Process $PID not found. Start new process . . ."
 87     fi
 88 }
 89
 90
 91 #  根據上邊的模式,
 92 #+ 設置整個文件的更新範圍, 從 root 或 $URL 開始.
 93 set_range () {
 94     include=
 95     exclude=
 96     for p in "${INCLUDE[@]}"; do
 97         include="$include --include \"$p\""
 98     done
 99
http://www.818198.com  Page 287
SHELL十三問
100     for p in "${EXCLUDE[@]}"; do
101         exclude="$exclude --exclude \"$p\""
102     done
103 }
104
105
106 # 獲得並提煉 rsync 更新列表.
107 get_list () {
108     echo $$ > $PID_FILE || {
109         echo "Can't write to pid file $PID_FILE"
110         exit $E_RETURN
111     }
112
113     echo -n "Retrieving and refining update list . . ."
114
115     # 獲得列表 -- 爲了作爲單個命令來運行 rsync 需要 'eval'.
116     # $3 和 $4 是文件創建的日期和時間.
117     # $5 是完整的包名字.
118     previous=
119     pre_file=
120     pre_date=0
121     eval /bin/nice /usr/bin/rsync \
122         -r $include $exclude $URL | \
123         egrep '^dr.x|^-r' | \
124         awk '{print $3, $4, $5}' | \
125         sort -k3 | \
126         { while read line; do
127             # 獲得這段運行的秒數, 過濾掉不用的包.
128             cur_date=$(date -d "$(echo $line | awk '{print $1, $2}')" +%s)
129             #  echo $cur_date
130
131             # 取得文件名.
132             cur_file=$(echo $line | awk '{print $3}')
133             #  echo $cur_file
134
135             # 如果可能的話, 從文件名中取得 rpm 的包名字.
136             if [[ $cur_file == *rpm ]]; then
137                 pkg_name=$(echo $cur_file | sed -r -e \
138                     's/(^([^_-]+[_-])+)[[:digit:]]+\..*[_-].*$/\1/')
139             else
140                 pkg_name=
141             fi
142             # echo $pkg_name
143
144             if [ -z "$pkg_name" ]; then   #  如果不是一個 rpm 文件,
145                 echo $cur_file >> $TMP    #+ 然後添加到下載列表裏.
146             elif [ "$pkg_name" != "$previous" ]; then   # 發現一個新包.
147                 echo $pre_file >> $TMP                  # 輸出最新的文件.
http://www.818198.com  Page 288
SHELL十三問
148                 previous=$pkg_name                      # 保存當前狀態.
149                 pre_date=$cur_date
150                 pre_file=$cur_file
151             elif [ "$cur_date" -gt "$pre_date" ]; then  #  如果是相同的包, 但是更新一些,
152                 pre_date=$cur_date                      #+ 那麼就更新最新的.
153                 pre_file=$cur_file
154             fi
155             done
156             echo $pre_file >> $TMP                      #  TMP 現在包含所有
157                                                         #+ 提煉過的列表.
158             # echo "subshell=$BASH_SUBSHELL"
159
160     }       # 這裏的打括號是爲了讓最後這句"echo $pre_file >> $TMP"
161             # 也能與整個循環一起放到同一個子 shell ( 1 )中.
162
163     RET=$?  # 取得管道命令的返回碼.
164
165     [ "$RET" -ne 0 ] && {
166         echo "List retrieving failed with code $RET"
167         exit $E_RETURN
168     }
169
170     echo "done"; echo
171 }
172
173 # 真正的 rsync 的下載部分.
174 get_file () {
175
176     echo "Downloading..."
177     /bin/nice /usr/bin/rsync \
178         $OPTS \
179         --filter "merge,+/ $TMP" \
180         --exclude '*'  \
181         $URL $DEST     \
182         | /usr/bin/tee $LOG
183
184     RET=$?
185
186         #  --filter merge,+/ is crucial for the intention.
187         #  + modifier means include and / means absolute path.
188         #  Then sorted list in $TMP will contain ascending dir name and
189         #+ prevent the following --exclude '*' from "shortcutting the circuit."
190
191     echo "Done"
192
193     rm -f $PID_FILE 2>/dev/null
194
195     return $RET
http://www.818198.com  Page 289
SHELL十三問
196 }
197
198 # -------199 # Main
200 init
201 check_pid
202 set_range
203 get_list
204 get_file
205 RET=$?
206 # -------207
208 if [ "$RET" -eq 0 ]; then
209     /usr/bin/logger -t ${0##*/} "Fedora update mirrored successfully."
210 else
211     /usr/bin/logger -t ${0##*/} "Fedora update mirrored with failure code: $RET"
212 fi
213
214 exit $RET
################################End Script#########################################
 使用 rcp, rsync, 和其他一些有安全問題的類似工具, 並將這些工具用在 shell 腳本中
 是不明智的. 應該考慮使用 ssh, scp, 或者一個 expect 腳本來代替這些不安全的工具.
ssh
 安全 shell, 登陸遠端主機並在其上運行命令. 這個工具具有身份認證和加密的功能, 可
 以安全的替換 telnet, rlogin, rcp, 和 rsh 等工具. 參見 man頁 來獲取詳細信息.
Example 12-40 使用 ssh
################################Start Script#######################################
 1 #!/bin/bash
 2 # remote.bash: 使用 ssh.
 3
 4 # 這個例子是 Michael Zick 編寫的.
 5 # 授權使用.
 6
 7
 8 #   假設:
 9 #   -----10 #   fd-2(文件描述符2) 並沒有被拋棄 ( '2>/dev/null' ).
11 #   ssh/sshd 假設 stderr ('2') 將會被顯示給用戶.
12 #
13 #   sshd 正運行在你的機器上.
14 #   對於大多數 '標準' 的發行版, 是應該有的,
15 #+  並且沒有一些稀奇古怪的 ssh-keygen.
16
17 # 在你的機器上從命令行中試一下 ssh:
18 #
19 # $ ssh $HOSTNAME
20 # 不同特殊的準備, 你將被要求輸入你的密碼.
21 #   輸入密碼
http://www.818198.com  Page 290
SHELL十三問
22 #   完成後,  $ exit
23 #
24 # 好使了麼? 如果好使了, 你可以做好準備來獲取更多的樂趣了.
25
26 # 在你的機器上用 'root'身份來試試 ssh:
27 #
28 #   $  ssh -l root $HOSTNAME
29 #   當詢問密碼時, 輸入 root 的密碼, 別輸入你的密碼.
30 #          Last login: Tue Aug 10 20:25:49 2004 from localhost.localdomain
31 #   完成後鍵入 'exit'.
32
33 #  上邊的動作將會給你一個交互的shell.
34 #  在 'single command' 模式下建立 sshd 是可能的, <rojy bug>
35 #+ 不過這已經超出本例的範圍了.
36 #  唯一需要注意的事情是下面都可以工作在
37 #+ 'single command' 模式.
38
39
40 # 一個基本的寫輸出(本地)命令.
41
42 ls -l
43
44 # 現在在遠端機器上使用同樣的基本命令.
45 # 使用一套不同的 'USERNAME' 和 'HOSTNAME' :
46 USER=${USERNAME:-$(whoami)}
47 HOST=${HOSTNAME:-$(hostname)}
48
49 #  現在在遠端主機上運行上邊的命令行命令,
50 #+ 當然, 所有的傳輸都被加密了.
51
52 ssh -l ${USER} ${HOST} " ls -l "
53
54 #  期望的結果就是在遠端主機上列出你的
55 #+ username 主目錄的所有文件.
56 #  如果想看點不一樣的, 那就
57 #+ 在別的地方運行這個腳本, 別再你的主目錄上運行這個腳本.
58
59 #  換句話說, Bash 命令已經作爲一個引用行
60 #+ 被傳遞到遠端的shell 中了,這樣就可以在遠端的機器上運行它了.
61 #  在這種情況下, sshd 代表你運行了 ' bash -c "ls -l" '.
62
63 #  對於每個命令行如果想不輸入密碼的話,
64 #+ 對於這種類似的議題, 可以參閱
65 #+    man ssh
66 #+    man ssh-keygen
67 #+    man sshd_config.
68
69 exit 0
http://www.818198.com  Page 291
SHELL十三問
################################End Script#########################################
 注意: 在循環中, ssh 可能會引起意想不到的異常行爲. 根據comp.unix 上的shell文檔
   Usenet post , ssh 繼承了循環的標準輸入.爲了解決這個問題, 使用 ssh 的 -n
   或者 -f 選項.
   感謝 Jason Bechtel, 指出這點.
scp
 安全拷貝, 在功能上與 rcp 很相似, 就是在2個不同的網絡主機之間拷貝文件, 但是要通
 過鑑權的方式, 並且使用與 ssh 類似的安全層.
Local Network
write
 這是一個端到端通訊的工具. 這個工具可以從你的終端上(console 或者 xterm)發送整行
 到另一個用戶的終端上. mesg 命令當然也可以用來對於一個終端的寫權限
 因爲 write 是需要交互的, 所以這個命令通常不使用在腳本中.
netconfig
 用來配置網絡適配器(使用 DHCP)的命令行工具. 這個命令對於紅帽發行版來說是內置的.
 
Mail
mail
 發送或讀取 e-mail 消息.
 如果把這個命令行的 mail 客戶端當成一個腳本中的命令來使用的話, 效果非常好.
Example 12-41 一個可以mail自己的腳本
################################Start Script#######################################
 1 #!/bin/sh
 2 # self-mailer.sh: mail自己的腳本
 3
 4 adr=${1:-`whoami`}     # 如果不指定的話, 默認是當前用戶.
 5 #  鍵入 'self-mailer.sh [email protected]'
 6 #+ 發送這個腳本到這個地址.
 7 #  如果只鍵入 'self-mailer.sh' (不給參數) 的話, 那麼這腳本就會被髮送給
 8 #+ 調用者, 比如 [email protected].
 9 #
10 #  如果想了解 ${parameter:-default} 結構的更多細節,
11 #+ 請參見第9章 變量重遊中的
12 #+ 第3節 參數替換.
13
14 # ============================================================================
15   cat $0 | mail -s "Script \"`basename $0`\" has mailed itself to you." "$adr"
16 # ============================================================================
17
18 # --------------------------------------------19 #  來自 self-mailing 腳本的一份祝福.
20 #  一個喜歡惡搞的傢伙運行了這個腳本,
21 #+ 這導致了他自己收到了這份mail.
22 #  顯然的, 有些人確實沒什麼事好做,
23 #+ 就只能浪費他們自己的時間玩了.
24 # --------------------------------------------25
26 echo "At `date`, script \"`basename $0`\" mailed to "$adr"."
http://www.818198.com  Page 292
SHELL十三問
27
28 exit 0
################################End Script#########################################
mailto
 與 mail 命令很相似, mailto 命令可以使用命令行或在腳本中發送 e-mail 消息. 然而,
 mailto 命令也允許發送 MIME (多媒體) 消息.
vacation
 這個工具可以自動回覆 e-mail 給發送者, 表示郵件的接受者正在度假暫時無法收到郵件.
 這個工具與 sendmail 一起運行於網絡上, 並且這個工具不支持撥號的 POPmail 帳號.
注意事項:
[1]  一個幽靈進程指的是並未附加在終端會話中的後臺進程. 幽靈進程 在指定的時間執
  行指定的服務, 或者由特定的事件出發來執行指定的服務.
12.7 終端控制命令
-----------------影響控制檯或終端的命令
tput
 初始化終端或者從 terminfo data 中取得終端信息. 不同的選項允許特定的終端操作.
 tput clear 與下邊的 clear 等價. tput reset 與下邊的 reset 等價. tput sgr0 也可
 以重置終端, 但是並不清除屏幕.
  bash$ tput longname
  xterm terminal emulator (XFree86 4.0 Window System)
 使用 tput cup X Y 將會把光標移動到當前終端的(X,Y)座標上. 使用這個命令之前一邊
 都要先使用一下 clear 命令, 把屏幕清除一下.
 注意: stty 提供了一個更強力的命令專門用來設置如何控制終端.
infocmp
 這個命令會打印出大量的當前終端的信息. 事實上它是引用了 terminfo 數據庫.
  bash$ infocmp
  #       通過來自於文件的 infocmp 顯示出來:
  /usr/share/terminfo/r/rxvt
  rxvt|rxvt terminal emulator (X Window System),
    am, bce, eo, km, mir, msgr, xenl, xon,
    colors#8, cols#80, it#8, lines#24, pairs#64,
    acsc=``aaffggjjkkllmmnnooppqqrrssttuuvvwwxxyyzz{{||}}~~,
    bel=^G, blink=\E[5m, bold=\E[1m,
    civis=\E[?25l,
    clear=\E[H\E[2J, cnorm=\E[?25h, cr=^M,
    ...
reset
 重置終端參數並且清除屏幕. 與 clear 命令一樣, 光標和提示符將會重新出現在終端的
 左上角.
clear
 clear 命令只不過是簡單的清除控制檯或者 xterm 的屏幕. 光標和提示符將會重新出現
 在屏幕或者 xterm window 的左上角. 這個命令既可以用在命令行中也可以用在腳本中.
 參見 Example 10-25.
script
 這個工具將會記錄(保存到一個文件中)所有的用戶在控制檯下的或在 xterm window下的
 按鍵信息. 這其實就是創建了一個會話記錄.
12.8 數學計算命令
http://www.818198.com  Page 293
SHELL十三問
-----------------"Doing the numbers"
factor
 將一個正數分解爲多個素數.
  bash$ factor 27417
  27417: 3 13 19 37
bc
 Bash 不能處理浮點運算, 並且缺乏特定的一些操作,這些操作都是一些重要的計算功能.
 幸運的是, bc 可以解決這個問題.
 bc 不僅僅是個多功能靈活的精確的工具, 而且它還提供許多編程語言才具備的一些方便
 的功能.
 bc 比較類似於 C 語言的語法.
 因爲它是一個完整的 UNIX 工具, 所以它可以用在管道中, bc 在腳本中也是很常用的.
 這裏有一個簡單的使用 bc 命令的模版可以用來在計算腳本中的變量. 用在命令替換中.
     variable=$(echo "OPTIONS; OPERATIONS" | bc)
Example 12-42 按月償還貸款
################################Start Script#######################################
 1 #!/bin/bash
 2 # monthlypmt.sh: 計算按月償還貸款的數量.
 3
 4
 5 #  這份代碼是一份修改版本, 原始版本在 "mcalc" (貸款計算)包中,
 6 #+ 這個包的作者是 Jeff Schmidt 和 Mendel Cooper (本書作者).
 7 #   http://www.ibiblio.org/pub/Linux/apps/financial/mcalc-1.6.tar.gz  [15k]
 8
 9 echo
10 echo "Given the principal, interest rate, and term of a mortgage,"
11 echo "calculate the monthly payment."
12
13 bottom=1.0
14
15 echo
16 echo -n "Enter principal (no commas) "
17 read principal
18 echo -n "Enter interest rate (percent) "  # 如果是 12%, 那就鍵入 "12", 別輸入 ".12".
19 read interest_r
20 echo -n "Enter term (months) "
21 read term
22
23
24  interest_r=$(echo "scale=9; $interest_r/100.0" | bc) # 轉換成小數.
25                  # "scale" 指定了有效數字的個數.
26  
27
28  interest_rate=$(echo "scale=9; $interest_r/12 + 1.0" | bc)
29 
30
31  top=$(echo "scale=9; $principal*$interest_rate^$term" | bc)
http://www.818198.com  Page 294
SHELL十三問
32
33  echo; echo "Please be patient. This may take a while."
34
35  let "months = $term - 1"
36 # ====================================================================
37  for ((x=$months; x > 0; x--))
38  do
39    bot=$(echo "scale=9; $interest_rate^$x" | bc)
40    bottom=$(echo "scale=9; $bottom+$bot" | bc)
41 #  bottom = $(($bottom + $bot"))
42  done
43 # ====================================================================
44
45 # --------------------------------------------------------------------
46 #  Rick Boivie 給出了一個對上邊循環的修改,
47 #+ 這個修改更加有效率, 將會節省大概 2/3 的時間.
48
49 # for ((x=1; x <= $months; x++))
50 # do
51 #   bottom=$(echo "scale=9; $bottom * $interest_rate + 1" | bc)
52 # done
53
54
55 #  然後他又想出了一個更加有效率的版本,
56 #+ 將會節省 95% 的時間!
57
58 # bottom=`{
59 #     echo "scale=9; bottom=$bottom; interest_rate=$interest_rate"
60 #     for ((x=1; x <= $months; x++))
61 #     do
62 #          echo 'bottom = bottom * interest_rate + 1'
63 #     done
64 #     echo 'bottom'
65 #     } | bc`       # 在命令替換中嵌入一個 'for 循環'.
66 # --------------------------------------------------------------------------67 #  On the other hand, Frank Wang suggests:
68 #  bottom=$(echo "scale=9; ($interest_rate^$term-1)/($interest_rate-1)" | bc)
69
70 #  因爲 . . .
71 #  在循環後邊的算法
72 #+ 事實上是一個等比數列的求和公式.
73 #  求和公式是 e0(1-q^n)/(1-q),
74 #+ e0 是第一個元素 並且 q=e(n+1)/e(n)
75 #+ 和 n 是元素的數量.
76 # --------------------------------------------------------------------------77
78
79  # let "payment = $top/$bottom"
http://www.818198.com  Page 295
SHELL十三問
80  payment=$(echo "scale=2; $top/$bottom" | bc)
81  # 使用2位有效數字來表示美元和美分.
82 
83  echo
84  echo "monthly payment = \$$payment"  # 在總和的前邊顯示美元符號.
85  echo
86
87
88  exit 0
89
90
91  # 練習:
92  #   1) 處理輸入允許本金總數中的逗號.
93  #   2) 處理輸入允許按照百分號和小數點的形式輸入利率.
94  #   3) 如果你真正想好好編寫這個腳本,
95  #      那麼就擴展這個腳本讓它能夠打印出完整的分期付款表.
################################End Script#########################################
Example 12-43 數制轉換
################################Start Script#######################################
  1 #!/bin/bash
  2 ##########################################################################
  3 # 腳本       : base.sh - 用不同的數值來打印數字 (Bourne Shell)
  4 # 作者       : Heiner Steven ([email protected])
  5 # 日期       : 07-03-95
  6 # 類型       : 桌面
  7 # $Id: base.sh,v 1.2 2000/02/06 19:55:35 heiner Exp $
  8 # ==> 上邊這行是 RCS ID 信息.
  9 ##########################################################################
 10 # 描述
 11 #
 12 # Changes
 13 # 21-03-95 stv fixed error occuring with 0xb as input (0.2)
 14 ##########################################################################
 15
 16 # ==> 在本書中使用這個腳本通過了作者的授權.
 17 # ==> 註釋是本書作者添加的.
 18
 19 NOARGS=65
 20 PN=`basename "$0"`          # 程序名
 21 VER=`echo '$Revision: 1.2 $' | cut -d' ' -f2`  # ==> VER=1.2
 22
 23 Usage () {
 24     echo "$PN - print number to different bases, $VER (stv '95)
 25 usage: $PN [number ...]
 26
 27 If no number is given, the numbers are read from standard input.
 28 A number may be
 29     binary (base 2)  starting with 0b (i.e. 0b1100)
http://www.818198.com  Page 296
SHELL十三問
 30     octal (base 8)  starting with 0  (i.e. 014)
 31     hexadecimal (base 16) starting with 0x (i.e. 0xc)
 32     decimal   otherwise (i.e. 12)" >&2
 33     exit $NOARGS
 34 }   # ==> 打印出用法信息的函數.
 35
 36 Msg () {
 37     for i   # ==> 省略 [list] .
 38     do echo "$PN: $i" >&2
 39     done
 40 }
 41
 42 Fatal () { Msg "$@"; exit 66; }
 43
 44 PrintBases () {
 45     # 決定數值的數制
 46     for i      # ==> 省略 [list]...
 47     do         # ==> 所以是對命令行參數進行操作.
 48  case "$i" in
 49      0b*)  ibase=2;; # 2進制
 50      0x*|[a-f]*|[A-F]*) ibase=16;; # 16進制
 51      0*)   ibase=8;; # 8進制
 52      [1-9]*)  ibase=10;; # 10進制
 53      *)
 54   Msg "illegal number $i - ignored"
 55   continue;;
 56  esac
 57
 58  # 去掉前綴, 將16進制數字轉換爲大寫(bc需要大寫)
 59  number=`echo "$i" | sed -e 's:^0[bBxX]::' | tr '[a-f]' '[A-F]'`
 60  # ==>使用":" 作爲sed分隔符, 而不使用"/".
 61
 62  # 將數字轉換爲10進制
 63  dec=`echo "ibase=$ibase; $number" | bc`  # ==> 'bc' 是個計算工具.
 64  case "$dec" in
 65      [0-9]*) ;;    # 數字沒問題
 66      *)  continue;;   # 錯誤: 忽略
 67  esac
 68
 69  # 在一行上打印所有的轉換後的數字.
 70  # ==> 'here document' 提供命令列表給'bc'.
 71  echo `bc <<!
 72      obase=16; "hex="; $dec
 73      obase=10; "dec="; $dec
 74      obase=8;  "oct="; $dec
 75      obase=2;  "bin="; $dec
 76 !
 77     ` | sed -e 's: : :g'
http://www.818198.com  Page 297
SHELL十三問
 78
 79     done
 80 }
 81
 82 while [ $# -gt 0 ]
 83 # ==>  這裏必須使用一個 "while 循環",
 84 # ==>+ 因爲所有的 case 都可能退出循環或者
 85 # ==>+ 結束腳本.
 86 # ==> (感謝, Paulo Marcel Coelho Aragao.)
 87 do
 88     case "$1" in
 89  --)     shift; break;;
 90  -h)     Usage;;                 # ==> 幫助信息.
 91  -*)     Usage;;
 92          *)     break;;   # 第一個數字
 93     esac   # ==> 對於非法輸入更嚴格檢查是非常有用的.
 94     shift
 95 done
 96
 97 if [ $# -gt 0 ]
 98 then
 99     PrintBases "$@"
100 else     # 從標準輸入中讀取
101     while read line
102     do
103  PrintBases $line
104     done
105 fi
106
107
108 exit 0
################################End Script#########################################
 調用 bc 的另一種可選的方法就是使用 here document ,並把它嵌入到 命令替換 塊中.
 當一個腳本需要將一個選項列表和多個命令傳遞到 bc 中時, 這種方法就顯得非常合適.
    1 variable=`bc << LIMIT_STRING
    2 options
    3 statements
    4 operations
    5 LIMIT_STRING
    6 `
    7
    8 ...or...
    9
   10
   11 variable=$(bc << LIMIT_STRING
   12 options
   13 statements
   14 operations
http://www.818198.com  Page 298
SHELL十三問
   15 LIMIT_STRING
   16 )
Example 12-44 使用 "here document" 來調用 bc
################################Start Script#######################################
 1 #!/bin/bash
 2 # 使用命令替換來調用 'bc'
 3 # 並與 'here document' 相結合.
 4
 5
 6 var1=`bc << EOF
 7 18.33 * 19.78
 8 EOF
 9 `
10 echo $var1       # 362.56
11
12
13 #  $( ... ) 這種標記法也可以.
14 v1=23.53
15 v2=17.881
16 v3=83.501
17 v4=171.63
18
19 var2=$(bc << EOF
20 scale = 4
21 a = ( $v1 + $v2 )
22 b = ( $v3 * $v4 )
23 a * b + 15.35
24 EOF
25 )
26 echo $var2       # 593487.8452
27
28
29 var3=$(bc -l << EOF
30 scale = 9
31 s ( 1.7 )
32 EOF
33 )
34 # 返回弧度爲1.7的正弦.
35 # "-l" 選項將會調用 'bc' 算數庫.
36 echo $var3       # .991664810
37
38
39 # 現在, 在函數中試一下...
40 hyp=             # 聲明全局變量.
41 hypotenuse ()    # 計算直角三角形的斜邊.
42 {
43 hyp=$(bc -l << EOF
44 scale = 9
http://www.818198.com  Page 299
SHELL十三問
45 sqrt ( $1 * $1 + $2 * $2 )
46 EOF
47 )
48 # 不幸的是, 不能從bash 函數中返回浮點值.
49 }
50
51 hypotenuse 3.68 7.31
52 echo "hypotenuse = $hyp"    # 8.184039344
53
54
55 exit 0
################################End Script#########################################
Example 12-45 計算圓周率
################################Start Script#######################################
  1 #!/bin/bash
  2 # cannon.sh: 通過開炮來取得近似的圓周率值.
  3
  4 # 這事實上是一個"Monte Carlo"蒙特卡洛模擬的非常簡單的實例:
  5 #+ 蒙特卡洛模擬是一種由現實事件抽象出來的數學模型,
  6 #+ 由於要使用隨機抽樣統計來估算數學函數, 所以使用僞隨機數來模擬真正的隨機.
  7
  8 #  想象有一個完美的正方形土地, 邊長爲10000個單位.
  9 #  在這塊土地的中間有一個完美的圓形湖,
 10 #+ 這個湖的直徑是10000個單位.
 11 #  這塊土地的絕大多數面積都是水, 當然只有4個角上有一些土地.
 12 #  (可以把這個湖想象成爲使這個正方形的內接圓.)
 13 #
 14 #  我們將使用老式的大炮和鐵炮彈
 15 #+ 向這塊正方形的土地上開炮.
 16 #  所有的炮彈都會擊中這塊正方形土地的某個地方.
 17 #+ 或者是打到湖上, 或者是打到4個角的土地上.
 18 #  因爲這個湖佔據了這個區域大部分地方,
 19 #+ 所以大部分的炮彈都會"撲通"一聲落到水裏.
 20 #  而只有很少的炮彈會"砰"的一聲落到4個
 21 #+ 角的土地上.
 22 #
 23 #  如果我們發出的炮彈足夠隨機的落到這塊正方形區域中的話,
 24 #+ 那麼落到水裏的炮彈與打出炮彈的總數的比率,
 25 #+ 大概非常接近於 PI/4.
 26 #
 27 #  原因是所有的炮彈事實上都
 28 #+ 打在了這個土地的右上角,
 29 #+ 也就是, 笛卡爾座標系的第一象限.
 30 #  (之前的解釋只是一個簡化.)
 31 #
 32 #  理論上來說, 如果打出的炮彈越多, 就越接近這個數字.
 33 #  然而, 對於shell 腳本來說一定會作些讓步的,
 34 #+ 因爲它肯定不能和那些內建就支持浮點運算的編譯語言相比.
http://www.818198.com  Page 300
SHELL十三問
 35 #  當然就會降低精度.
 36
 37
 38 DIMENSION=10000  # 這塊土地的邊長.
 39                  # 這也是所產生的隨機整數的上限.
 40
 41 MAXSHOTS=1000    # 開炮次數.
 42                  # 10000 或更多次的話, 效果應該更好, 但有點太浪費時間了.
 43 PMULTIPLIER=4.0  # 接近於 PI 的比例因子.
 44
 45 get_random ()
 46 {
 47 SEED=$(head -1 /dev/urandom | od -N 1 | awk '{ print $2 }')
 48 RANDOM=$SEED                                  #  來自於 "seeding-random.sh"
 49                                               #+ 的例子腳本.
 50 let "rnum = $RANDOM % $DIMENSION"             #  範圍小於 10000.
 51 echo $rnum
 52 }
 53
 54 distance=        # 聲明全局變量.
 55 hypotenuse ()    # 從 "alt-bc.sh" 例子來的,
 56 {                # 計算直角三角形的斜邊的函數.
 57 distance=$(bc -l << EOF
 58 scale = 0
 59 sqrt ( $1 * $1 + $2 * $2 )
 60 EOF
 61 )
 62 #  設置 "scale" 爲 0 , 好讓結果四捨五入爲整數值,
 63 #+ 這是這個腳本中必須折中的一個地方.
 64 #  不幸的是, 這將降低模擬的精度.
 65 }
 66
 67
 68 # main() {
 69
 70 # 初始化變量.
 71 shots=0
 72 splashes=0
 73 thuds=0
 74 Pi=0
 75
 76 while [ "$shots" -lt  "$MAXSHOTS" ]           # 主循環.
 77 do
 78
 79   xCoord=$(get_random)                        # 取得隨機的 X 與 Y 座標.
 80   yCoord=$(get_random)
 81   hypotenuse $xCoord $yCoord                  #  直角三角形斜邊 =
 82                                               #+ distance.
http://www.818198.com  Page 301
SHELL十三問
 83   ((shots++))
 84
 85   printf "#%4d   " $shots
 86   printf "Xc = %4d  " $xCoord
 87   printf "Yc = %4d  " $yCoord
 88   printf "Distance = %5d  " $distance         #  到湖中心的
 89                                               #+ 距離 -- 90                                               #  起始座標點 -- 91                                               #+  (0,0).
 92
 93   if [ "$distance" -le "$DIMENSION" ]
 94   then
 95     echo -n "SPLASH!  "
 96     ((splashes++))
 97   else
 98     echo -n "THUD!    "
 99     ((thuds++))
100   fi
101
102   Pi=$(echo "scale=9; $PMULTIPLIER*$splashes/$shots" | bc)
103   # 將比例乘以 4.0.
104   echo -n "PI ~ $Pi"
105   echo
106
107 done
108
109 echo
110 echo "After $shots shots, PI looks like approximately $Pi."
111 # 如果不太準的話, 那麼就提高一下運行的次數. . .
112 # 可能是由於運行錯誤和隨機數隨機程度不高造成的.
113 echo
114
115 # }
116
117 exit 0
118
119 #  要想知道一個shell腳本到底適不適合作爲
120 #+ 一種需要對複雜和精度都有要求的計算應用的模擬的話.
121 #
122 #  一般至少需要兩個判斷條件.
123 #  1) 作爲一種概念的驗證: 來顯示它可以做到.
124 #  2) 在使用真正的編譯語言來實現一個算法之前,
125 #+    使用腳本來測試和驗證這個算法.
################################End Script#########################################
dc
 dc (桌面計算器desk calculator) 工具是面向棧的並且使用 RPN (逆波蘭表達式
 "Reverse Polish Notation" 又叫"後綴表達式"). 與 bc 命令很相像 , 但是這個工具
 具備好多隻有編程語言才具備的能力.
http://www.818198.com  Page 302
SHELL十三問
 (譯者注: 正常表達式  逆波蘭表達式
    a+b    a,b,+
    a+(b-c)   a,b,c,-,+
    a+(b-c)*d  a,d,b,c,-,*,+
 )
    絕大多數人都避免使用這個工具, 因爲它需要非直覺的 RPN 輸入. 但是, 它卻有特定的
 用途.
Example 12-46 將10進制數字轉換爲16進制數字
################################Start Script#######################################
 1 #!/bin/bash
 2 # hexconvert.sh: 將10進制數字轉換爲16進制數字
 3
 4 E_NOARGS=65 # 缺命令行參數錯誤.
 5 BASE=16     # 16進制.
 6
 7 if [ -z "$1" ]
 8 then
 9   echo "Usage: $0 number"
10   exit $E_NOARGS
11   # 需要一個命令行參數.
12 fi
13 # 練習: 添加命令行參數檢查.
14
15
16 hexcvt ()
17 {
18 if [ -z "$1" ]
19 then
20   echo 0
21   return    # 如果沒有參數傳遞到這個函數中就 "return" 0.
22 fi
23
24 echo ""$1" "$BASE" o p" | dc
25 #                 "o" 設置輸出的基數(數制).
26 #                   "p" 打印棧頂.
27 # 察看 dc 的 man 頁來了解其他的選項.
28 return
29 }
30
31 hexcvt "$1"
32
33 exit 0
################################End Script#########################################
 通過仔細學習 dc 命令的 info 頁, 可以更深入的理解這個複雜的命令. 但是, 有一些
 精通 dc巫術 的小組經常會炫耀他們使用這個強大而又晦澀難懂的工具時的一些技巧,
 並以此爲樂.
  bash$ echo "16i[q]sa[ln0=aln100%Pln100/snlbx]sbA0D68736142snlbxq" | dc"
  Bash
http://www.818198.com  Page 303
SHELL十三問
Example 12-47 因子分解
################################Start Script#######################################
 1 #!/bin/bash
 2 # factr.sh: 分解約數
 3
 4 MIN=2       # 如果比這個數小就不行了.
 5 E_NOARGS=65
 6 E_TOOSMALL=66
 7
 8 if [ -z $1 ]
 9 then
10   echo "Usage: $0 number"
11   exit $E_NOARGS
12 fi
13
14 if [ "$1" -lt "$MIN" ]
15 then
16   echo "Number to factor must be $MIN or greater."
17   exit $E_TOOSMALL
18 fi 
19
20 # 練習: 添加類型檢查 (防止非整型的參數).
21
22 echo "Factors of $1:"
23 # ---------------------------------------------------------------------------------24 echo "$1[p]s2[lip/dli%0=1dvsr]s12sid2%0=13sidvsr[dli%0=1lrli2+dsi!>.]ds.xd1<2" | dc
25 # ---------------------------------------------------------------------------------26 # 上邊這行代碼是 Michel Charpentier 編寫的<[email protected]>.
27 # 在此使用經過授權 (thanks).
28
29  exit 0
################################End Script#########################################
awk
 在腳本中使用浮點運算的另一種方法是使用 awk  內建的數學運算函數, 可以用在shell
 wrapper中.
Example 12-48 計算直角三角形的斜邊
################################Start Script#######################################
 1 #!/bin/bash
 2 # hypotenuse.sh: 返回直角三角形的斜邊.
 3 #               ( 直角邊長的平方和,然後對和取平方根)
 4
 5 ARGS=2                # 需要將2個直角邊作爲參數傳遞進來.
 6 E_BADARGS=65          # 錯誤的參數值.
 7
 8 if [ $# -ne "$ARGS" ] # 測試傳遞到腳本中的參數值.
 9 then
10   echo "Usage: `basename $0` side_1 side_2"
11   exit $E_BADARGS
http://www.818198.com  Page 304
SHELL十三問
12 fi
13
14
15 AWKSCRIPT=' { printf( "%3.7f\n", sqrt($1*$1 + $2*$2) ) } '
16 #              命令 / 傳遞給awk的參數
17
18
19 # 現在, 將參數通過管道傳遞給awk.
20 echo -n "Hypotenuse of $1 and $2 = "
21 echo $1 $2 | awk "$AWKSCRIPT"
22
23 exit 0
################################End Script#########################################
12.9 混雜命令
-------------一些不好歸類的命令
jot, seq
 這些工具通過用戶指定的範圍和增量來產生一系列的整數.
 每個產生出來的整數一般都佔一行, 但是可以使用 -s 選項來改變這種設置.
  bash$ seq 5
  1
  2
  3
  4
  5
 
 
 
  bash$ seq -s : 5
  1:2:3:4:5
 jot 和 seq  命令都經常用在 for 循環中.
Example 12-49 使用 seq 來產生循環參數
################################Start Script#######################################
 1 #!/bin/bash
 2 # 使用 "seq"
 3
 4 echo
 5
 6 for a in `seq 80`  # 或者   for a in $( seq 80 )
 7 # 與  " for a in 1 2 3 4 5 ... 80 "相同  (少敲了好多字!).
 8 # 也可以使用 'jot' (如果系統上有的話).
 9 do
10   echo -n "$a "
11 done      # 1 2 3 4 5 ... 80
12 # 這也是一個通過使用命令的輸出
13 # 來產生 "for"循環中 [list] 列表的例子.
14
15 echo; echo
http://www.818198.com  Page 305
SHELL十三問
16
17
18 COUNT=80  # 當然, 'seq' 也可以使用一個可替換的參數.
19
20 for a in `seq $COUNT`  # 或者   for a in $( seq $COUNT )
21 do
22   echo -n "$a "
23 done      # 1 2 3 4 5 ... 80
24
25 echo; echo
26
27 BEGIN=75
28 END=80
29
30 for a in `seq $BEGIN $END`
31 #  傳給 "seq" 兩個參數, 從第一個參數開始增長,
32 #+ 一直增長到第二個參數爲止.
33 do
34   echo -n "$a "
35 done      # 75 76 77 78 79 80
36
37 echo; echo
38
39 BEGIN=45
40 INTERVAL=5
41 END=80
42
43 for a in `seq $BEGIN $INTERVAL $END`
44 #  傳給 "seq" 三個參數從第一個參數開始增長,
45 #+ 並以第二個參數作爲增量,
46 #+ 一直增長到第三個參數爲止.
47 do
48   echo -n "$a "
49 done      # 45 50 55 60 65 70 75 80
50
51 echo; echo
52
53 exit 0
################################End Script#########################################
 一個簡單些的例子:
  1 #  產生10個連續擴展名的文件,
  2 #+ 名字分別是 file.1, file.2 . . . file.10.
  3 COUNT=10
  4 PREFIX=file
  5
  6 for filename in `seq $COUNT`
  7 do
  8   touch $PREFIX.$filename
http://www.818198.com  Page 306
SHELL十三問
  9   #  或者, 你可以做一些其他的操作,
 10   #+ 比如 rm, grep, 等等.
 11 done
Example 12-50 字母統計
################################Start Script#######################################
 1 #!/bin/bash
 2 # letter-count.sh: 統計一個文本文件中字母出現的次數.
 3 # 由 Stefano Palmeri 編寫.
 4 # 經過授權使用在本書中.
 5 # 本書作者做了少許修改.
 6
 7 MINARGS=2          # 本腳本至少需要2個參數.
 8 E_BADARGS=65
 9 FILE=$1
10
11 let LETTERS=$#-1   # 制定了多少個字母 (作爲命令行參數).
12                    # (從命令行參數的個數中減1.)
13
14
15 show_help(){
16     echo
17            echo Usage: `basename $0` file letters 
18            echo Note: `basename $0` arguments are case sensitive.
19            echo Example: `basename $0` foobar.txt G n U L i N U x.
20     echo
21 }
22
23 # 檢查參數個數.
24 if [ $# -lt $MINARGS ]; then
25    echo
26    echo "Not enough arguments."
27    echo
28    show_help
29    exit $E_BADARGS
30 fi 
31
32
33 # 檢查文件是否存在.
34 if [ ! -f $FILE ]; then
35     echo "File \"$FILE\" does not exist."
36     exit $E_BADARGS
37 fi
38
39
40
41 # 統計字母出現的次數.
42 for n in `seq $LETTERS`; do
43       shift
http://www.818198.com  Page 307
SHELL十三問
44       if [[ `echo -n "$1" | wc -c` -eq 1 ]]; then             #  檢查參數.
45              echo "$1" -\> `cat $FILE | tr -cd  "$1" | wc -c` #  統計.
46       else
47              echo "$1 is not a  single char."
48       fi 
49 done
50
51 exit $?
52
53 #  這個腳本在功能上與 letter-count2.sh 完全相同,
54 #+ 但是運行得更快.
55 #  爲什麼?
################################End Script#########################################
getopt
 getopt 命令將會分析以破折號開頭的命令行選項. 這個外部命令與Bash的內建命令
 getopts 作用相同. 通過使用 -l 標誌, getopt 可以處理長(多字符)選項, 並且也允許參
 數重置.
Example 12-51 使用getopt來分析命令行選項
################################Start Script#######################################
 1 #!/bin/bash
 2 # 使用 getopt.
 3
 4 # 嘗試使用下邊的不同的方法來調用這腳本:
 5 #   sh ex33a.sh -a
 6 #   sh ex33a.sh -abc
 7 #   sh ex33a.sh -a -b -c
 8 #   sh ex33a.sh -d
 9 #   sh ex33a.sh -dXYZ
10 #   sh ex33a.sh -d XYZ
11 #   sh ex33a.sh -abcd
12 #   sh ex33a.sh -abcdZ
13 #   sh ex33a.sh -z
14 #   sh ex33a.sh a
15 # 解釋上面每一次調用的結果.
16
17 E_OPTERR=65
18
19 if [ "$#" -eq 0 ]
20 then   # 腳本需要至少一個命令行參數.
21   echo "Usage $0 -[options a,b,c]"
22   exit $E_OPTERR
23 fi 
24
25 set -- `getopt "abcd:" "$@"`
26 # 爲命令行參數設置位置參數.
27 # 如果使用 "$*" 來代替 "$@" 的話會發生什麼?
28
29 while [ ! -z "$1" ]
http://www.818198.com  Page 308
SHELL十三問
30 do
31   case "$1" in
32     -a) echo "Option \"a\"";;
33     -b) echo "Option \"b\"";;
34     -c) echo "Option \"c\"";;
35     -d) echo "Option \"d\" $2";;
36      *) break;;
37   esac
38
39   shift
40 done
41
42 #  通常來說在腳本中使用內建的 'getopts' 命令,
43 #+ 會比使用 'getopt' 好一些.
44 #  參見 "ex33.sh".
45
46 exit 0
################################End Script#########################################
 參見 Example 9-12 , 這是對 getopt 命令的一個簡單模擬.
run-parts
 run-parts 命令 [1] 將會執行目標目錄中所有的腳本, 這些將本會以 ASCII 的循序進行
 排列. 當然, 這些腳本都需要具有可執行權限.
 cron 幽靈進程 會調用 run-parts 來運行 /etc/cron.* 下的所有腳本.
yes
 yes 命令的默認行爲是向 stdout 中連續不斷的輸出字符 y,每個y佔一行.使用control-c
 來結束運行. 如果想換一個輸出字符的話, 可以使用 yes 其他的字符串, 這樣就會連續
 不同的輸出你指定的字符串. 那麼這樣的命令究竟能做什麼呢? 在命令行或者腳本中,
 yes的輸出可以通過重定向或管道來傳遞給一些需要用戶輸入進行交互的命令. 事實上,
 這個命令可以說是 expect 命令(譯者注: 這個命令本書未介紹, 一個自動實現交互的命
 令)的一個簡化版本.
 yes | fsck /dev/hda1 將會以非交互的形式運行fsck(因爲需要用戶輸入的 y 全由yes
 命令搞定了)(小心使用!).
 yes | rm -r dirname 與 rm -rf dirname 效果相同(小心使用!).
 注意: 當用 yes 的管道形式來使用一些可能具有潛在危險的系統命令的時候一定要深思
  熟慮, 比如 fsck 或 fdisk. 可能會產生一些意外的副作用.
banner
 將會把字符串用一個 ASCII 字符(默認是 '#')來畫出來(就是將多個'#'拼出一副字符的
 圖形).可以作爲硬拷貝重定向到打印機上(譯者注: 可以使用-w 選項設置寬度).
printenv
 對於某個特定的用戶, 顯示出所有的 環境變量.
  bash$ printenv | grep HOME
  HOME=/home/bozo
lp
 lp 和 lpr 命令將會把文件發送到打印隊列中, 並且作爲硬拷貝來打印. [2] 這些命令
 會紀錄它們名字的起始位置並傳遞到行打印機的另一個位置.<rojy bug>
 bash$ lp file1.txt 或者 bash lp <file1.txt
 通常情況下都是將pr的格式化的輸出傳遞到 lp.
 bash$ pr -options file1.txt | lp
http://www.818198.com  Page 309
SHELL十三問
 格式化的包, 比如 groff 和 Ghostscript 就可以將它們的輸出直接發送給 lp.
 bash$ groff -Tascii file.tr | lp
 bash$ gs -options | lp file.ps
 還有一些相關的命令, 比如 lpq, 可以查看打印隊列, lprm, 可以用來從打印隊列中刪
 除作業.
tee
 [UNIX 從管道行業借來的主意.]
 這是一個重定向操作, 但是有些不同. 就像管道中的"三通"一樣, 這個命令可以將命令或
 者管道命令的輸出抽出到一個文件中,而且並不影響結果. 當你想將一個正在運行的進程
 的輸出保存到文件中時, 或者爲了debug而保存輸出記錄的時候, 這個命令就非常有用了.
                              (重定向)
                             |----> to file
                             |
   ==========================|====================
   command ---> command ---> |tee ---> command ---> ---> output of pipe
   ===============================================
   1 cat listfile* | sort | tee check.file | uniq > result.file
 (在對排序的結果進行 uniq (去掉重複行) 之前,文件 check.file 中保存了排過序的
 "listfiles".)
mkfifo
 這個不大引人注意的命令可以創建一個命名管道, 併產生一個臨時的先進先出的buffer
 用來在兩個進程間傳輸數據. [3] 典型的使用是一個進程向FIFO中寫數據, 另一個進程讀
 出來. 參見 Example A-15.
pathchk
 這個命令用來檢查文件名的有效性. 如果文件名超過了最大允許長度(255 個字符), 或者
 它所在的一個或多個路徑搜索不到, 那麼就會產生一個錯誤結果.
 不幸的是,並不能夠返回一個可識別的錯誤碼, 因此它在腳本中幾乎沒有什麼用. 一般都
 使用文件測試操作.
dd
 這也是一個不太出名的工具, 但卻是一個令人恐懼的 "數據複製" 命令. 最開始, 這個命
 令是被用來在UNIX 微機和IBM大型機之間通過磁帶來交換數據, 這個命令現在仍然有它的
 用途. dd 命令只不過是簡單的拷貝一個文件 (或者 stdin/stdout), 但是它會做一些轉
 換. 下邊是一些可能的轉換, 比如 ASCII/EBCDIC, [4]  大寫/小寫, 在輸入和輸出之間
 的字節對的交換, 還有對輸入文件做一些截頭去尾的工作. dd --help  列出了所有轉換,
 還有這個強力工具的一些其他選項.
    1 # 將一個文件轉換爲大寫:
    2
    3 dd if=$filename conv=ucase > $filename.uppercase
    4 #                    lcase   # 轉換爲小寫
Example 12-52 一個拷貝自身的腳本
################################Start Script#######################################
 1 #!/bin/bash
 2 # self-copy.sh
 3
 4 # 這個腳本將會拷貝自身.
 5
 6 file_subscript=copy
 7
http://www.818198.com  Page 310
SHELL十三問
 8 dd if=$0 of=$0.$file_subscript 2>/dev/null
 9 # 阻止dd產生的消息:            ^^^^^^^^^^^
10
11 exit $?
################################End Script#########################################
Example 12-53 練習dd
################################Start Script#######################################
 1 #!/bin/bash
 2 # exercising-dd.sh
 3
 4 # 由Stephane Chazelas編寫.
 5 # 本文作者做了少量修改.
 6
 7 input_file=$0   # 腳本本身.
 8 output_file=log.txt
 9 n=3
10 p=5
11
12 dd if=$input_file of=$output_file bs=1 skip=$((n-1)) count=$((p-n+1)) 2> /dev/null
13 # 從腳本中把位置n到p的字符提取出來.
14
15 # -------------------------------------------------------16
17 echo -n "hello world" | dd cbs=1 conv=unblock 2> /dev/null
18 # 垂直的 echo "hello world" .
19
20 exit 0
################################End Script#########################################
 爲了展示dd的多種用途, 讓我們使用它來記錄按鍵.
Example 12-54 記錄按鍵
################################Start Script#######################################
 1 #!/bin/bash
 2 # dd-keypress.sh: 記錄按鍵, 不需要按回車.
 3
 4
 5 keypresses=4                      # 記錄按鍵的個數.
 6
 7
 8 old_tty_setting=$(stty -g)        # 保存老的終端設置.
 9
10 echo "Press $keypresses keys."
11 stty -icanon -echo                # 禁用標準模式.
12                                   # 禁用本地 echo.
13 keys=$(dd bs=1 count=$keypresses 2> /dev/null)
14 # 如果不指定輸入文件的話, 'dd' 使用標準輸入.
15
16 stty "$old_tty_setting"           # 恢復老的終端設置.
17
http://www.818198.com  Page 311
SHELL十三問
18 echo "You pressed the \"$keys\" keys."
19
20 # 感謝 Stephane Chazelas, 演示了這種方法.
21 exit 0
################################End Script#########################################
 dd 命令可以在數據流上做隨即存取.
    1 echo -n . | dd bs=1 seek=4 of=file conv=notrunc
    2 # "conv=notrunc" 選項意味着輸出文件不能被截短.
    3
    4 # Thanks, S.C.
 dd 命令可以將數據或磁盤鏡像拷貝到設備中, 也可以從設備中拷貝數據或磁盤鏡像, 比
 如說磁盤或磁帶設備都可以 (Example A-5). 通常用來創建啓動盤.
 dd if=kernel-image of=/dev/fd0H1440
 同樣的, dd 可以拷貝軟盤的整個內容(甚至是其他操作系統的磁盤格式) 到硬盤驅動器上
 (以鏡像文件的形式).
 dd if=/dev/fd0 of=/home/bozo/projects/floppy.img
 dd 命令還有一些其他用途, 包括可以初始化臨時交換文件 (Example 28-2) 和 ramdisks
 (內存虛擬硬盤) (Example 28-3). 它甚至可以做一些對整個硬盤分區的底層拷貝, 雖然
 不建議這麼做.
 一些(可能是比較無聊的)人總會想一些關於 dd 命令的有趣的應用.
Example 12-55 安全的刪除一個文件
################################Start Script#######################################
 1 #!/bin/bash
 2 # blot-out.sh: 刪除一個文件所有的記錄.
 3
 4 #  這個腳本會使用隨即字節交替的覆蓋
 5 #+ 目標文件, 並且在最終刪除這個文件之前清零.
 6 #  這麼做之後, 即使你通過傳統手段來檢查磁盤扇區
 7 #+ 也不能把文件原始數據重新恢復.
 8
 9 PASSES=7         #  破壞文件的次數.
10                  #  提高這個數字會減慢腳本運行的速度,
11                  #+ 尤其是對尺寸比較大的目標文件進行操作的時候.
12 BLOCKSIZE=1      #  帶有 /dev/urandom 的 I/O 需要單位塊尺寸,
13                  #+ 否則你可能會獲得奇怪的結果.
14 E_BADARGS=70     #  不同的錯誤退出碼.
15 E_NOT_FOUND=71
16 E_CHANGED_MIND=72
17
18 if [ -z "$1" ]   # 沒指定文件名.
19 then
20   echo "Usage: `basename $0` filename"
21   exit $E_BADARGS
22 fi
23
24 file=$1
25
26 if [ ! -e "$file" ]
http://www.818198.com  Page 312
SHELL十三問
27 then
28   echo "File \"$file\" not found."
29   exit $E_NOT_FOUND
30 fi 
31
32 echo; echo -n "Are you absolutely sure you want to blot out \"$file\" (y/n)? "
33 read answer
34 case "$answer" in
35 [nN]) echo "Changed your mind, huh?"
36       exit $E_CHANGED_MIND
37       ;;
38 *)    echo "Blotting out file \"$file\".";;
39 esac
40
41
42 flength=$(ls -l "$file" | awk '{print $5}')  # 5 是文件長度.
43 pass_count=1
44
45 chmod u+w "$file"   # Allow overwriting/deleting the file.
46
47 echo
48
49 while [ "$pass_count" -le "$PASSES" ]
50 do
51   echo "Pass #$pass_count"
52   sync         # 刷新buffer.
53   dd if=/dev/urandom of=$file bs=$BLOCKSIZE count=$flength
54                # 使用隨機字節進行填充.
55   sync         # 再刷新buffer.
56   dd if=/dev/zero of=$file bs=$BLOCKSIZE count=$flength
57                # 用0填充.
58   sync         # 再刷新buffer.
59   let "pass_count += 1"
60   echo
61 done 
62
63
64 rm -f $file    # 最後, 刪除這個已經被破壞得不成樣子的文件.
65 sync           # 最後一次刷新buffer.
66
67 echo "File \"$file\" blotted out and deleted."; echo
68
69
70 exit 0
71
72 #  這是一種真正安全的刪除文件的辦法,
73 #+ 但是效率比較低, 運行比較慢.
74 #  GNU 的文件工具包中的 "shred" 命令,
http://www.818198.com  Page 313
SHELL十三問
75 #+ 也可以完成相同的工作, 不過更有效率.
76
77 #  使用普通的方法是不可能重新恢復這個文件了.
78 #  然而 . . .
79 #+ 這個簡單的例子是不能夠抵抗
80 #+ 那些經驗豐富並且正規的分析.
81
82 #  這個腳本可能不會很好的運行在日誌文件系統上.(譯者注: JFS)
83 #  練習 (很難): 像它做的那樣修正這個問題.
84
85
86
87 #  Tom Vier的文件刪除包可以更加徹底
88 #+ 的刪除文件, 比這個簡單的例子厲害得多.
89 #     http://www.ibiblio.org/pub/Linux/utils/file/wipe-2.0.0.tar.bz2
90
91 #  如果想對安全刪除文件這一論題進行深度的分析,
92 #+ 可以參見Peter Gutmann的頁面,
93 #+     "Secure Deletion of Data From Magnetic and Solid-State Memory".
94 #       http://www.cs.auckland.ac.nz/~pgut001/pubs/secure_del.html
################################End Script#########################################
od
 od(octal dump)過濾器, 將會把輸入(或文件)轉換爲8進制或者其他進制. 在你需要查看
 或處理一些二進制數據文件或者一個不可讀的系統設備文件的時候, 這個命令非常有用,
 比如/dev/urandom,或者是一個二進制數據過濾器. 參見 Example 9-28 和
 Example 12-13.
hexdump
 對二進制文件進行 16進制, 8進制, 10進制, 或者 ASCII 碼的查閱動作. 這個命令大體
 上與上邊的od命令作用相同, 但是遠不及 od 命令有用.
objdump
 顯示編譯後的2進制文件或2進制可執行文件的信息, 以16進制的形式顯示, 或者顯示反匯
 編列表(使用-d選項).
  bash$ objdump -d /bin/ls
  /bin/ls:     file format elf32-i386
  Disassembly of section .init:
  080490bc <.init>:
   80490bc:       55                      push   %ebp
   80490bd:       89 e5                   mov    %esp,%ebp
   . . .
mcookie
 這個命令會產生一個"magic cookie", 這是一個128-bit (32-字符) 的僞隨機16進制數字,
 這個數字一般都用來作爲X server的鑑權"簽名". 這個命令還可以用來在腳本中作爲一
 種生成隨機數的手段, 當然這是一種"小吃店"(雖然不太正統, 但是很方便)的風格.
    1 random000=$(mcookie)
 當然, 完成同樣的目的還可以使用 md5 命令.
    1 # 產生關於腳本本身的 md5 checksum.
    2 random001=`md5sum $0 | awk '{print $1}'`
    3 # 使用 'awk' 來去掉文件名.
http://www.818198.com  Page 314
SHELL十三問
 mcookie 還給出了產生"唯一"文件名的另一種方法.
Example 12-56 文件名產生器
################################Start Script#######################################
 1 #!/bin/bash
 2 # tempfile-name.sh:  臨時文件名產生器
 3
 4 BASE_STR=`mcookie`   # 32-字符的 magic cookie.
 5 POS=11               # 字符串中隨便的一個位置.
 6 LEN=5                # 取得 $LEN 長度連續的字符串.
 7
 8 prefix=temp          #  最終的一個臨時文件.
 9                      #  如果想讓這個文件更加唯一,
10                      #+ 可以對這個前綴也使用下邊的方法來生成.
11
12 suffix=${BASE_STR:POS:LEN}
13                      # 提取從第11個字符之後的長度爲5的字符串.
14
15 temp_filename=$prefix.$suffix
16                      # 構造文件名.
17
18 echo "Temp filename = "$temp_filename""
19
20 # sh tempfile-name.sh
21 # Temp filename = temp.e19ea
22
23 #  與使用 'date' 命令(參考 ex51.sh)來創建唯一文件名
24 #+ 的方法相比較.
25
26 exit 0
################################End Script#########################################
units
 這個工具用來在不同的計量單位之間互相轉換. 當你在交互模式下正常調用時, 會發現在
 腳本中 units 也很有用.
Example 12-57 將米轉換爲英里
################################Start Script#######################################
 1 #!/bin/bash
 2 # unit-conversion.sh
 3
 4
 5 convert_units ()  # 通過參數取得需要轉換的單位.
 6 {
 7   cf=$(units "$1" "$2" | sed --silent -e '1p' | awk '{print $2}')
 8   # 除了真正需要轉換的部分保留下來外,其他的部分都去掉.
 9   echo "$cf"
10 } 
11
12 Unit1=miles
13 Unit2=meters
http://www.818198.com  Page 315
SHELL十三問
14 cfactor=`convert_units $Unit1 $Unit2`
15 quantity=3.73
16
17 result=$(echo $quantity*$cfactor | bc)
18
19 echo "There are $result $Unit2 in $quantity $Unit1."
20
21 #  如果你傳遞了兩個不匹配的單位會發生什麼?
22 #+ 比如分別傳入英畝和英里?
23
24 exit 0
################################End Script#########################################
m4
 一個隱藏的財寶, m4 是一個強力的宏處理過濾器, [5]  差不多可以說是一種語言了. 雖
 然最開始這個工具是用來作爲 RatFor 的預處理器而編寫的, 但是後來證明 m4  作爲獨
 立的工具也是非常有用的. 事實上, m4 結合了許多工具的功能, 比如 eval, tr, 和 awk,
 除此之外, 它還使得宏擴展變得容易.
 在 2004年4月的 Linux Journal  的問題列表中有一篇關於 m4 命令用法得非常好的文章.
Example 12-58 使用 m4
################################Start Script#######################################
 1 #!/bin/bash
 2 # m4.sh: 使用 m4 宏處理器
 3
 4 # 字符操作
 5 string=abcdA01
 6 echo "len($string)" | m4                           # 7
 7 echo "substr($string,4)" | m4                      # A01
 8 echo "regexp($string,[0-1][0-1],\&Z)" | m4         # 01Z
 9
10 # 算術操作
11 echo "incr(22)" | m4                               # 23
12 echo "eval(99 / 3)" | m4                           # 33
13
14 exit 0
################################End Script#########################################
doexec
 doexec 命令允許將一個隨便的參數列表傳遞到一個二進制可執行文件中. 特別的, 甚至
 可以傳遞 arg[0] (相當於腳本中的 $0 ), 這樣可以使用不同的名字來調用這個可執行
 文件, 並且通過不同的調用的名字, 可以讓這個可執行文件執行不同的動作. 這也可以
 說是一種將參數傳遞到可執行文件中的比較繞圈子的做法.
 比如, /usr/local/bin 目錄可能包含一個 "aaa" 的二進制文件. 使用
 doexec /usr/local/bin/aaa list  可以 列出 當前工作目錄下所有以 "a" 開頭的的文
 件, 而使用 doexec /usr/local/bin/aaa delete  將會刪除這些文件.
 注意: 可執行文件的不同行爲必須定義在可執行文件自身的代碼中, 可以使用如下的
  shell腳本作類比:
    1 case `basename $0` in
    2 "name1" ) do_something;;
    3 "name2" ) do_something_else;;
http://www.818198.com  Page 316
SHELL十三問
    4 "name3" ) do_yet_another_thing;;
    5 *       ) bail_out;;
    6 esac
dialog
 dialog 工具集提供了一種從腳本中調用交互對話框的方法. dialog 的更好的變種版本是
  -- gdialog, Xdialog, 和 kdialog -- 事實上是調用的 X-Windows 的界面工具集. 參
 見 Example 33-19.
sox
 sox 命令, "sound exchange" (聲音轉換)命令, 可以進行聲音文件的轉換. 事實上,可執
 行文件 /usr/bin/play (現在不建議使用) 只不過是 sox 的一個 shell 包裝器而已.
 舉個例子, sox soundfile.wav soundfile.au 將會把一個 WAV 聲音文件轉換成一個
 (Sun 音頻格式) AU 聲音文件.
 Shell 腳本非常適合於使用 sox 的聲音操作來批處理聲音文件. 比如, 參見
 Linux Radio Timeshift HOWTO 和 MP3do Project. 注意事項:
[1]  這個工具事實上是從 Debian Linux 發行版中的一個腳本借鑑過來的.
[2]  打印隊列 就是"在線等待"打印的作業組.
[3]  對於本話題的一個完美的介紹, 請參見 Andy Vaught 的文章, 命名管道的介紹,
  (http://www2.linuxjournal.com/lj-issues/issue41/2156.html), 這是
  Linux Journal (http://www.linuxjournal.com/)1997年9月的一個問題.
[4]  EBCDIC (發音是 "ebb-sid-ick") 是單詞 (Extended Binary Coded Decimal
  Interchange Code) 的首字母縮寫. 這是 IBM 的數據格式, 現在已經不常見了.
  dd 命令的 conv=ebcdic 選項的一個比較奇異的使用方法是對一個文件進行快速而
  且容易但不太安全的編碼.
     1 cat $file | dd conv=swab,ebcdic > $file_encrypted
     2 # 編碼 (看起來好像沒什麼用).     
     3 # 應該交換字節(swab), 有點晦澀.
     4
     5 cat $file_encrypted | dd conv=swab,ascii > $file_plaintext
     6 # 解碼.
[5]  宏 是一個符號常量, 將會被擴展成一個命令字符串或者一系列的參數操作. 
[]    
   
高級Bash腳本編程指南(四)
文章整理: 文章來源: 網絡
高級Bash腳本編程指南(四)
 
 
第13章 系統與管理命令
======================
在/etc/rc.d目錄中的啓動和關機腳本中包含了好多有用的(和沒用的)這些系統管理命令. 這些
命令通常總是被root用戶使用, 用與系統維護或者是緊急文件系統修復.一定要小心使用這些工
具, 因爲如果濫用的話, 它們會損壞你的系統.
Users 和 Groups 類命令
users
 顯示所有的登錄的用戶. 這個命令與 who -q 基本一致.
groups
 列出當前用戶和他所屬於的組. 這相當於 $GROUPS 內部變量, 但是這個命令將會給出組名
 字, 而不是數字.
http://www.818198.com  Page 317
SHELL十三問
  bash$ groups
  bozita cdrom cdwriter audio xgrp
 
  bash$ echo $GROUPS
  501
chown, chgrp
 chown 命令將會修改一個或多個文件的所有權. 對於root來說這是一種非常好的將文件的
 所有權從一個用戶換到另一個用戶的方法. 一個普通用戶不能修改文件的所有權, 即使他
 是文件的宿主也不行. [1]
  root# chown bozo *.txt
 chgrp 將會修改一個或個文件黨組所有權. 你必須是這些文件的宿主, 並且是目的組的成
 員(或者root), 這樣才能使用這個操作.
    1 chgrp --recursive dunderheads *.data
    2 #  "dunderheads"(譯者: 暈,蠢才...) 組現在擁有了所有的"*.data"文件.
    3 #+ 包括所有$PWD目錄下的子目錄中的文件(--recursive的作用就是包含子目錄).
useradd, userdel
 useradd 管理命令將會在系統上添加一個用戶帳號, 並且如果指定的話, 還會爲特定的用
 戶創建home目錄. 相應的userdel 命令將會從系統上刪除一個用戶帳號, [2] 並且刪除相
 應的文件.
 注意: adduser命令與useradd是相同的, adduser通常都是一個符號鏈接.
usermod
 修改用戶帳號. 可以修改密碼, 組身份, 截止日期, 或者給定用戶帳號的其他的屬性. 使
 用這個命令, 用戶的密碼可能會被鎖定, 因爲密碼會影響到帳號的有效性.
groupmod
 修改指定組. 組名字或者ID號都可以使用這個命令來修改.
id
 id 將會列出當前進程的真實和有效用戶ID, 還有用戶的組ID. 這與Bash的內部變量
 $UID, $EUID, 和 $GROUPS 很相像.
  bash$ id
  uid=501(bozo) gid=501(bozo) groups=501(bozo),22(cdrom),80(cdwriter),81(audio)
 
  bash$ echo $UID
  501
 注意: id 命令只有在有效ID與真實ID不符時纔會顯示有效id.
 參見 Example 9-5.
who
 顯示系統上所有已經登錄的用戶.
  bash$ who
  bozo  tty1     Apr 27 17:45
  bozo  pts/0    Apr 27 17:46
  bozo  pts/1    Apr 27 17:47
  bozo  pts/2    Apr 27 17:49
 -m 選項將會只給出當前用戶的詳細信息. 將任意兩個參數傳遞到who中 都等價於who -m,
 就像 who am i 或者 who The Man.
  bash$ who -m
  localhost.localdomain!bozo  pts/2    Apr 27 17:49
 whoami 與who -m 很相似, 但是隻列出用戶名.
  bash$ whoami
http://www.818198.com  Page 318
SHELL十三問
  bozo
w
 顯示所有的登錄的用戶和屬於它們的進程. 這是一個who的擴展版本. w的輸出可以通過管
 道傳遞到grep中, 這樣就可以查找指定的用戶或進程.
  bash$ w | grep startx
  bozo  tty1     -                 4:22pm  6:41   4.47s  0.45s  startx
logname
 顯示當前用戶的登錄名(可以在/var/run/utmp中找到). 這與上邊的whoami很相近.
  bash$ logname
  bozo
 
  bash$ whoami
  bozo
 然而...
  bash$ su
  Password: ......
 
  bash# whoami
  root
  bash# logname
  bozo
 注意: logname只會打印出登錄的用戶名, 而whoami 將會給出附着到當前進程的用戶名.
  就像我們上邊看到的那樣, 這兩個名字有時會不同.
su
 使用一個代替的用戶來運行一個程序或腳本. su rjones 將會以 rjones 來啓動一個
 shell. 一個不加參數的su默認就是root. 參見 Example A-15.
sudo
 以root(或其他用戶)的身份來運行一個命令. 這個命令可以運行在腳本中, 這樣就允許以
 正規的用戶身份來運行腳本.
    1 #!/bin/bash
    2
    3 # 一些命令.
    4 sudo cp /root/secretfile /home/bozo/secret
    5 # 一些命令.
 文件 /etc/sudoers 持有允許調用sudo的用戶名.
passwd
 設置, 修改, 或者管理用戶的密碼.
 passwd 命令可以用在腳本中, 但可能你不想這麼用.
Example 13-1 設置一個新密碼
################################Start Script#######################################
 1 #!/bin/bash
 2 #  setnew-password.sh: 只用於說明目的.
 3 #                      如果真正運行這個腳本並不是一個好主意.
 4 #  這個腳本必須以root身份運行.
 5
 6 ROOT_UID=0         # Root 的 $UID 0.
 7 E_WRONG_USER=65    # 不是 root?
 8
http://www.818198.com  Page 319
SHELL十三問
 9 E_NOSUCHUSER=70
10 SUCCESS=0
11
12
13 if [ "$UID" -ne "$ROOT_UID" ]
14 then
15   echo; echo "Only root can run this script."; echo
16   exit $E_WRONG_USER
17 else
18   echo
19   echo "You should know better than to run this script, root."
20   echo "Even root users get the blues... "
21   echo
22 fi 
23
24
25 username=bozo
26 NEWPASSWORD=security_violation
27
28 # 檢查bozo是否在這裏.
29 grep -q "$username" /etc/passwd
30 if [ $? -ne $SUCCESS ]
31 then
32   echo "User $username does not exist."
33   echo "No password changed."
34   exit $E_NOSUCHUSER
35 fi 
36
37 echo "$NEWPASSWORD" | passwd --stdin "$username"
38 #  'passwd'命令 '--stdin' 選項允許
39 #+ 從stdin(或者管道)中獲得一個新的密碼.
40
41 echo; echo "User $username's password changed!"
42
43 # 在腳本中使用'passwd'命令是很危險的.
44
45 exit 0
################################End Script#########################################
 passwd 命令的 -l, -u, 和 -d 選項允許鎖定, 解鎖,和刪除一個用戶的密碼. 只有root
 用戶可以使用這些選項.
ac
 顯示用戶登錄的連接時間, 就像從 /var/log/wtmp 中讀取一樣. 這是GNU的一個統計工具.
  bash$ ac
    total       68.08
last
 用戶最後登錄的信息, 就像從/var/log/wtmp中讀出來一樣. 這個命令也可以用來顯示遠
 端登錄.
 比如, 顯示最後幾次系統的重啓信息:
http://www.818198.com  Page 320
SHELL十三問
  bash$ last reboot
  reboot   system boot  2.6.9-1.667      Fri Feb  4 18:18          (00:02)   
  reboot   system boot  2.6.9-1.667      Fri Feb  4 15:20          (01:27)   
  reboot   system boot  2.6.9-1.667      Fri Feb  4 12:56          (00:49)   
  reboot   system boot  2.6.9-1.667      Thu Feb  3 21:08          (02:17)   
  . . .
  wtmp begins Tue Feb  1 12:50:09 2005
newgrp
 不用登出就可以修改用戶的組ID. 並且允許存取新組的文件. 因爲用戶可能同時屬於多個
 組, 這個命令很少被使用.
終端類命令
tty
 顯示當前用戶終端的名字. 注意每一個單獨的xterm窗口都被算作一個不同的終端.
  bash$ tty
  /dev/pts/1
stty
 顯示並(或)修改終端設置. 這個複雜命令可以用在腳本中, 並可以用來控制終端的行爲和
 其顯示輸出的方法. 參見這個命令的info頁, 並仔細學習它.
Example 13-2 設置一個擦除字符
################################Start Script#######################################
 1 #!/bin/bash
 2 # erase.sh: 在讀取輸入時使用"stty"來設置一個擦除字符.
 3
 4 echo -n "What is your name? "
 5 read name                      #  試試退格鍵
 6                                #+ 來刪除輸入的字符.
 7                                #  有什麼問題?
 8 echo "Your name is $name."
 9
10 stty erase '#'                 #  將 "hashmark" (#) 設置爲退格字符.
11 echo -n "What is your name? "
12 read name                      #  使用#來刪除最後鍵入的字符.
13 echo "Your name is $name."
14
15 # 警告: 即使在腳本退出後, 新的鍵值還是保持設置.(譯者: 使用stty erase '^?' 恢復)
16
17 exit 0
################################End Script#########################################
Example 13-3 關掉終端對於密碼的echo
################################Start Script#######################################
 1 #!/bin/bash
 2 # secret-pw.sh: 保護密碼不被顯示
 3
 4 echo
 5 echo -n "Enter password "
 6 read passwd
 7 echo "password is $passwd"
 8 echo -n "If someone had been looking over your shoulder, "
http://www.818198.com  Page 321
SHELL十三問
 9 echo "your password would have been compromised."
10
11 echo && echo  # 在一個"與列表"中產生2個換行.
12
13
14 stty -echo    # 關閉屏幕的echo.
15
16 echo -n "Enter password again "
17 read passwd
18 echo
19 echo "password is $passwd"
20 echo
21
22 stty echo     # 恢復屏幕的echo.
23
24 exit 0
25
26 # 詳細的閱讀stty命令的info頁, 以便於更好的掌握這個有用並且狡猾的工具.
################################End Script#########################################
 一個具有創造性的stty命令的用法, 檢測用戶所按的鍵(不用敲回車).
Example 13-4 按鍵檢測
################################Start Script#######################################
 1 #!/bin/bash
 2 # keypress.sh: 檢測用戶按鍵 ("hot keys").
 3
 4 echo
 5
 6 old_tty_settings=$(stty -g)   # 保存老的設置(爲什麼?).
 7 stty -icanon
 8 Keypress=$(head -c1)          # 或者 $(dd bs=1 count=1 2> /dev/null)
 9                               # 在非GNU的系統上
10
11 echo
12 echo "Key pressed was \""$Keypress"\"."
13 echo
14
15 stty "$old_tty_settings"      # 恢復老的設置.
16
17 # 感謝, Stephane Chazelas.
18
19 exit 0
################################End Script#########################################
 參見 Example 9-3.
 注意: 終端與模式terminals and modes
  一般情況下, 一個終端都是工作在canonical(標準)模式下. 當用戶按鍵後, 事實上所
  產生的字符並沒有馬上傳遞到運行在當前終端上的程序. 終端上的一個本地的緩存保
  存了這些按鍵. 當用按下ENTER鍵的時候, 纔會將所有保存的按鍵信息傳遞到運行的程
  序中. 這就意味着在終端內部存在一個基本的行編輯器. 
http://www.818198.com  Page 322
SHELL十三問
   bash$ stty -a
   speed 9600 baud; rows 36; columns 96; line = 0;
   intr = ^C; quit = ^\; erase = ^H; kill = ^U; eof = ^D; eol = <undef>; eol2 = <undef>;
   start = ^Q; stop = ^S; susp = ^Z; rprnt = ^R; werase = ^W; lnext = ^V; flush = ^O;
   ...
   isig icanon iexten echo echoe echok -echonl -noflsh -xcase -tostop -echoprt
  在使用canonical模式的時候, 可以對本地終端行編輯器所定義的特殊按鍵進行重新定
  義.
   bash$ cat > filexxx
   wha<ctl-W>I<ctl-H>foo bar<ctl-U>hello world<ENTER>
   <ctl-D>
   bash$ cat filexxx
   hello world 
   bash$ wc -c < filexxx
   12  
  控制終端的進程只保存了12個字符(11個字母加上一個換行), 雖然用戶敲了26個按鍵.
  在 non-canonical ("raw") 模式, 每次按鍵(包括特殊定義的按鍵, 比如 ctl-H)將會
  立即發送一個字符到控制進程.
  Bash提示符禁用了icanon和echo, 因爲它用自己的更好的行編輯器代替了終端的基本
  行編輯器. 比如, 當你在Bash提示符下敲ctl-A的時候, 終端將不會顯示 ^A, 但是
  Bash將會獲得\1字符, 然後解釋這個字符, 這樣光標就移動到行首了.
  Stéphane Chazelas
setterm
 設置特定的終端屬性. 這個命令將向它的終端的stdout寫一個字符串, 這個字符串將修改
 終端的行爲.
  bash$ setterm -cursor off
  bash$
 setterm 命令可以被用在腳本中來修改寫到stdout的文本的外觀, 雖然如果你僅僅只想完
 成這個目的, 還有特定的更好的工具可以用.
    1 setterm -bold on
    2 echo bold hello
    3
    4 setterm -bold off
    5 echo normal hello
tset
 顯示或初始化終端設置. 可以說這是stty的功能比較弱的版本.
 
  bash$ tset -r
  Terminal type is xterm-xfree86.
  Kill is control-U (^U).
  Interrupt is control-C (^C).
 
setserial
 設置或者顯示串口參數. 這個腳本只能被root用戶來運行, 並且通常都在系統安裝腳本
 中使用.
    1 # 來自於 /etc/pcmcia/serial 腳本:
    2
    3 IRQ=`setserial /dev/$DEVICE | sed -e 's/.*IRQ: //'`
http://www.818198.com  Page 323
SHELL十三問
    4 setserial /dev/$DEVICE irq 0 ; setserial /dev/$DEVICE irq $IRQ
getty, agetty
 一個終端的初始化過程通常都是使用getty或agetty來建立, 這樣才能讓用戶登錄. 這些
 命令並不用在用戶的shell腳本中. 它們的行爲與stty很相似.
mesg
 使能或禁用當前用戶終端的存取權限. 禁用存取權限將會阻止網絡上的另一用戶向這個終
 端寫消息.
 注意: 當你正在編寫文本文件的時候, 在文本中間突然來了一個莫名其妙的消息, 這對你
  來說是非常煩人的. 在多用戶的網絡環境下, 當你不想被打斷的時候, 你可能因此希
  望禁用對你終端的寫權限.
wall
 這是一個縮寫單詞 "write all", 也就是, 向登錄到網絡上的任何終端的所有用戶都發送
 一個消息. 最早這是一個管理員的工具, 很有用, 比如, 當系統有問題的時候, 管理可以
 警告系統上的所有人暫時離開 (參見 Example 17-1).
  bash$ wall System going down for maintenance in 5 minutes!
  Broadcast message from bozo (pts/1) Sun Jul  8 13:53:27 2001...
  System going down for maintenance in 5 minutes!
 注意: 如果某個特定終端使用mesg來禁止了寫權限, 那麼wall將不會給它發消息.
信息與統計類
uname
 輸出系統的說明(OS, 內核版本, 等等.)到stdout. 使用 -a 選項, 將會給出詳細的信息
 (參見 Example 12-5). 使用-s選項只會輸出OS類型.
  bash$ uname -a
  Linux localhost.localdomain 2.2.15-2.5.0 #1 Sat Feb 5 00:13:43 EST 2000 i686 unknown
 
  bash$ uname -s
  Linux
arch
 顯示系統的硬件體系結構. 等價於 uname -m. 參見 Example 10-26.
  bash$ arch
  i686
 
  bash$ uname -m
  i686
lastcomm
 給出前一個命令的信息, 存儲在/var/account/pacct文件中. 命令名字與用戶名字都可以
 使用選項來指定. 這是GNU的一個統計工具.
lastlog
 列出系統上所有用戶最後登錄的時間. 存在/var/log/lastlog文件中.
  bash$ lastlog
  root          tty1                      Fri Dec  7 18:43:21 -0700 2001
  bin                                     **Never logged in**
  daemon                                  **Never logged in**
  ...
  bozo          tty1                      Sat Dec  8 21:14:29 -0700 2001
 
 
 
http://www.818198.com  Page 324
SHELL十三問
  bash$ lastlog | grep root
  root          tty1                      Fri Dec  7 18:43:21 -0700 2001
 注意: 如果用戶對於/var/log/lastlog文件沒有讀權限的話, 那麼調用這個命令就會失敗.
lsof
 列出打開的文件. 這個命令將會把所有當前打開的文件列出一份詳細的表格, 包括文件的
 所有者信息, 尺寸, 與它們相關的信息等等. 當然, lsof也可以管道輸出到 grep 和(或)
 awk來分析它的結果.
  bash$ lsof
  COMMAND    PID    USER   FD   TYPE     DEVICE    SIZE     NODE NAME
  init         1    root  mem    REG        3,5   30748    30303 /sbin/init
  init         1    root  mem    REG        3,5   73120     8069 /lib/ld-2.1.3.so
  init         1    root  mem    REG        3,5  931668     8075 /lib/libc-2.1.3.so
  cardmgr    213    root  mem    REG        3,5   36956    30357 /sbin/cardmgr
  ...
strace
 爲了跟蹤系統和信號的診斷和調試工具. 調用它最簡單的方法就是strace COMMAND.
  bash$ strace df
  execve("/bin/df", ["df"], [/* 45 vars */]) = 0
  uname({sys="Linux", node="bozo.localdomain", ...}) = 0
  brk(0)                                  = 0x804f5e4
  ...
 這是 Solaris truss命令的Linux的等價工具.
nmap
 網絡端口掃描器. 這個命令將會掃描一個服務器來定位打開的端口, 並且定位這些端口相
 關的服務. 這是一個防止網絡被黑客入侵的一個重要的安全工具.
    1 #!/bin/bash
    2
    3 SERVER=$HOST                           # localhost.localdomain (127.0.0.1).
    4 PORT_NUMBER=25                         # SMTP 端口.
    5
    6 nmap $SERVER | grep -w "$PORT_NUMBER"  # 這個指定端口打開了麼?
    7 #              grep -w 匹配整個單詞,
    8 #+             這樣就不會匹配類似於1025這種含有25的端口了.
    9
   10 exit 0
   11
   12 # 25/tcp     open        smtp
nc
 nc(netcat)工具是一個完整的工具包, 可以使用它來連接和監聽TCP和UDP端口. 它可以用
 來作爲診斷和測試工具, 也可以用來作爲基於腳本的HTTP客戶端和服務器的組件.
  bash$ nc localhost.localdomain 25
  220 localhost.localdomain ESMTP Sendmail 8.13.1/8.13.1; Thu, 31 Mar 2005 15:41:35 -0700
Example 13-5 Checking a remote server for identd<rojy bug>
################################Start Script#######################################
 1 #! /bin/sh
 2 ## Duplicate DaveG's ident-scan thingie using netcat. Oooh, he'll be p*ssed.
 3 ## Args: target port [port port port ...]
 4 ## Hose stdout _and_ stderr together.
http://www.818198.com  Page 325
SHELL十三問
 5 ##
 6 ##  優點: runs slower than ident-scan, giving remote inetd less cause
 7 ##+ for alarm, and only hits the few known daemon ports you specify.
 8 ##  缺點: requires numeric-only port args, the output sleazitude,
 9 ##+ and won't work for r-services when coming from high source ports.
10 # 腳本作者: Hobbit <[email protected]>
11 # 授權使用在本書中.
12
13 # ---------------------------------------------------14 E_BADARGS=65       # 至少需要兩個參數.
15 TWO_WINKS=2        # 需要睡多長時間.
16 THREE_WINKS=3
17 IDPORT=113         # Authentication "tap ident" port.
18 RAND1=999
19 RAND2=31337
20 TIMEOUT0=9
21 TIMEOUT1=8
22 TIMEOUT2=4
23 # ---------------------------------------------------24
25 case "${2}" in
26   "" ) echo "Need HOST and at least one PORT." ; exit $E_BADARGS ;;
27 esac
28
29 # Ping 'em once and see if they *are* running identd.
30 nc -z -w $TIMEOUT0 "$1" $IDPORT || { echo "Oops, $1 isn't running identd." ; exit 0 ; }
31 #  -z scans for listening daemons.
32 #     -w $TIMEOUT = How long to try to connect.
33
34 # Generate a randomish base port.
35 RP=`expr $$ % $RAND1 + $RAND2`
36
37 TRG="$1"
38 shift
39
40 while test "$1" ; do
41   nc -v -w $TIMEOUT1 -p ${RP} "$TRG" ${1} < /dev/null > /dev/null &
42   PROC=$!
43   sleep $THREE_WINKS
44   echo "${1},${RP}" | nc -w $TIMEOUT2 -r "$TRG" $IDPORT 2>&1
45   sleep $TWO_WINKS
46
47 # 這個腳本看起來是不是一個瘸腿腳本, 或者其它更差的什麼東西?
48 # ABS Guide 作者註釋: "並不是真的那麼差,
49 #+                            事實上相當清楚."
50
51   kill -HUP $PROC
52   RP=`expr ${RP} + 1`
http://www.818198.com  Page 326
SHELL十三問
53   shift
54 done
55
56 exit $?
57
58 #  注意事項:
59 #  ---------60
61 #  嘗試註釋一下第30行的程序, 並且使用"localhost.localdomain 25"
62 #+ 作爲參數來運行這個腳本.
63
64 #  For more of Hobbit's 'nc' example scripts,
65 #+ look in the documentation:
66 #+ the /usr/share/doc/nc-X.XX/scripts directory.
################################End Script#########################################
 並且, 當然, 這裏還有Dr. Andrew Tridgell在BistKeeper事件中臭名卓著的一行腳本:
    1 echo clone | nc thunk.org 5000 > e2fsprogs.dat
free
 使用表格形式來顯示內存和緩存的使用情況. 這個命令的輸出非常適合於使用 grep, awk
 或者Perl來分析. procinfo命令將會顯示free命令所能顯示的所有信息, 而且更多.
  bash$ free
      total       used       free     shared    buffers     cached
    Mem:         30504      28624       1880      15820       1608       16376
    -/+ buffers/cache:      10640      19864
    Swap:        68540       3128      65412
 顯示未使用的RAM內存:
  bash$ free | grep Mem | awk '{ print $4 }'
  1880
procinfo
 從/proc pseudo-filesystem中提取和顯示所有信息和統計資料. 這個命令將給出更詳細
 的信息.
  bash$ procinfo | grep Bootup
  Bootup: Wed Mar 21 15:15:50 2001    Load average: 0.04 0.21 0.34 3/47 6829
lsdev
 顯示設備, 也就是顯示安裝的硬件.
  bash$ lsdev
  Device            DMA   IRQ  I/O Ports
  ------------------------------------------------  cascade             4     2
  dma                          0080-008f
  dma1                         0000-001f
  dma2                         00c0-00df
  fpu                          00f0-00ff
  ide0                     14  01f0-01f7 03f6-03f6
  ...
du
 遞歸的顯示(磁盤)文件的使用狀況. 除非指定, 默認是當前工作目錄.
  bash$ du -ach
http://www.818198.com  Page 327
SHELL十三問
  1.0k    ./wi.sh
  1.0k    ./tst.sh
  1.0k    ./random.file
  6.0k    .
  6.0k    total
df
 使用列表的形式顯示文件系統的使用狀況.
  bash$ df
  Filesystem           1k-blocks      Used Available Use% Mounted on
  /dev/hda5               273262     92607    166547  36% /
  /dev/hda8               222525    123951     87085  59% /home
  /dev/hda7              1408796   1075744    261488  80% /usr
dmesg
 將所有的系統啓動消息輸出到stdout上. 方便出錯,並且可以查出安裝了哪些設備驅動和
 察看使用了哪些系統中斷. dmesg命令的輸出當然也可以在腳本中使用 grep, sed, 或
 awk 來進行分析.
  bash$ dmesg | grep hda
  Kernel command line: ro root=/dev/hda2
  hda: IBM-DLGA-23080, ATA DISK drive
  hda: 6015744 sectors (3080 MB) w/96KiB Cache, CHS=746/128/63
  hda: hda1 hda2 hda3 < hda5 hda6 hda7 > hda4
stat
 顯示一個或多個給定文件(也可以是目錄文件或設備文件)的詳細的統計信息.
  bash$ stat test.cru
    File: "test.cru"
    Size: 49970        Allocated Blocks: 100          Filetype: Regular File
    Mode: (0664/-rw-rw-r--)         Uid: (  501/ bozo)  Gid: (  501/ bozo)
  Device:  3,8   Inode: 18185     Links: 1   
  Access: Sat Jun  2 16:40:24 2001
  Modify: Sat Jun  2 16:40:24 2001
  Change: Sat Jun  2 16:40:24 2001
 如果目標文件不存在, stat 將會返回一個錯誤信息.
  bash$ stat nonexistent-file
  nonexistent-file: No such file or directory
vmstat
 顯示虛擬內存的統計信息.
  bash$ vmstat
  procs                      memory    swap          io system         cpu
  r  b  w   swpd   free   buff  cache  si  so    bi    bo   in    cs  us  sy id
  0  0  0      0  11040   2636  38952   0   0    33     7  271    88   8   3 89
netstat
 顯示當前網絡的統計和信息, 比如路由表和激活的連接. 這個工具存取/proc/net(第27章)
 中的信息. 參見 Example 27-3.
 netstat -r 等價於 route 命令.
  bash$ netstat
  Active Internet connections (w/o servers)
  Proto Recv-Q Send-Q Local Address           Foreign Address         State     
  Active UNIX domain sockets (w/o servers)
http://www.818198.com  Page 328
SHELL十三問
  Proto RefCnt Flags       Type       State         I-Node Path
  unix  11     [ ]         DGRAM                    906    /dev/log
  unix  3      [ ]         STREAM     CONNECTED     4514   /tmp/.X11-unix/X0
  unix  3      [ ]         STREAM     CONNECTED     4513
  . . .
uptime
 顯示系統運行的時間, 還有其他一些統計信息.
  bash$ uptime
  10:28pm  up  1:57,  3 users,  load average: 0.17, 0.34, 0.27
 注意: load average 如果小於或等於1, 那麼就意味着系統會馬上處理. 如果
  load average大於1, 那麼就意味着進程需要排隊. 如果load average大於3,
  那麼就意味着, 系統性能已經顯著下降了.
hostname
 顯示系統的主機名字. 這個命令在 /etc/rc.d 安裝腳本(/etc/rc.d/rc.sysinit
 或類似的)中設置主機名. 等價於uname -n, 並且與$HOSTNAME內部變量很相像.
  bash$ hostname
  localhost.localdomain
 
  bash$ echo $HOSTNAME
  localhost.localdomain
 與 hostname 命令很相像的命令還有 domainname, dnsdomainname, nisdomainname, 和
 ypdomainname 命令. 使用這些來顯示或設置系統DNS 或者 NIS/YP 域名. 對於hostname
 命令來說使用不同的選項一樣可以達到上邊這些命令的目的.
hostid
 顯示主機的32位的16進制ID.
  bash$ hostid
  7f0100
 注意: 這個命令據說對於特定系統可以獲得一個"唯一"的序號. 某些產品的註冊過程可能
  會需要這個序號來作爲用戶的許可證. 不幸的是, hostid 只會使用字節轉換的方法
  來用16進制顯示機器的網絡地址.
  一個沒有網絡的Linux機器的典型的網絡地址設置在/ect/hosts中.
   bash$ cat /etc/hosts
   127.0.0.1               localhost.localdomain localhost
  碰巧, 通過對127.0.0.1進行字節轉換, 我們獲得了 0.127.1.0, 用16進製表示就是
  007f0100, 這就是上邊hostid返回的結果. 這樣幾乎所有的無網絡的Linux機器都會
  得到這個hostid.
sar
 sar (System Activity Reporter系統活動報告) 命令將會給出系統統計的一個非常詳細的
 概要. Santa Cruz Operation("老" SCO)公司在1999年4月份以開源軟件的形式發佈了sar.
 這個命令並不是基本Linux發行版的一部分, 但是你可以從Sebastien Godard 寫的
 sysstat utilities 包中獲得這個工具.
  bash$ sar
  Linux 2.4.9 (brooks.seringas.fr)  09/26/03
 10:30:00          CPU     %user     %nice   %system   %iowait     %idle
 10:40:00          all      2.21     10.90     65.48      0.00     21.41
 10:50:00          all      3.36      0.00     72.36      0.00     24.28
 11:00:00          all      1.12      0.00     80.77      0.00     18.11
 Average:          all      2.23      3.63     72.87      0.00     21.27
http://www.818198.com  Page 329
SHELL十三問
 14:32:30          LINUX RESTART
 15:00:00          CPU     %user     %nice   %system   %iowait     %idle
 15:10:00          all      8.59      2.40     17.47      0.00     71.54
 15:20:00          all      4.07      1.00     11.95      0.00     82.98
 15:30:00          all      0.79      2.94      7.56      0.00     88.71
 Average:          all      6.33      1.70     14.71      0.00     77.26
readelf
 顯示指定的 elf 格式的2進制文件的統計信息. 這個工具是binutils工具包的一部分.
  bash$ readelf -h /bin/bash
  ELF Header:
    Magic:   7f 45 4c 46 01 01 01 00 00 00 00 00 00 00 00 00
    Class:                             ELF32
    Data:                              2's complement, little endian
    Version:                           1 (current)
    OS/ABI:                            UNIX - System V
    ABI Version:                       0
    Type:                              EXEC (Executable file)
    . . .
size
 size [/path/to/binary] 命令可以顯示2進制可執行文件或歸檔文件每部分的尺寸. 這個
 工具主要是程序員使用.
  bash$ size /bin/bash
  text    data     bss     dec     hex filename
   495971   22496   17392  535859   82d33 /bin/bash
系統日誌類
logger
 附加一個用戶產生的消息到系統日之中 (/var/log/messages). 不是root用戶也可以調用
 logger.
    1 logger Experiencing instability in network connection at 23:10, 05/21.
    2 # 現在, 運行 'tail /var/log/messages'.
 通過在腳本中調用一個logger命令, 就可以將調試信息寫到/var/log/messages中.
    1 logger -t $0 -i Logging at line "$LINENO".
    2 # "-t" 選項可以爲長的入口指定標籤.
    3 # "-i" 選項記錄進程ID.
    4
    5 # tail /var/log/message
    6 # ...
    7 # Jul  7 20:48:58 localhost ./test.sh[1712]: Logging at line 3.
logrotate
 這個工具用來管理系統的log文件, 可以在合適的時候輪換, 壓縮, 刪除, 和(或)e-mail
 它們. 這個工具將從老的log文件中取得一些雜亂的記錄保存在/var/log中. 通常使用
 cron 來每天運行logrotate.
 在/etc/logrotate.conf中添加合適的入口就可以管理自己的log文件了, 就像管理系統
 log文件一樣.
 注意: Stefano Falsetto 創造了rottlog, 他認爲這是logrotate的改進版本.
作業控制
ps
 進程統計: 通過進程所有者和PID(進程ID)來列出當前執行的進程. 通常都是使用ax選項
http://www.818198.com  Page 330
SHELL十三問
 來調用這個命令, 並且結果可以通過管道傳遞到 grep 或 sed 中來搜索特定的進程
 (參見 Example 11-12 和 Example 27-2).
  bash$  ps ax | grep sendmail
  295 ?    S   0:00 sendmail: accepting connections on port 25
 如果想使用"樹"的形式來顯示系統進程: ps afjx 或者 ps ax --forest.
pgrep, pkill
 ps 命令與grep或kill結合使用.
  bash$ ps a | grep mingetty
  2212 tty2     Ss+    0:00 /sbin/mingetty tty2
  2213 tty3     Ss+    0:00 /sbin/mingetty tty3
  2214 tty4     Ss+    0:00 /sbin/mingetty tty4
  2215 tty5     Ss+    0:00 /sbin/mingetty tty5
  2216 tty6     Ss+    0:00 /sbin/mingetty tty6
  4849 pts/2    S+     0:00 grep mingetty
  bash$ pgrep mingetty
  2212 mingetty
  2213 mingetty
  2214 mingetty
  2215 mingetty
  2216 mingetty
pstree
 使用"樹"形式列出當前執行的進程. -p選項顯示PID,和進程名字.
top
 連續不斷的顯示cpu使用率最高的進程. -b 選項將會以文本方式顯示, 以便於可以在腳本
 中分析或存取.
  bash$ top -b
    8:30pm  up 3 min,  3 users,  load average: 0.49, 0.32, 0.13
  45 processes: 44 sleeping, 1 running, 0 zombie, 0 stopped
  CPU states: 13.6% user,  7.3% system,  0.0% nice, 78.9% idle
  Mem:    78396K av,   65468K used,   12928K free,       0K shrd,    2352K buff
  Swap:  157208K av,       0K used,  157208K free                   37244K cached
    PID USER     PRI  NI  SIZE  RSS SHARE STAT %CPU %MEM   TIME COMMAND
    848 bozo      17   0   996  996   800 R     5.6  1.2   0:00 top
   1 root       8   0   512  512   444 S     0.0  0.6   0:04 init
   2 root       9   0     0    0     0 SW    0.0  0.0   0:00 keventd
    ... 
nice
 使用修改後的優先級來運行一個後臺作業. 優先級從19(最低)到-20(最高). 只有root用
 戶可以設置負的(比較高的)優先級. 相關的命令是renice, snice, 和skill.
nohup
 保持一個命令的運行, 即使用戶登出系統. 這個命令做爲前臺進程來運行, 除非前邊加 &.
 如果你在腳本中使用nohup命令, 最好和wait 命令一起使用, 這樣可以避免創建一個
 孤兒進程或殭屍進程.
pidof
 取得一個正在運行的作業的進程ID(PID). 因爲一些作業控制命令, 比如kill和renice只
 能使用進程的PID(而不是它的名字), 所以有時候必須的取得PID. pidof命令與$PPID內部
 變量非常相似.
  bash$ pidof xclock
http://www.818198.com  Page 331
SHELL十三問
  880
Example 13-6 pidof 幫助殺掉一個進程
################################Start Script#######################################
 1 #!/bin/bash
 2 # kill-process.sh
 3
 4 NOPROCESS=2
 5
 6 process=xxxyyyzzz  # 使用不存在的進程.
 7 # 只不過是爲了演示...
 8 # ... 並不想在這個腳本中殺掉任何真正的進程.
 9 #
10 # 如果, 舉個例子, 你想使用這個腳本來斷線Internet,
11 #     process=pppd
12
13 t=`pidof $process`       # 取得$process的pid(進程id).
14 # 'kill'必須使用pid(不能用程序名).
15
16 if [ -z "$t" ]           # 如果沒這個進程, 'pidof' 返回空.
17 then
18   echo "Process $process was not running."
19   echo "Nothing killed."
20   exit $NOPROCESS
21 fi 
22
23 kill $t                  # 對於頑固的進程可能需要'kill -9'.
24
25 # 這裏需要做一個檢查, 看看進程是否允許自身被kill.
26 # 或許另一個 " t=`pidof $process` " 或者 ...
27
28
29 # 整個腳本都可以使用下邊這句來替換:
30 #    kill $(pidof -x process_name)
31 # 但是這就沒有教育意義了.
32
33 exit 0
################################End Script#########################################
fuser
 取得一個正在存取某個或某些文件(或目錄)的進程ID. 使用-k選項將會殺掉這些進程. 對
 於系統安全來說, 尤其是在腳本中想阻止未被授權的用戶存取系統服務的時候, 這個命令
 就顯得很有用了.
  bash$ fuser -u /usr/bin/vim
  /usr/bin/vim:         3207e(bozo)
 
  bash$ fuser -u /dev/null
  /dev/null:            3009(bozo)  3010(bozo)  3197(bozo)  3199(bozo)
 當正常的插入或刪除保存的媒體, 比如CD ROM或者USB閃存設備的時候, fuser的應用也顯
 得特別重要. 有時候當你想umount一個設備失敗的時候(出現設備忙的錯誤消息), 這意味
http://www.818198.com  Page 332
SHELL十三問
 着某些用戶或進程正在存取這個設備. 使用fuser -um /dev/device_name可以搞定這些,
 這樣你就可以殺掉所有相關的進程.
  bash$ umount /mnt/usbdrive
  umount: /mnt/usbdrive: device is busy
 
  bash$ fuser -um /dev/usbdrive
  /mnt/usbdrive:        1772c(bozo)
 
  bash$ kill -9 1772
  bash$ umount /mnt/usbdrive
 fuser 的-n選項可以獲得正在存取某一端口的進程. 當和nmap命令組合使用的時候尤其
 有用.
  root# nmap localhost.localdomain
  PORT     STATE SERVICE
  25/tcp   open  smtp
 
  root# fuser -un tcp 25
  25/tcp:               2095(root)
 
  root# ps ax | grep 2095 | grep -v grep
  2095 ?        Ss     0:00 sendmail: accepting connections
cron
 管理程序調度器, 執行一些日常任務, 比如清除和刪除系統log文件, 或者更新slocate命
 令的數據庫. 這是at命令的超級用戶版本(雖然每個用戶都可以有自己的crontab文件, 並
 且這個文件可以使用crontab命令來修改). 它以幽靈進程T的身份來運行, 並且從
 /ect/crontab中獲得執行的調度入口.
 注意: 一些Linux的風格都使用crond, Matthew Dillon的cron.
進程控制和啓動類
init
 init 命令是所有進程的父進程. 在系統啓動的最後一步調用, init 將會依據
 /etc/inittab來決定系統的運行級別. 只能使用root身份來運行它的別名telinit.
telinit
 init命令的符號鏈接, 這是一種修改系統運行級別的一個手段, 通常在系統維護或者緊急
 的文件系統修復的時候才用. 只能使用root身份調用. 調用這個命令是非常危險的 - 在
 你使用之前確定你已經很好地瞭解它.
runlevel
 顯示當前和最後的運行級別, 也就是, 確定你的系統是否終止(runlevel 爲0), 還是運行
 在單用戶模式(1), 多用戶模式(2), 或者是運行在X Windows(5), 還是正在重啓(6). 這
 個命令將會存取/var/run/utmp文件.
halt, shutdown, reboot
 設置系統關機的命令, 通常比電源關機的優先級高.
service
 開啓或停止一個系統服務. 啓動腳本在/etc/init.d中, 並且/etc/rc.d在系統啓動的時候
 使用這個命令來啓動服務.
  root# /sbin/service iptables stop
  Flushing firewall rules:                                   [  OK  ]
  Setting chains to policy ACCEPT: filter                    [  OK  ]
  Unloading iptables modules:                                [  OK  ]
http://www.818198.com  Page 333
SHELL十三問
網絡類
ifconfig
 網絡的接口配置和調試工具.
  bash$ ifconfig -a
  lo        Link encap:Local Loopback
      inet addr:127.0.0.1  Mask:255.0.0.0
      UP LOOPBACK RUNNING  MTU:16436  Metric:1
      RX packets:10 errors:0 dropped:0 overruns:0 frame:0
      TX packets:10 errors:0 dropped:0 overruns:0 carrier:0
      collisions:0 txqueuelen:0
      RX bytes:700 (700.0 b)  TX bytes:700 (700.0 b)
 ifconfig 命令絕大多數情況都是在啓動時候設置接口, 或者在重啓的時候關閉它們.
    1 # 來自於 /etc/rc.d/init.d/network 的代碼片段
    2
    3 # ...
    4
    5 # 檢查網絡是否啓動.
    6 [ ${NETWORKING} = "no" ] && exit 0
    7
    8 [ -x /sbin/ifconfig ] || exit 0
    9
   10 # ...
   11
   12 for i in $interfaces ; do
   13   if ifconfig $i 2>/dev/null | grep -q "UP" >/dev/null 2>&1 ; then
   14     action "Shutting down interface $i: " ./ifdown $i boot
   15   fi
   16 # grep命令的GNU指定的 "-q" 的意思是"安靜", 也就是不產生輸出.
   17 # 這樣, 後邊重定向到/dev/null的操作就有點重複了.
   18       
   19 # ...
   20
   21 echo "Currently active devices:"
   22 echo `/sbin/ifconfig | grep ^[a-z] | awk '{print $1}'`
   23 #                            ^^^^^  應該被引用防止globbing.
   24 #  下邊這段也能工作.
   25 #    echo $(/sbin/ifconfig | awk '/^[a-z]/ { print $1 })'
   26 #    echo $(/sbin/ifconfig | sed -e 's/ .*//')
   27 #  Thanks, S.C.做了額外的註釋.
 參見 Example 29-6.
iwconfig
 這是爲了配置無線網絡的命令集合. 可以說是上邊的ifconfig的無線版本.
route
 顯示內核路由表信息, 或者查看內核路由表的修改.
  bash$ route
  Destination     Gateway         Genmask         Flags   MSS Window  irtt Iface
  pm3-67.bozosisp *               255.255.255.255 UH       40 0          0 ppp0
  127.0.0.0       *               255.0.0.0       U        40 0          0 lo
http://www.818198.com  Page 334
SHELL十三問
  default         pm3-67.bozosisp 0.0.0.0         UG       40 0          0 ppp0
chkconfig
 檢查網絡配置. 這個命令負責顯示和管理在啓動過程中所開啓的網絡服務(這些服務都是
 從/etc/rc?.d目錄中開啓的).
 最開始是從IRIX到Red Hat Linux的一個接口, chkconfig在某些Linux發行版中並不是核
 心安裝的一部分.
  bash$ chkconfig --list
  atd             0:off   1:off   2:off   3:on    4:on    5:on    6:off
  rwhod           0:off   1:off   2:off   3:off   4:off   5:off   6:off
  ...
tcpdump
 網絡包的"嗅探器". 這是一個用來分析和調試網絡上傳輸情況的工具, 它所使用的手段是
 把匹配指定規則的包頭都顯示出來.
 顯示主機bozoville和主機caduceus之間所有傳輸的ip包.
  bash$ tcpdump ip host bozoville and caduceus
 當然,tcpdump的輸出可以被分析, 可以用我們之前討論的文本處理工具來分析結果.
文件系統類
mount
 加載一個文件系統, 通常都用來安裝外部設備, 比如軟盤或CDROM. 文件/etc/fstab 將會
 提供一個方便的列表, 這個列表列出了所有可用的文件系統, 分區和設備, 另外還包括某
 些選項, 比如是否可以自動或者手動的mount. 文件/etc/mtab 顯示了當前已經mount的文
 件系統和分區(包括虛擬的, 比如/proc).
 mount -a 將會mount所有列在/ect/fstab中的文件系統和分區, 除了那些標記有非自動選
 項的. 在啓動的時候, 在/etc/rc.d中的一個啓動腳本(rc.sysinit或者一些相似的腳本)
 將會這麼調用, mount所有可用的文件系統和分區.
    1 mount -t iso9660 /dev/cdrom /mnt/cdrom
    2 # 加載 CDROM
    3 mount /mnt/cdrom
    4 # 方便的方法, 如果 /mnt/cdrom 包含在 /etc/fstab 中
 這個多功能的命令甚至可以將一個普通文件mount到塊設備中, 並且這個文件就好像一個
 文件系統一樣. mount可以將文件與一個loopback設備相關聯來達到這個目的.
 ccomplishes that by associating the file with a loopback device. 這種應用通常
 都是用來mount和檢查一個ISO9660鏡像,在這個鏡像被燒錄到CDR之前. [3]
Example 13-7 檢查一個CD鏡像
################################Start Script#######################################
1 # 以root身份...
2
3 mkdir /mnt/cdtest  # 如果沒有的話,準備一個mount點.
4
5 mount -r -t iso9660 -o loop cd-image.iso /mnt/cdtest   # mount這個鏡像.
6 #                  "-o loop" option equivalent to "losetup /dev/loop0"
7 cd /mnt/cdtest     # 現在檢查這個鏡像.
8 ls -alR            # 列出目錄樹中的文件.
9                    # 等等.
################################End Script#########################################
umount
 卸除一個當前已經mount的文件系統. 在正常刪除之前已經mount的軟盤和CDROM之前, 這
 個設備必須被unmount, 否則文件系統將會損壞.
http://www.818198.com  Page 335
SHELL十三問
   1 umount /mnt/cdrom
   2 # 現在你可以按下退出按鈕(指的是cdrom或軟盤驅動器上的退出鈕), 並安全的退出光盤.
sync
 強制寫入所有需要更新的buffer上的數據到硬盤上(同步帶有buffer的驅動器). 如果不是
 嚴格必要的話,一個sync就可以保證系統管理員或者用戶剛剛修改的數據會安全的在突然
 的斷點中倖存下來. 在比較早以前, 在系統重啓前都是使用 sync; sync (兩次, 這樣保
 證絕對可靠), 這是一種很有用的小心的方法.
 有時候, 比如當你想安全刪除一個文件的時候(參見 Example 12-55), 或者當磁盤燈開始
 閃爍的時候, 你可能需要強制馬上進行buffer刷新.
losetup
 建立和配置loopback設備.
Example 13-8 在一個文件中創建文件系統
################################Start Script#######################################
1 SIZE=1000000  # 1M
2
3 head -c $SIZE < /dev/zero > file  # 建立指定尺寸的文件.
4 losetup /dev/loop0 file           # 作爲loopback設備來建立.
5 mke2fs /dev/loop0                 # 創建文件系統.
6 mount -o loop /dev/loop0 /mnt     # Mount它.
7
8 # Thanks, S.C.
################################End Script#########################################
mkswap
 創建一個交換分區或文件. 交換區域隨後必須馬上使用swapon來使能.
swapon, swapoff
 使能/禁用 交換分區或文件. 這兩個命令通常在啓動和關機的時候纔有效.
mke2fs
 創建Linux ext2 文件系統. 這個命令必須以root身份調用.
Example 13-9 添加一個新的硬盤驅動器
################################Start Script#######################################
 1 #!/bin/bash
 2
 3 # 在系統上添加第二塊硬盤驅動器.
 4 # 軟件配置. 假設硬件已經安裝了.
 5 # 來自於本書作者的一篇文章.
 6 # 在"Linux Gazette"的問題#38上, http://www.linuxgazette.com.
 7
 8 ROOT_UID=0     # 這個腳本必須以root身份運行.
 9 E_NOTROOT=67   # 非root用戶將會產生這個錯誤.
10
11 if [ "$UID" -ne "$ROOT_UID" ]
12 then
13   echo "Must be root to run this script."
14   exit $E_NOTROOT
15 fi 
16
17 # 要非常謹慎的小心使用!
18 # 如果某步錯了, 可能會徹底摧毀你當前的文件系統.
http://www.818198.com  Page 336
SHELL十三問
19
20
21 NEWDISK=/dev/hdb         # 假設/dev/hdb空白. 檢查一下!
22 MOUNTPOINT=/mnt/newdisk  # 或者選擇另外的mount點.
23
24
25 fdisk $NEWDISK
26 mke2fs -cv $NEWDISK1   # 檢查壞塊, 詳細輸出.
27 #  注意:    /dev/hdb1, *不是* /dev/hdb!
28 mkdir $MOUNTPOINT
29 chmod 777 $MOUNTPOINT  # 讓所有用戶都具有全部權限.
30
31
32 # 現在, 測試一下...
33 # mount -t ext2 /dev/hdb1 /mnt/newdisk
34 # 嘗試創建一個目錄.
35 # 如果工作起來了, umount它, 然後繼續.
36
37 # 最後一步:
38 # 將下邊這行添加到/etc/fstab.
39 # /dev/hdb1  /mnt/newdisk  ext2  defaults  1 1
40
41 exit 0
################################End Script#########################################
 參見 Example 13-8 和 Example 28-3.
tune2fs
 調整ext2文件系統. 可以用來修改文件系統參數, 比如mount的最大數量. 必須以root身
 份調用.
 注意: 這是一個非常危險的命令. 如果壞了, 你需要自己負責, 因爲它可能會破壞你的文
  件系統.
dumpe2fs
 打印(輸出到stdout上)非常詳細的文件系統信息. 必須以root身份調用.
  root# dumpe2fs /dev/hda7 | grep 'ount count'
  dumpe2fs 1.19, 13-Jul-2000 for EXT2 FS 0.5b, 95/08/09
  Mount count:              6
  Maximum mount count:      20
hdparm
 列出或修改硬盤參數. 這個命令必須以root身份調用, 如果濫用的話會有危險.
fdisk
 在存儲設備上(通常都是硬盤)創建和修改一個分區表. 必須以root身份使用.
 注意: 謹慎使用這個命令. 如果出錯, 會破壞你現存的文件系統.
fsck, e2fsck, debugfs
 文件系統的檢查, 修復, 和除錯命令集合.
 fsck: 檢查UNIX文件系統的前端工具(也可以調用其它的工具). 文件系統的類型一般都是
  默認的ext2.
 e2fsck: ext2文件系統檢查器.
 debugfs: ext2文件系統除錯器. 這個多功能但是危險的工具的用處之一就是(嘗試)恢復
  刪除的文件. 只有高級用戶才能用.
http://www.818198.com  Page 337
SHELL十三問
 上邊的這幾個命令都必須以root身份調用, 這些命令都很危險, 如果濫用的話會破壞文件
 系統.
badblocks
 檢查存儲設備的壞塊(物理損壞). 這個命令在格式化新安裝的硬盤時或者測試備份的完整
 性的時候會被用到. [4] 舉個例子, badblocks /dev/fd0 測試一個軟盤.
 badblocks可能會引起比較糟糕的結果(覆蓋所有數據), 在只讀模式下就不會發生這種情
 況.如果root用戶擁有需要測試的設備(通常都是這種情況), 那麼root用戶必須調用這個
 命令.
lsusb, usbmodules
 lsusb 命令會列出所有USB(Universal Serial Bus通用串行總線)總線和使用USB的設備.
 usbmodules 命令會輸出連接USB設備的驅動模塊的信息.
  root# lsusb
  Bus 001 Device 001: ID 0000:0000 
  Device Descriptor:
    bLength                18
    bDescriptorType         1
    bcdUSB               1.00
    bDeviceClass            9 Hub
    bDeviceSubClass         0
    bDeviceProtocol         0
    bMaxPacketSize0         8
    idVendor           0x0000
    idProduct          0x0000
    . . .
mkbootdisk
 創建啓動軟盤, 啓動盤可以喚醒系統, 比如當MBR(master boot record主啓動記錄)壞掉
 的時候. mkbootdisk 命令其實是一個Bash腳本, 由Erik Troan所編寫, 放在/sbin目錄中.
chroot
 修改ROOT目錄. 一般的命令都是從$PATH中獲得的, 相對的默認的根目錄是 /. 這個命令
 將會把根目錄修改爲另一個目錄(並且也將把工作目錄修改到那). 出於安全目的, 這個命
 令時非常有用的, 舉個例子, 當系統管理員希望限制一些特定的用戶, 比如telnet上來的
 用戶, 將他們限定到文件系統上一個安全的地方(這有時候被稱爲將一個guest用戶限制在
 "chroot 監牢"中). 注意, 在使用chroot之後, 系統的二進制可執行文件的目錄將不再
 可用了.
 chroot /opt 將會使得原來的/usr/bin目錄變爲/opt/usr/bin. 同樣,
 chroot /aaa/bbb /bin/ls 將會使得ls命令以/aaa/bbb作爲根目錄, 而不是以前的/.
 如果使用alias XX 'chroot /aaa/bbb ls', 並把這句放到用戶的~/.bashrc文件中的話,
 這將可以有效地限制運行命令"XX"時, 命令"XX"可以使用文件系統的範圍.
 當從啓動盤恢復的時候(chroot 到 /dev/fd0), 或者當系統從死機狀態恢復過來並作爲進
 入lilo的選擇手段的時候, chroot命令都是非常方便的. 其它的應用還包括從不同的文件
 系統進行安裝(一個rpm選項)或者從CDROM上運行一個只讀文件系統. 只能以root身份調用,
 小心使用.
 注意: 由於正常的$PATH將不再被關聯了, 所以可能需要將一些特定的系統文件拷貝到
  chrooted目錄中.
lockfile
 這個工具是procmail包的一部分(www.procmail.org). 它可以創建一個鎖定文件, 鎖定文
 件是一種用來控制存取文件, 設備或資源的標記文件. 鎖定文件就像一個標記一樣被使用,
  如果特定的文件, 設備, 或資源正在被一個特定的進程所使用("busy"), 那麼對於其它進
http://www.818198.com  Page 338
SHELL十三問
 程來說, 就只能受限進行存取(或者不能存取).
    1 lockfile /home/bozo/lockfiles/$0.lock
    2 # 創建一個以腳本名字爲前綴的寫保護鎖定文件.
 鎖定文件用在一些特定的場合, 比如說保護系統的mail目錄以防止多個用戶同時修改, 或
 者提示一個modem端口正在被存取, 或者顯示Netscape的一個實例正在使用它的緩存. 腳本
 可以做一些檢查工作, 比如說一個特定的進程可以創建一個鎖定文件, 那麼只要檢查這個
 特定的進程是否在運行, 就可以判斷出鎖定文件是否存在了. 注意如果腳本嘗試創建一個
 已經存在的鎖定文件的話, 那麼腳本很可能被掛起.
 一般情況下, 應用創建或檢查鎖定文件都放在/var/lock目錄中. [5] 腳本可以使用下面
 的方法來檢測鎖定文件是否存在.
    1 appname=xyzip
    2 # 應用 "xyzip" 創建鎖定文件 "/var/lock/xyzip.lock".
    3
    4 if [ -e "/var/lock/$appname.lock" ]
    5 then
    6   ...
flock<rojy bug>
 flock命令比lockfile命令用得少得多.Much less useful than the lockfile command
 is flock. It sets an "advisory" lock on a file and then executes a command
 while the lock is on. This is to prevent any other process from setting a lock
 on that file until completion of the specified command.
    1 flock $0 cat $0 > lockfile__$0
    2 #  Set a lock on the script the above line appears in,
    3 #+ while listing the script to stdout.
 注意: 與lockfile不同, flock不會自動創建一個鎖定文件.
mknod
 創建塊或者字符設備文件(當在系統上安裝新硬盤時可能是必要的). MAKEDEV工具事實上
 具有nknod的全部功能, 而且更容易使用.
MAKEDEV
 創建設備文件的工具. 必須在/dev目錄下, 並且以root身份使用.
  root# ./MAKEDEV
 這是mknod的高級版本.
tmpwatch
 自動刪除在指定時間內未被存取過的文件. 通常都是被cron調用, 用來刪掉老的log文件.
備份類
dump, restore
 dump 命令是一個精巧的文件系統備份工具, 通常都用在比較大的安裝和網絡上. [6] 它
 讀取原始的磁盤分區並且以二進制形式來寫備份文件. 需要備份的文件可以保存到各種各
 樣的存儲設備上, 包括磁盤和磁帶. restore命令用來恢復dump所產生的備份.
fdformat
 對軟盤進行低級格式化.
系統資源類
ulimit
 設置使用系統資源的上限. 通常情況下都是使用-f選項來調用, -f用來設置文件尺寸的限
 制(ulimit -f 1000就是將文件大小限制爲1M). -c(譯者注: 這裏應該是作者筆誤, 作者
 寫的是-t)選項來限制coredump(譯者注: 核心轉儲, 程序崩潰時的內存狀態寫入文件)
 尺寸(ulimit -c 0 就是不要coredumps). 一般情況下, ulimit的值應該設置在
 /etc/profile 和(或)~/.bash_profile中(參見 Appendix G).
http://www.818198.com  Page 339
SHELL十三問
 注意: Judicious 使用ulimit 可以保護系統免受可怕的fork炸彈的迫害.
     1 #!/bin/bash
     2 # 這個腳本只是爲了展示用.
     3 # 你要自己爲運行這個腳本的後果負責 -- 它*將*凝固你的系統.
     4
     5 while true  #  死循環.
     6 do
     7   $0 &      #  這個腳本調用自身 . . .
     8             #+ fork無限次 . . .
     9             #+ 直道系統完全不動, 因爲所有的資源都耗盡了.
    10 done        #  這就是臭名卓著的 "sorcerer's appentice" 劇情.<rojy bug>(譯者注:巫師的廂房?沒看懂)
    11
    12 exit 0      #  這裏不會真正的推出, 因爲這個腳本不會終止.
  當這個腳本超過預先設置的限制時, 在/etc/profile中的 ulimit -Hu XX (XX 就是需
  要限制的用戶進程) 可以終止這個腳本的運行.
quota
 顯示用戶或組的磁盤配額.
setquota
 從命令行中設置用戶或組的磁盤配額.
umask
 設定用戶創建文件時權限的缺省mask(掩碼). 也可以用來限制特定用戶的默認文件屬性.
 所有用戶創建的文件屬性都是由umask所指定的. The (octal) 傳遞給umask的8進制的值定
 義了文件的權限. 比如, umask 022將會使得新文件的權限最多爲755(777 與非 022) [7]
 當然, 用戶可以隨後使用chmod來修改指定文件的屬性. 用戶一般都是將umask設置值的地
 方放在/etc/profile 和(或) ~/.bash_profile中 (參見 Appendix G).
Example 13-10 使用umask來將輸出文件隱藏起來
################################Start Script#######################################
 1 #!/bin/bash
 2 # rot13a.sh: 與"rot13.sh"腳本相同, 但是會將輸出寫道"安全"文件中.
 3
 4 # 用法: ./rot13a.sh filename
 5 # 或     ./rot13a.sh <filename
 6 # 或     ./rot13a.sh 同時提供鍵盤輸入(stdin)
 7
 8 umask 177               #  文件創建掩碼.
 9                         #  被這個腳本所創建的文件
10                         #+ 將具有600權限.
11
12 OUTFILE=decrypted.txt   #  結果保存在"decrypted.txt"中
13                         #+ 這個文件只能夠被
14                         #  這個腳本的調用者(or root)所讀寫.
15
16 cat "$@" | tr 'a-zA-Z' 'n-za-mN-ZA-M' > $OUTFILE
17 #    ^^ 從stdin 或文件中輸入.         ^^^^^^^^^^ 輸出重定向到文件中.
18
19 exit 0
################################End Script#########################################
rdev
http://www.818198.com  Page 340
SHELL十三問
 取得root device, swap space, 或 video mode的相關信息, 或者對它們進行修改. 通常
 說來rdev都是被lilo所使用, 但是在建立一個ram disk的時候, 這個命令也很有用. 小心
 使用, 這是一個危險的命令.
模塊類
lsmod
 列出所有安裝的內核模塊.
  bash$ lsmod
  Module                  Size  Used by
  autofs                  9456   2 (autoclean)
  opl3                   11376   0
  serial_cs               5456   0 (unused)
  sb                     34752   0
  uart401                 6384   0 [sb]
  sound                  58368   0 [opl3 sb uart401]
  soundlow                 464   0 [sound]
  soundcore               2800   6 [sb sound]
  ds                      6448   2 [serial_cs]
  i82365                 22928   2
  pcmcia_core            45984   0 [serial_cs ds i82365]
 注意: 使用cat /proc/modules可以得到同樣的結果.
insmod
 強制一個內核模塊的安裝(如果可能的話, 使用modprobe來代替) 必須以root身份調用.
rmmod
 強制卸載一個內核模塊. 必須以root身份調用.
modprobe
 模塊裝載器, 一般情況下都是在啓動腳本中自動調用. 必須以root身份調用.
depmod
 創建模塊依賴文件, 一般都是在啓動腳本中調用.
modinfo
 輸出一個可裝載模塊的信息.
  bash$ modinfo hid
  filename:    /lib/modules/2.4.20-6/kernel/drivers/usb/hid.o
  description: "USB HID support drivers"
  author:      "Andreas Gal, Vojtech Pavlik <[email protected]>"
  license:     "GPL"
雜項類
env
 使用設置過的或修改過(並不是修改整個系統環境)的環境變量來運行一個程序或腳本. 使
 用 [varname=xxx] 形式可以在腳本中修改環境變量. 如果沒有指定參數, 那麼這個命令
 將會列出所有設置的環境變量.
 注意: 在Bash和其它的Bourne shell 衍生物中, 是可以在單一命令行上設置多個變量的.
     1 var1=value1 var2=value2 commandXXX
     2 # $var1 和 $var2 只設置在'commandXXX'的環境中.
 注意: 當不知道shell或解釋器的路徑的時候, 腳本的第一行(#!行)可以使用env.
     1 #! /usr/bin/env perl
     2
     3 print "This Perl script will run,\n";
     4 print "even when I don't know where to find Perl.\n";
http://www.818198.com  Page 341
SHELL十三問
     5
     6 # 便於跨平臺移植,
     7 # Perl程序可能沒在期望的地方.
     8 # Thanks, S.C.
ldd
 顯示一個可執行文件的共享庫的依賴關係.
  bash$ ldd /bin/ls
  libc.so.6 => /lib/libc.so.6 (0x4000c000)
 /lib/ld-linux.so.2 => /lib/ld-linux.so.2 (0x80000000)
watch
 以指定的時間間隔來重複運行一個命令.
 默認的時間間隔是2秒, 但時刻以使用-n選項來修改.
    1 watch -n 5 tail /var/log/messages
    2 # 每隔5秒鐘顯示系統log文件的結尾, /var/log/messages.
strip
 從可執行文件中去掉調試符號引用. 這樣做可以減小尺寸, 但是就不能調試了.
 這個命令一般都用在Makefile中, 但是很少用在shell腳本中.
nm
 列出未strip過的編譯後的2進制文件的符號.
rdist
 遠程文件分佈客戶機程序: 在遠端服務器上同步, 克隆, 或者備份一個文件系統.
13.1 分析一個系統腳本
---------------------利用我們所學到的關於管理命令的知識, 讓我們一起來練習分析一個系統腳本. 最簡單並
且最短的系統腳本之一是killall, 這個腳本被用來在系統關機時掛起運行的腳本.
Example 13-11 killall, 來自於 /etc/rc.d/init.d
################################Start Script#######################################
 1 #!/bin/sh
 2
 3 # --> 本書作者所作的註釋全部以"# -->"開頭.
 4
 5 # --> 這是由Miquel van Smoorenburg所編寫的
 6 # --> 'rc'腳本包的一部分, <[email protected]>.
 7
 8 # --> 這個特殊的腳本看起來是是爲Red Hat / FC所特定的,
 9 # --> (在其它的發行版中可能不會出現).
10
11 #  停止所有正在運行的不必要的服務
12 #+ (there shouldn't be any, so this is just a sanity check)
13
14 for i in /var/lock/subsys/*; do
15         # --> 標準的for/in循環, 但是由於"do"在同一行上,
16         # --> 所以必須添加";".
17         # 檢查腳本是否在那.
18         [ ! -f $i ] && continue
19         # --> 這是一種使用"與列表"的聰明的方法, 等價於:
20         # --> if [ ! -f "$i" ]; then continue
21
http://www.818198.com  Page 342
SHELL十三問
22         # 取得子系統的名字.
23         subsys=${i#/var/lock/subsys/}
24         # --> 匹配變量名, 在這裏就是文件名.
25         # --> 與subsys=`basename $i`完全等價.
26 
27         # -->  從鎖定文件名中獲得
28         # -->+ (如果那裏有鎖定文件的話,
29         # -->+ 那就證明進程正在運行).
30         # -->  參考一下上邊所講的"鎖定文件"的內容.
31
32
33         # 終止子系統.
34         if [ -f /etc/rc.d/init.d/$subsys.init ]; then
35            /etc/rc.d/init.d/$subsys.init stop
36         else
37            /etc/rc.d/init.d/$subsys stop
38         # -->  掛起運行的作業和幽靈進程.
39         # -->  注意"stop"只是一個位置參數,
40         # -->+ 並不是shell內建命令.
41         fi
42 done
################################End Script#########################################
這個沒有那麼糟. 除了在變量匹配的地方玩了一點花樣, 其它也沒有別的材料了.
練習 1. 在/etc/rc.d/init.d中, 分析halt腳本. 比腳本killall長一些, 但是概念上很相近.
  對這個腳本做一個拷貝, 放到你的home目錄下並且用它練習一下(不要以root身份運
  行它). 使用-vn標誌來模擬運行一下(sh -vn scriptname). 添加詳細的註釋. 將
  "action"命令修改爲"echos".
練習 2. 察看/etc/rc.d/init.d下的更多更復雜的腳本. 看看你是不是能夠理解其中的一些腳
  本. 使用上邊的過程來分析這些腳本. 爲了更詳細的理解, 你可能也需要分析在
  usr/share/doc/initscripts-?.??目錄下的文件sysvinitfile, 這些都是
  "initscript"文件的一部分.
注意事項:
[1]  這是在Linux機器上或者在帶有磁盤配額的UNIX系統上的真實情況.
[2]  如果正在被刪除的特定的用戶已經登錄了主機, 那麼 userdel 命令將會失敗.
[3]  對於燒錄CDR的更多的細節, 可以參見Alex Withers的文章, 創建CD, 在
  Linux Journal 的1999年的10月文章列表中.
[4]  mke2fs的-c選項也會進行壞塊檢查.
[5]  因爲只有root用戶才具有對/var/lock目錄的寫權限, 一般的用戶腳本是不能在那裏
  設置一個鎖定文件的.
[6]  單用戶的Linux系統的操作更傾向於使用簡單的備份工具, 比如tar.
[7]  NAND(與非)是一種邏輯操作. 這種操作的效果和減法很相像.
 
第14章 命令替換
================
命令替換將會重新分配一個命令[1]甚至是多個命令的輸出; 它會將命令的輸出如實地添加到
另一個上下文中. [2]
使用命令替換的典型形式是使用後置引用(`...`). 後置引用形式的命令(就是被反引號括起來)
將會產生命令行文本.
http://www.818198.com  Page 343
SHELL十三問
   1 script_name=`basename $0`
   2 echo "The name of this script is $script_name."
這樣的話, 命令的輸出可以被當成傳遞到另一個命令的參數, 或者保存到變量中, 甚至可以用
來產生for循環的參數列表.
   1 rm `cat filename`   # "filename" 包含了需要被刪除的文件列表.
   2 #
   3 # S. C. 指出使用這種形式, 可能會產生"參數列表太長"的錯誤.
   4 # 更好的方法是              xargs rm -- < filename
   5 # ( -- 同時覆蓋了那些以"-"開頭的文件所產生的特殊情況 )
   6
   7 textfile_listing=`ls *.txt`
   8 # 變量中包含了當前工作目錄下所有的*.txt文件.
   9 echo $textfile_listing
  10
  11 textfile_listing2=$(ls *.txt)   # 這是命令替換的另一種形式.
  12 echo $textfile_listing2
  13 # 同樣的結果.
  14
  15 # 將文件列表放入到一個字符串中的一個可能的問題就是
  16 # 可能會混進一個新行.
  17 #
  18 # 一個安全的將文件列表傳遞到參數中的方法就是使用數組.
  19 #      shopt -s nullglob    # 如果不匹配, 那就不進行文件名擴展.
  20 #      textfile_listing=( *.txt )
  21 #
  22 # Thanks, S.C.
注意: 命令替換將會調用一個subshell.
注意: 命令替換可能會引起word splitting.
   1 COMMAND `echo a b`     # 2個參數: a and b
   2
   3 COMMAND "`echo a b`"   # 1個參數: "a b"
   4
   5 COMMAND `echo`         # 無參數
   6
   7 COMMAND "`echo`"       # 一個空的參數
   8
   9
  10 # Thanks, S.C.
 即使沒有引起word splitting, 命令替換也會去掉多餘的新行.
   1 # cd "`pwd`"  # 這句總會正常的工作.
   2 # 然而...
   3
   4 mkdir 'dir with trailing newline
   5 '
   6
   7 cd 'dir with trailing newline
   8 '
   9
http://www.818198.com  Page 344
SHELL十三問
  10 cd "`pwd`"  # 錯誤消息:
  11 # bash: cd: /tmp/file with trailing newline: No such file or directory
  12
  13 cd "$PWD"   # 運行良好.
  14
  15
  16
  17
  18
  19 old_tty_setting=$(stty -g)   # 保存老的終端設置.
  20 echo "Hit a key "
  21 stty -icanon -echo           # 對終端禁用"canonical"模式.
  22                              # 這樣的話, 也會禁用了*本地*的echo.
  23 key=$(dd bs=1 count=1 2> /dev/null)   # 使用'dd'命令來取得一個按鍵.
  24 stty "$old_tty_setting"      # 保存老的設置.
  25 echo "You hit ${#key} key."  # ${#variable} = number of characters in $variable
  26 #
  27 # 按鍵任何鍵除了回車, 那麼輸出就是"You hit 1 key."
  28 # 按下回車, 那麼輸出就是"You hit 0 key."
  29 # 新行已經被命令替換吃掉了.
  30
  31 Thanks, S.C.
注意: 當一個變量是使用命令替換的結果做爲值的時候, 然後使用echo命令來輸出這個變量
 (並且不引用這個變量, 就是不用引號括起來), 那麼命令替換將會從最終的輸出中刪掉換
 行符. 這可能會引起一些異常情況.
   1 dir_listing=`ls -l`
   2 echo $dir_listing     # 未引用, 就是沒用引號括起來
   3
   4 # 想打出來一個有序的目錄列表.Expecting a nicely ordered directory listing.
   5
   6 # 可惜, 下邊將是我們所獲得的:
   7 # total 3 -rw-rw-r-- 1 bozo bozo 30 May 13 17:15 1.txt -rw-rw-r-- 1 bozo
   8 # bozo 51 May 15 20:57 t2.sh -rwxr-xr-x 1 bozo bozo 217 Mar 5 21:13 wi.sh
   9
  10 # 新行消失了.
  11
  12
  13 echo "$dir_listing"   # 用引號括起來
  14 # -rw-rw-r--    1 bozo       30 May 13 17:15 1.txt
  15 # -rw-rw-r--    1 bozo       51 May 15 20:57 t2.sh
  16 # -rwxr-xr-x    1 bozo      217 Mar  5 21:13 wi.sh
命令替換甚至允許將整個文件的內容放到變量中, 可以使用重定向或者cat命令.
   1 variable1=`<file1`      #  將"file1"的內容放到"variable1"中.
   2 variable2=`cat file2`   #  將"file2"的內容放到"variable2"中.
   3                         #  但是這行將會fork一個新進程, This, however, forks a new process,
   4                         #+ 所以這行代碼將會比第一行代碼執行得慢.
   5
   6 #  注意:
http://www.818198.com  Page 345
SHELL十三問
   7 #  變量中是可以包含空白的,
   8 #+ 甚至是 (厭惡至極的), 控制字符.
   1 #  摘錄自系統文件, /etc/rc.d/rc.sysinit
   2 #+ (這是紅帽安裝中使用的)
   3
   4
   5 if [ -f /fsckoptions ]; then
   6         fsckoptions=`cat /fsckoptions`
   7 ...
   8 fi
   9 #
  10 #
  11 if [ -e "/proc/ide/${disk[$device]}/media" ] ; then
  12              hdmedia=`cat /proc/ide/${disk[$device]}/media`
  13 ...
  14 fi
  15 #
  16 #
  17 if [ ! -n "`uname -r | grep -- "-"`" ]; then
  18        ktag="`cat /proc/version`"
  19 ...
  20 fi
  21 #
  22 #
  23 if [ $usb = "1" ]; then
  24     sleep 5
  25     mouseoutput=`cat /proc/bus/usb/devices 2>/dev/null|grep -E "^I.*Cls=03.*Prot=02"`
  26     kbdoutput=`cat /proc/bus/usb/devices 2>/dev/null|grep -E "^I.*Cls=03.*Prot=01"`
  27 ...
  28 fi
注意: 不要將一個非常長的文本文件的內容設置到一個變量中, 除非你有一個非常好的原因非
 要這麼做不可. 不要將2進制文件的內容保存到變量中.
Example 14-1 愚蠢的腳本策略
################################Start Script#######################################
 1 #!/bin/bash
 2 # stupid-script-tricks.sh: 朋友, 別在家這麼做.
 3 # 來自於"Stupid Script Tricks," 卷I.
 4
 5
 6 dangerous_variable=`cat /boot/vmlinuz`   # 這是壓縮過的Linux內核本身.
 7
 8 echo "string-length of \$dangerous_variable = ${#dangerous_variable}"
 9 # 這個字符串變量的長度是 $dangerous_variable = 794151
10 # (不要使用'wc -c /boot/vmlinuz'來計算長度.)
11
12 # echo "$dangerous_variable"
13 # 千萬別嘗試這麼做! 這樣將掛起這個腳本.
14
http://www.818198.com  Page 346
SHELL十三問
15
16 #  文檔作者已經意識到將二進制文件設置到
17 #+ 變量中是一個沒用的應用.
18
19 exit 0
################################End Script#########################################
 注意, 在這裏是不會發生緩衝區溢出錯誤. 因爲這是一個解釋型語言的實例, Bash就是一
 種解釋型語言, 解釋型語言會比編譯型語言提供更多的對程序錯誤的保護措施.
變量替換允許將一個循環的輸出放入到一個變量中.這麼做的關鍵就是將循環中echo命令的輸
出全部截取.
Example 14-2 從循環的輸出中產生一個變量
################################Start Script#######################################
 1 #!/bin/bash
 2 # csubloop.sh: 從循環的輸出中產生一個變量.
 3
 4 variable1=`for i in 1 2 3 4 5
 5 do
 6   echo -n "$i"                 #  對於這裏的命令替換來說
 7 done`                          #+ 這個'echo'命令是非常關鍵的.
 8
 9 echo "variable1 = $variable1"  # variable1 = 12345
10
11
12 i=0
13 variable2=`while [ "$i" -lt 10 ]
14 do
15   echo -n "$i"                 # 再來一個, 'echo'是必須的.
16   let "i += 1"                 # 遞增.
17 done`
18
19 echo "variable2 = $variable2"  # variable2 = 0123456789
20
21 #  這就證明了在一個變量聲明中
22 #+ 嵌入一個循環是可行的.
23
24 exit 0
################################End Script#########################################
注意: 命令替換使得擴展有效的Bash工具集變爲可能. 這樣, 寫一段小程序或者一段腳本就可
 以達到目的, 因爲程序或腳本的輸出會傳到stdout上(就像一個標準的工具所做的那樣),
 然後重新將這些輸出保存到變量中.(譯者: 作者的意思就是在這種情況下寫腳本和寫程序
 作用是一樣的.)
   1 #include <stdio.h>
   2
   3 /*  "Hello, world." C program  */ 
   4
   5 int main()
   6 {
   7   printf( "Hello, world." );
http://www.818198.com  Page 347
SHELL十三問
   8   return (0);
   9 }
 bash$ gcc -o hello hello.c
   1 #!/bin/bash
   2 # hello.sh 
   3
   4 greeting=`./hello`
   5 echo $greeting
 bash$ sh hello.sh
 Hello, world.
注意: 對於命令替換來說,$(COMMAND) 形式已經取代了反引號"`".
   1 output=$(sed -n /"$1"/p $file)   # 來自於 "grp.sh"例子.
   2       
   3 # 將一個文本的內容保存到變量中.
   4 File_contents1=$(cat $file1)     
   5 File_contents2=$(<$file2)        # Bash 也允許這麼做.
 $(...) 形式的命令替換在處理雙反斜線(\\)時與`...`形式不同.
  bash$ echo `echo \\`
 
  bash$ echo $(echo \\)
 
 $(...) 形式的命令替換是允許嵌套的. [3]
   1 word_count=$( wc -w $(ls -l | awk '{print $9}') )
 或者, 可以更加靈活. . .
Example 14-3 找anagram(迴文構詞法, 可以將一個有意義的單詞, 變換爲1個或多個有意義的單詞, 但是還是原來的子母集合)
################################Start Script#######################################
 1 #!/bin/bash
 2 # agram2.sh
 3 # 關於命令替換嵌套的例子.
 4
 5 #  使用"anagram"工具
 6 #+ 這是作者的"yawl"文字表包中的一部分.
 7 #  http://ibiblio.org/pub/Linux/libs/yawl-0.3.2.tar.gz
 8 #  http://personal.riverusers.com/~thegrendel/yawl-0.3.2.tar.gz
 9
10 E_NOARGS=66
11 E_BADARG=67
12 MINLEN=7
13
14 if [ -z "$1" ]
15 then
16   echo "Usage $0 LETTERSET"
17   exit $E_NOARGS         # 腳本需要一個命令行參數.
18 elif [ ${#1} -lt $MINLEN ]
19 then
20   echo "Argument must have at least $MINLEN letters."
21   exit $E_BADARG
22 fi
http://www.818198.com  Page 348
SHELL十三問
23
24
25
26 FILTER='.......'         # 必須至少有7個字符.
27 #       1234567
28 Anagrams=( $(echo $(anagram $1 | grep $FILTER) ) )
29 #           |     |    嵌套的命令替換        | |
30 #        (              數組分配                 )
31
32 echo
33 echo "${#Anagrams[*]}  7+ letter anagrams found"
34 echo
35 echo ${Anagrams[0]}      # 第一個anagram.
36 echo ${Anagrams[1]}      # 第二個anagram.
37                          # 等等.
38
39 # echo "${Anagrams[*]}"  # 在一行上列出所有的anagram . . .
40
41 #  考慮到後邊還有"數組"作爲單獨的一章進行講解,
42 #+ 這裏就不深入了.
43
44 # 可以參閱agram.sh腳本, 這也是一個找出anagram的例子.
45
46 exit $?
################################End Script#########################################
命令替換在腳本中使用的例子:
1.  Example 10-7
2.  Example 10-26
3.  Example 9-28
4.  Example 12-3
5.  Example 12-19
6.  Example 12-15
7.  Example 12-49
8.  Example 10-13
9.  Example 10-10
10. Example 12-29
11. Example 16-8 
12. Example A-17 
13. Example 27-2 
14. Example 12-42
15. Example 12-43
16. Example 12-44
注意事項:
[1]  對於命令替換來說, 這個命令可以是外部的系統命令, 也可以是內部腳本的內建
  命令, 甚至是一個腳本函數.
[2]  從技術的角度來講, 命令替換將會抽取出一個命令的輸出, 然後使用=操作賦值到
  一個變量中.
[3]  事實上, 對於反引號的嵌套是可行的, 但是隻能將內部的反引號轉義才行, 就像
http://www.818198.com  Page 349
SHELL十三問
  John默認指出的那樣.
     1 word_count=` wc -w \`ls -l | awk '{print $9}'\` `
 
第15章 算術擴展
================
算術擴展提供了一種強力的工具, 可以在腳本中執行(整型)算法操作. 可以使用backticks,
double parentheses, 或 let來將字符串轉換爲數字表達式.
一些變化
使用反引號的算術擴展(通常都是和expr一起使用)
    1 z=`expr $z + 3`          # 'expr'命令將會執行這個擴展.
使用雙括號, 和let形式的算術擴展
反引號形式的算術擴展已經被雙括號形式所替代了 -- ((...)) 和 $((...)) -- 當然也可以
使用非常方便的let形式.
    1 z=$(($z+3))
    2 z=$((z+3))                                  #  也正確.
    3                                             #  使用雙括號的形式,
    4                                             #+ 參數解引用
    5                                             #+ 是可選的.
    6
    7 # $((EXPRESSION)) is arithmetic expansion.  #  不要與命令
    8                                             #+ 替換相混淆.
    9
   10
   11
   12 # 使用雙括號的形式也可以不用給變量賦值.
   13
   14   n=0
   15   echo "n = $n"                             # n = 0
   16
   17   (( n += 1 ))                              # 遞增.
   18 # (( $n += 1 )) is incorrect!
   19   echo "n = $n"                             # n = 1
   20
   21
   22 let z=z+3
   23 let "z += 3"  #  使用引用的形式, 允許在變量賦值的時候存在空格.
   24               #  'let'操作事實上執行得的是算術賦值,
   25               #+ 而不是算術擴展.
下邊是一些在腳本中使用算術擴展的例子:
1.  Example 12-9
2.  Example 10-14
3.  Example 26-1
4.  Example 26-11
5.  Example A-17
第16章 I/O 重定向
==================
默認情況下始終有3個"文件"處於打開狀態, stdin (鍵盤), stdout (屏幕), and stderr
(錯誤消息輸出到屏幕上). 這3個文件和其他打開的文件都可以被重定向. 對於重定向簡單的
http://www.818198.com  Page 350
SHELL十三問
解釋就是捕捉一個文件, 命令, 程序, 腳本, 或者甚至是腳本中的代碼塊(參見 Example 3-1
和 Example 3-2)的輸出, 然後將這些輸出作爲輸入發送到另一個文件, 命令, 程序, 或腳本
中.
每個打開的文件都會被分配一個文件描述符.[1]stdin, stdout, 和stderr的文件描述符分別
是0, 1, 和 2. 對於正在打開的額外文件, 保留了描述符3到9. 在某些時候將這些格外的文件
描述符分配給stdin, stdout, 或者是stderr作爲臨時的副本鏈接是非常有用的. [2] 在經過
複雜的重定向和刷新之後需要把它們恢復成正常的樣子 (參見 Example 16-1).
   1    COMMAND_OUTPUT >
   2       # 重定向stdout到一個文件.
   3       # 如果沒有這個文件就創建, 否則就覆蓋.
   4
   5       ls -lR > dir-tree.list
   6       # 創建一個包含目錄樹列表的文件.
   7
   8    : > filename
   9       # > 會把文件"filename"截斷爲0長度.
  10       # 如果文件不存在, 那麼就創建一個0長度的文件(與'touch'的效果相同).
  11       # : 是一個佔位符, 不產生任何輸出.
  12
  13    > filename   
  14       # > 會把文件"filename"截斷爲0長度.
  15       # 如果文件不存在, 那麼就創建一個0長度的文件(與'touch'的效果相同).
  16       # (與上邊的": >"效果相同, 但是在某些shell下可能不能工作.)
  17
  18    COMMAND_OUTPUT >>
  19       # 重定向stdout到一個文件.
  20       # 如果文件不存在, 那麼就創建它, 如果存在, 那麼就追加到文件後邊.
  21
  22
  23       # 單行重定向命令(只會影響它們所在的行):
  24       # --------------------------------------------------------------------  25
  26    1>filename
  27       # 重定向stdout到文件"filename".
  28    1>>filename
  29       # 重定向並追加stdout到文件"filename".
  30    2>filename
  31       # 重定向stderr到文件"filename".
  32    2>>filename
  33       # 重定向並追加stderr到文件"filename".
  34    &>filename
  35       # 將stdout和stderr都重定向到文件"filename".
  36
  37       #==============================================================================
  38       # 重定向stdout, 一次一行.
  39       LOGFILE=script.log
  40
  41       echo "This statement is sent to the log file, \"$LOGFILE\"." 1>$LOGFILE
http://www.818198.com  Page 351
SHELL十三問
  42       echo "This statement is appended to \"$LOGFILE\"." 1>>$LOGFILE
  43       echo "This statement is also appended to \"$LOGFILE\"." 1>>$LOGFILE
  44       echo "This statement is echoed to stdout, and will not appear in \"$LOGFILE\"."
  45       # 每行過後, 這些重定向命令會自動"reset".
  46
  47
  48
  49       # 重定向stderr, 一次一行.
  50       ERRORFILE=script.errors
  51
  52       bad_command1 2>$ERRORFILE       #  錯誤消息發到$ERRORFILE中.
  53       bad_command2 2>>$ERRORFILE      #  錯誤消息添加到$ERRORFILE中.
  54       bad_command3                    #  錯誤消息echo到stderr,
  55                                       #+ 並且不出現在$ERRORFILE中.
  56       # 每行過後, 這些重定向命令也會自動"reset".
  57       #==============================================================================
  58
  59
  60
  61    2>&1
  62       # 重定向stderr到stdout.
  63       # 得到的錯誤消息與stdout一樣, 發送到一個地方.
  64
  65    i>&j
  66       # 重定向文件描述符i 到 j.
  67       # 指向i文件的所有輸出都發送到j中去.
  68
  69    >&j
  70       # 默認的, 重定向文件描述符1(stdout)到 j.
  71       # 所有傳遞到stdout的輸出都送到j中去.
  72
  73    0< FILENAME
  74     < FILENAME
  75       # 從文件中接受輸入.
  76       # 與">"是成對命令, 並且通常都是結合使用.
  77       #
  78       # grep search-word <filename
  79
  80
  81    [j]<>filename
  82       # 爲了讀寫"filename", 把文件"filename"打開, 並且分配文件描述符"j"給它.
  83       # 如果文件"filename"不存在, 那麼就創建它.
  84       # 如果文件描述符"j"沒指定, 那默認是fd 0, stdin.
  85       #
  86       # 這種應用通常是爲了寫到一個文件中指定的地方.
  87       echo 1234567890 > File    # 寫字符串到"File".
  88       exec 3<> File             # 打開"File"並且給它分配fd 3.
  89       read -n 4 <&3             # 只讀4個字符.
http://www.818198.com  Page 352
SHELL十三問
  90       echo -n . >&3             # 寫一個小數點.
  91       exec 3>&-                 # 關閉fd 3.
  92       cat File                  # ==> 1234.67890
  93       # 隨機存儲.
  94
  95
  96
  97    |
  98       # 管道.
  99       # 通用目的的處理和命令鏈工具.
 100       # 與">"很相似, 但是實際上更通用.
 101       # 對於想將命令, 腳本, 文件和程序串連起來的時候很有用.
 102       cat *.txt | sort | uniq > result-file
 103       # 對所有的.txt文件的輸出進行排序, 並且刪除重複行,
 104       # 最後將結果保存到"result-file"中.
可以將輸入輸出重定向和(或)管道的多個實例結合到一起寫在一行上.
   1 command < input-file > output-file
   2
   3 command1 | command2 | command3 > output-file
參見 Example 12-28 和 Example A-15.
可以將多個輸出流重定向到一個文件上.
   1 ls -yz >> command.log 2>&1
   2 #  將錯誤選項"yz"的結果放到文件"command.log"中.
   3 #  因爲stderr被重定向到這個文件中,
   4 #+ 所有的錯誤消息也就都指向那裏了.
   5
   6 #  注意, 下邊這個例子就不會給出相同的結果.
   7 ls -yz 2>&1 >> command.log
   8 #  輸出一個錯誤消息, 但是並不寫到文件中.
   9
  10 #  如果將stdout和stderr都重定向,
  11 #+ 命令的順序會有些不同.
關閉文件描述符
n<&-  關閉輸入文件描述符n.
0<&-, <&- 關閉stdin.
n>&-  關閉輸出文件描述符n.
1>&-, >&- 關閉stdout.
子進程繼承了打開的文件描述符. 這就是爲什麼管道可以工作. 如果想阻止fd被繼承, 那麼可
以關掉它.
   1 # 只重定向stderr到一個管道.
   2
   3 exec 3>&1                              # 保存當前stdout的"值".
   4 ls -l 2>&1 >&3 3>&- | grep bad 3>&-    # 對'grep'關閉fd 3(但不關閉'ls').
   5 #              ^^^^   ^^^^
   6 exec 3>&-                              # 現在對於剩餘的腳本關閉它.
   7
   8 # Thanks, S.C.
如果想了解關於I/O重定向更多的細節參見 附錄 E.
http://www.818198.com  Page 353
SHELL十三問
16.1. 使用exec
--------------exec <filename 命令會將stdin重定向到文件中. 從這句開始, 後邊的輸入就都來自於這個文
件了, 而不是標準輸入了(通常都是鍵盤輸入). 這樣就提供了一種按行讀取文件的方法, 並且
可以使用sed 和/或 awk來對每一行進行分析.
Example 16-1 使用exec重定向標準輸入
################################Start Script#######################################
 1 #!/bin/bash
 2 # 使用'exec'重定向標準輸入.
 3
 4
 5 exec 6<&0          # 將文件描述符#6與stdin鏈接起來.
 6                    # 保存了stdin.
 7
 8 exec < data-file   # stdin被文件"data-file"所代替.
 9
10 read a1            # 讀取文件"data-file"的第一行.
11 read a2            # 讀取文件"data-file"的第二行.
12
13 echo
14 echo "Following lines read from file."
15 echo "-------------------------------"
16 echo $a1
17 echo $a2
18
19 echo; echo; echo
20
21 exec 0<&6 6<&-22 #  現在將stdin從fd #6中恢復, 因爲剛纔我們把stdin重定向到#6了,
23 #+ 然後關閉fd #6 ( 6<&- ), 好讓這個描述符繼續被其他進程所使用.
24 #
25 # <&6 6<&-    這麼做也可以.
26
27 echo -n "Enter data  "
28 read b1  # 現在"read"已經恢復正常了, 就是從stdin中讀取.
29 echo "Input read from stdin."
30 echo "----------------------"
31 echo "b1 = $b1"
32
33 echo
34
35 exit 0
################################End Script#########################################
同樣的, exec >filename 命令將會把stdout重定向到一個指定的文件中. 這樣所有的命令輸
出就都會發向那個指定的文件, 而不是stdout.
Example 16-2 使用exec來重定向stdout
################################Start Script#######################################
 1 #!/bin/bash
http://www.818198.com  Page 354
SHELL十三問
 2 # reassign-stdout.sh
 3
 4 LOGFILE=logfile.txt
 5
 6 exec 6>&1           # 將fd #6與stdout相連接.
 7                     # 保存stdout.
 8
 9 exec > $LOGFILE     # stdout就被文件"logfile.txt"所代替了.
10
11 # ----------------------------------------------------------- #
12 # 在這塊中所有命令的輸出就都發向文件 $LOGFILE.
13
14 echo -n "Logfile: "
15 date
16 echo "-------------------------------------"
17 echo
18
19 echo "Output of \"ls -al\" command"
20 echo
21 ls -al
22 echo; echo
23 echo "Output of \"df\" command"
24 echo
25 df
26
27 # ----------------------------------------------------------- #
28
29 exec 1>&6 6>&-      # 恢復stdout, 然後關閉文件描述符#6.
30
31 echo
32 echo "== stdout now restored to default == "
33 echo
34 ls -al
35 echo
36
37 exit 0
################################End Script#########################################
Example 16-3 使用exec在同一腳本中重定向stdin和stdout
################################Start Script#######################################
 1 #!/bin/bash
 2 # upperconv.sh
 3 # 將一個指定的輸入文件轉換爲大寫.
 4
 5 E_FILE_ACCESS=70
 6 E_WRONG_ARGS=71
 7
 8 if [ ! -r "$1" ]     # 判斷指定的輸入文件是否可讀?
 9 then
http://www.818198.com  Page 355
SHELL十三問
10   echo "Can't read from input file!"
11   echo "Usage: $0 input-file output-file"
12   exit $E_FILE_ACCESS
13 fi                   #  即使輸入文件($1)沒被指定
14                      #+ 也還是會以相同的錯誤退出(爲什麼?).
15
16 if [ -z "$2" ]
17 then
18   echo "Need to specify output file."
19   echo "Usage: $0 input-file output-file"
20   exit $E_WRONG_ARGS
21 fi
22
23
24 exec 4<&0
25 exec < $1            # 將會從輸入文件中讀取.
26
27 exec 7>&1
28 exec > $2            # 將寫到輸出文件中.
29                      # 假設輸出文件是可寫的(添加檢查?).
30
31 # -----------------------------------------------32     cat - | tr a-z A-Z   # 轉換爲大寫.
33 #   ^^^^^                # 從stdin中讀取.Reads from stdin.
34 #           ^^^^^^^^^^   # 寫到stdout上.
35 # 然而, stdin和stdout都被重定向了.
36 # -----------------------------------------------37
38 exec 1>&7 7>&-       # 恢復 stout.
39 exec 0<&4 4<&-       # 恢復 stdin.
40
41 # 恢復之後, 下邊這行代碼將會如期望的一樣打印到stdout上.
42 echo "File \"$1\" written to \"$2\" as uppercase conversion."
43
44 exit 0
################################End Script#########################################
I/O重定向是一種避免可怕的子shell中不可存取變量問題的方法.
Example 16-4 避免子shell
################################Start Script#######################################
 1 #!/bin/bash
 2 # avoid-subshell.sh
 3 # Matthew Walker提出的建議.
 4
 5 Lines=0
 6
 7 echo
 8
 9 cat myfile.txt | while read line;  #  (譯者注: 管道會產生子shell)
http://www.818198.com  Page 356
SHELL十三問
10                  do {
11                    echo $line
12                    (( Lines++ ));  #  增加這個變量的值
13                                    #+ 但是外部循環卻不能存取.
14                                    #  子shell問題.
15                  }
16                  done
17
18 echo "Number of lines read = $Lines"     # 0
19                                          # 錯誤!
20
21 echo "------------------------"
22
23
24 exec 3<> myfile.txt
25 while read line <&3
26 do {
27   echo "$line"
28   (( Lines++ ));                   #  增加這個變量的值
29                                    #+ 現在外部循環就可以存取了.
30                                    #  沒有子shell, 現在就沒問題了.
31 }
32 done
33 exec 3>&-34
35 echo "Number of lines read = $Lines"     # 8
36
37 echo
38
39 exit 0
40
41 # 下邊這些行是腳本的結果, 腳本是不會走到這裏的.
42
43 $ cat myfile.txt
44
45 Line 1.
46 Line 2.
47 Line 3.
48 Line 4.
49 Line 5.
50 Line 6.
51 Line 7.
52 Line 8.
################################End Script#########################################
注意事項:
[1]  一個文件描述符說白了就是文件系統爲了跟蹤這個打開的文件而分配給它的一個數字.
  也可以的將其理解爲文件指針的一個簡單版本. 與C中的文件句柄的概念相似.
[2]  使用文件描述符5可能會引起問題. 當Bash使用exec創建一個子進程的時候, 子進程
http://www.818198.com  Page 357
SHELL十三問
  會繼承fd5(參見Chet Ramey的歸檔e-mail, SUBJECT: RE: File descriptor 5 is
  held open). 最好還是不要去招惹這個特定的fd.
第17章 Here Documents
======================
here document 就是一段特殊目的的代碼塊. 他使用I/O 重定向的形式來將一個命令序列傳遞
到一個交互程序或者命令中, 比如ftp, cat, 或者ex文本編輯器.
   1 COMMAND <<InputComesFromHERE
   2 ...
   3 InputComesFromHERE
limit string 用來劃定命令序列的範圍(譯者注: 兩個相同的limit string之間就是命令序列)
. 特殊符號 << 用來表識limit string. 這個符號具有重定向文件的輸出到程序或命令的輸入
的作用. 與 interactive-program < command-file 很相象, command-file包含:
   1 command #1
   2 command #2
   3 ...
而here document 的形式看上去是如下的樣子:
   1 #!/bin/bash
   2 interactive-program <<LimitString
   3 command #1
   4 command #2
   5 ...
   6 LimitString
選擇一個名字非常詭異的limit string將會避免命令列表和limit string重名的問題.
注意,某些時候here document 用在非交互工具和命令上的時候也會有好的效果, 比如, wall.
Example 17-1 廣播: 發送消息給每個登錄上的用戶
################################Start Script#######################################
 1 #!/bin/bash
 2
 3 wall <<zzz23EndOfMessagezzz23
 4 E-mail your noontime orders for pizza to the system administrator.
 5     (Add an extra dollar for anchovy or mushroom topping.)
 6 # 額外的消息文本寫在這裏.
 7 # 注意: 'wall' 會打印註釋行.
 8 zzz23EndOfMessagezzz23
 9
10 # 可以使用更有效率的做法
11 #         wall <message-file
12 #  然而將消息模版嵌入到腳本中
13 #+ 是一種"小吃店"(快速但是比較髒)的只能使用一次的解決辦法.
14
15 exit 0
################################End Script#########################################
即使是某些不大可能的工具, 如vi也可以使用here document.
Example 17-2 仿造文件: 創建一個兩行的仿造文件
################################Start Script#######################################
 1 #!/bin/bash
 2
 3 # 用非交互的方式來使用'vi'編輯一個文件.
http://www.818198.com  Page 358
SHELL十三問
 4 # 模仿'sed'.
 5
 6 E_BADARGS=65
 7
 8 if [ -z "$1" ]
 9 then
10   echo "Usage: `basename $0` filename"
11   exit $E_BADARGS
12 fi
13
14 TARGETFILE=$1
15
16 # 在文件中插入兩行, 然後保存.
17 #--------Begin here document-----------#
18 vi $TARGETFILE <<x23LimitStringx23
19 i
20 This is line 1 of the example file.
21 This is line 2 of the example file.
22 ^[
23 ZZ
24 x23LimitStringx23
25 #----------End here document-----------#
26
27 #  注意上邊^[是一個轉義符,鍵入Ctrl+v <Esc>就行,
28 #+ 事實上它是<Esc>鍵.
29
30 #  Bram Moolenaar指出這種方法不能正常地用在'vim'上, (譯者注: Bram Moolenaar是vim作者)
31 #+ 因爲可能會有終端的相互影響問題.
32
33 exit 0
################################End Script#########################################
上邊的腳本也可以不用vi而用ex來實現. Here document 包含ex命令列表的做法足夠形成自己
的類別了, 叫ex scripts.
   1 #!/bin/bash
   2 #  把所有後綴爲".txt"文件
   3 #+ 中的"Smith"都替換成"Jones".
   4
   5 ORIGINAL=Smith
   6 REPLACEMENT=Jones
   7
   8 for word in $(fgrep -l $ORIGINAL *.txt)
   9 do
  10   # -------------------------------------  11   ex $word <<EOF
  12   :%s/$ORIGINAL/$REPLACEMENT/g
  13   :wq
  14 EOF
  15   # :%s 是"ex"的替換命令.
http://www.818198.com  Page 359
SHELL十三問
  16   # :wq 是保存並退出的意思.
  17   # -------------------------------------  18 done
與"ex scripts"相似的是cat scripts.
Example 17-3 使用cat的多行消息
################################Start Script#######################################
 1 #!/bin/bash
 2
 3 #  'echo' 對於打印單行消息是非常好的,
 4 #+  但是在打印消息塊時可能就有點問題了.
 5 #   'cat' here document可以解決這個限制.
 6
 7 cat <<End-of-message
 8 ------------------------------------- 9 This is line 1 of the message.
10 This is line 2 of the message.
11 This is line 3 of the message.
12 This is line 4 of the message.
13 This is the last line of the message.
14 -------------------------------------15 End-of-message
16
17 #  用下邊這行代替上邊的第7行
18 #+   cat > $Newfile <<End-of-message
19 #+       ^^^^^^^^^^
20 #+ 那麼就會把輸出寫到文件$Newfile中, 而不是stdout.
21
22 exit 0
23
24
25 #--------------------------------------------26 # 下邊的代碼不會運行, 因爲上邊的"exit 0".
27
28 # S.C. 指出下邊代碼也可以運行.
29 echo "-------------------------------------30 This is line 1 of the message.
31 This is line 2 of the message.
32 This is line 3 of the message.
33 This is line 4 of the message.
34 This is the last line of the message.
35 -------------------------------------"
36 # 然而, 文本可能不包含雙引號, 除非它們被轉義.
################################End Script#########################################
- 選項用來標記here document的limit string (<<-LimitString), 可以抑制輸出時前邊的tab
(不是空格). 這可以增加一個腳本的可讀性.
Example 17-4 帶有抑制tab功能的多行消息
################################Start Script#######################################
 1 #!/bin/bash
http://www.818198.com  Page 360
SHELL十三問
 2 # 與之前的例子相同, 但是...
 3
 4 #  - 選項對於here docutment來說,<<- 5 #+ 可以抑制文檔體前邊的tab,
 6 #+ 而*不*是空格 *not* spaces.
 7
 8 cat <<-ENDOFMESSAGE
 9  This is line 1 of the message.
10  This is line 2 of the message.
11  This is line 3 of the message.
12  This is line 4 of the message.
13  This is the last line of the message.
14 ENDOFMESSAGE
15 # 腳本在輸出的時候左邊將被刷掉.
16 # 就是說每行前邊的tab將不會顯示.
17
18 # 上邊5行"消息"的前邊都是tab, 不是空格.
19 # 空格是不受<<-影響的.
20
21 # 注意, 這個選項對於*嵌在*中間的tab沒作用.
22
23 exit 0
################################End Script#########################################
here document 支持參數和命令替換. 所以也可以給here document的消息體傳遞不同的參數,
這樣相應的也會修改輸出.
Example 17-5 使用參數替換的here document
################################Start Script#######################################
 1 #!/bin/bash
 2 # 一個使用'cat'命令的here document, 使用了參數替換
 3
 4 # 不傳命令行參數給它,   ./scriptname
 5 # 傳一個命令行參數給它,   ./scriptname Mortimer
 6 # 傳一個2個單詞(用引號括起來)的命令行參數給它,
 7 #                           ./scriptname "Mortimer Jones"
 8
 9 CMDLINEPARAM=1     #  所期望的最少的命令行參數的個數.
10
11 if [ $# -ge $CMDLINEPARAM ]
12 then
13   NAME=$1          #  如果命令行參數超過1個,
14                    #+ 那麼就只取第一個參數.
15 else
16   NAME="John Doe"  #  默認情況下, 如果沒有命令行參數的話.
17 fi 
18
19 RESPONDENT="the author of this fine script" 
20  
21
http://www.818198.com  Page 361
SHELL十三問
22 cat <<Endofmessage
23
24 Hello, there, $NAME.
25 Greetings to you, $NAME, from $RESPONDENT.
26
27 # This comment shows up in the output (why?).
28
29 Endofmessage
30
31 # 注意上邊的空行也打印到輸出,
32 # 而上邊那行"註釋"當然也會打印到輸出.
33 # (譯者注: 這就是爲什麼不翻譯那行註釋的原因, 儘量保持原代碼的原樣)
34 exit 0
################################End Script#########################################
這是一個包含參數替換的here document的有用的腳本.
Example 17-6 上傳一個文件對到"Sunsite"的incoming目錄
################################Start Script#######################################
 1 #!/bin/bash
 2 # upload.sh
 3
 4 #  上傳文件對(Filename.lsm, Filename.tar.gz)
 5 #+ 到Sunsite/UNC (ibiblio.org)的incoming目錄.
 6 #  Filename.tar.gz是自身的tar包.
 7 #  Filename.lsm是描述文件.
 8 #  Sunsite需要"lsm"文件, 否則就拒絕貢獻.
 9
10
11 E_ARGERROR=65
12
13 if [ -z "$1" ]
14 then
15   echo "Usage: `basename $0` Filename-to-upload"
16   exit $E_ARGERROR
17 fi 
18
19
20 Filename=`basename $1`           # 從文件名中去掉目錄字符串.
21
22 Server="ibiblio.org"
23 Directory="/incoming/Linux"
24 #  在這裏也不一定非得將上邊的參數寫死在這個腳本中,
25 #+ 可以使用命令行參數的方法來替換.
26
27 Password="your.e-mail.address"   # 可以修改成相匹配的密碼.
28
29 ftp -n $Server <<End-Of-Session
30 # -n 選項禁用自動登錄.
31
http://www.818198.com  Page 362
SHELL十三問
32 user anonymous "$Password"
33 binary
34 bell                             # 在每個文件傳輸後, 響鈴.
35 cd $Directory
36 put "$Filename.lsm"
37 put "$Filename.tar.gz"
38 bye
39 End-Of-Session
40
41 exit 0
################################End Script#########################################
在here document的開頭引用或轉義"limit string"會使得here document的消息體中的參數替
換被禁用.
Example 17-7 關閉參數替換
################################Start Script#######################################
 1 #!/bin/bash
 2 #  一個使用'cat'的here document, 但是禁用了參數替換.
 3
 4 NAME="John Doe"
 5 RESPONDENT="the author of this fine script" 
 6
 7 cat <<'Endofmessage'
 8
 9 Hello, there, $NAME.
10 Greetings to you, $NAME, from $RESPONDENT.
11
12 Endofmessage
13
14 #  當"limit string"被引用或轉義那麼就禁用了參數替換.
15 #  下邊的兩種方式具有相同的效果.
16 #  cat <<"Endofmessage"
17 #  cat <<\Endofmessage
18
19 exit 0
################################End Script#########################################
禁用了參數替換後, 將允許輸出文本本身(譯者注: 就是未轉義的原文). 產生腳本甚至是程序
代碼就是這種用法的用途之一.
Example 17-8 一個產生另外一個腳本的腳本
################################Start Script#######################################
 1 #!/bin/bash
 2 # generate-script.sh
 3 # 基於Albert Reiner的一個主意.
 4
 5 OUTFILE=generated.sh         # 所產生文件的名字.
 6
 7
 8 # ----------------------------------------------------------- 9 # 'Here document包含了需要產生的腳本的代碼.
http://www.818198.com  Page 363
SHELL十三問
10 (
11 cat <<'EOF'
12 #!/bin/bash
13
14 echo "This is a generated shell script."
15 #  Note that since we are inside a subshell,
16 #+ we can't access variables in the "outside" script.
17
18 echo "Generated file will be named: $OUTFILE"
19 #  Above line will not work as normally expected
20 #+ because parameter expansion has been disabled.
21 #  Instead, the result is literal output.
22
23 a=7
24 b=3
25
26 let "c = $a * $b"
27 echo "c = $c"
28
29 exit 0
30 EOF
31 ) > $OUTFILE
32 # -----------------------------------------------------------33
34 #  將'limit string'引用起來將會阻止上邊
35 #+ here document的消息體中的變量擴展.
36 #  這會使得輸出文件中的內容保持here document消息體中的原文.
37
38 if [ -f "$OUTFILE" ]
39 then
40   chmod 755 $OUTFILE
41   # 讓所產生的文件具有可執行權限.
42 else
43   echo "Problem in creating file: \"$OUTFILE\""
44 fi
45
46 #  這個方法也用來產生
47 #+ C程序代碼, Perl程序代碼, Python程序代碼, makefile,
48 #+ 和其他的一些類似的代碼.
49 #  (譯者注: 中間一段沒譯的註釋將會被here document打印出來)
50 exit 0
################################End Script#########################################
也可以將here document的輸出保存到變量中.
   1 variable=$(cat <<SETVAR
   2 This variable
   3 runs over multiple lines.
   4 SETVAR)
   5
http://www.818198.com  Page 364
SHELL十三問
   6 echo "$variable"
同一腳本中的函數也可以接受here document的輸出作爲自身的參數.
Example 17-9 Here documents與函數
################################Start Script#######################################
 1 #!/bin/bash
 2 # here-function.sh
 3
 4 GetPersonalData ()
 5 {
 6   read firstname
 7   read lastname
 8   read address
 9   read city
10   read state
11   read zipcode
12 } # 這個函數無疑的看起來就一個交互函數, 但是...
13
14
15 # 給上邊的函數提供輸入.
16 GetPersonalData <<RECORD001
17 Bozo
18 Bozeman
19 2726 Nondescript Dr.
20 Baltimore
21 MD
22 21226
23 RECORD001
24
25
26 echo
27 echo "$firstname $lastname"
28 echo "$address"
29 echo "$city, $state $zipcode"
30 echo
31
32 exit 0
################################End Script#########################################
也可以這麼使用: 做一個假命令來從一個here document中接收輸出. 這麼做事實上就是創建了
一個"匿名"的here document.
Example 17-10 "匿名" here Document
################################Start Script#######################################
1 #!/bin/bash
2
3 : <<TESTVARIABLES
4 ${HOSTNAME?}${USER?}${MAIL?}  # 如果其中一個變量沒被設置, 那麼就打印錯誤信息.
5 TESTVARIABLES
6
7 exit 0
http://www.818198.com  Page 365
SHELL十三問
################################End Script#########################################
注意: 上邊所示技術的一種變化可以用來"註釋"掉代碼塊.
Example 17-11 註釋掉一段代碼塊
################################Start Script#######################################
 1 #!/bin/bash
 2 # commentblock.sh
 3
 4 : <<COMMENTBLOCK
 5 echo "This line will not echo."
 6 This is a comment line missing the "#" prefix.
 7 This is another comment line missing the "#" prefix.
 8
 9 &*@!!++=
10 The above line will cause no error message,
11 because the Bash interpreter will ignore it.
12 COMMENTBLOCK
13
14 echo "Exit value of above \"COMMENTBLOCK\" is $?."   # 0
15 # 這裏將不會顯示任何錯誤.
16
17
18 #  上邊的這種技術當然也可以用來註釋掉
19 #+ 一段正在使用的代碼, 如果你有某些特定調試要求的話.
20 #  這將比對每行都敲入"#"來得方便的多,
21 #+ 而且如果你想恢復的話, 還得將添加上的"#"刪除掉.
22
23 : <<DEBUGXXX
24 for file in *
25 do
26  cat "$file"
27 done
28 DEBUGXXX
29
30 exit 0
################################End Script#########################################
注意: 關於這種小技巧的另一個應用就是能夠產生自文檔化(self-documenting)的腳本.
Example 17-12 一個自文檔化(self-documenting)的腳本
################################Start Script#######################################
 1 #!/bin/bash
 2 # self-document.sh: 自文檔化(self-documenting)的腳本
 3 # Modification of "colm.sh".
 4
 5 DOC_REQUEST=70
 6
 7 if [ "$1" = "-h"  -o "$1" = "--help" ]     # 請求幫助.
 8 then
 9   echo; echo "Usage: $0 [directory-name]"; echo
10   sed --silent -e '/DOCUMENTATIONXX$/,/^DOCUMENTATIONXX$/p' "$0" |
http://www.818198.com  Page 366
SHELL十三問
11   sed -e '/DOCUMENTATIONXX$/d'; exit $DOC_REQUEST; fi
12
13
14 : <<DOCUMENTATIONXX
15 List the statistics of a specified directory in tabular format.
16 ---------------------------------------------------------------17 The command line parameter gives the directory to be listed.
18 If no directory specified or directory specified cannot be read,
19 then list the current working directory.
20
21 DOCUMENTATIONXX
22
23 if [ -z "$1" -o ! -r "$1" ]
24 then
25   directory=.
26 else
27   directory="$1"
28 fi 
29
30 echo "Listing of "$directory":"; echo
31 (printf "PERMISSIONS LINKS OWNER GROUP SIZE MONTH DAY HH:MM PROG-NAME\n" \
32 ; ls -l "$directory" | sed 1d) | column -t
33
34 exit 0
################################End Script#########################################
使用cat 腳本 也能夠完成相同的目的.
   1 DOC_REQUEST=70
   2
   3 if [ "$1" = "-h"  -o "$1" = "--help" ]     # 請求幫助.
   4 then                                       # 使用"cat 腳本" . . .
   5   cat <<DOCUMENTATIONXX
   6 List the statistics of a specified directory in tabular format.
   7 ---------------------------------------------------------------   8 The command line parameter gives the directory to be listed.
   9 If no directory specified or directory specified cannot be read,
  10 then list the current working directory.
  11
  12 DOCUMENTATIONXX
  13 exit $DOC_REQUEST
  14 fi
參見 Example A-27 可以瞭解更多關於自文檔化腳本的好例子.
注意: Here document創建臨時文件, 但是這些文件將在打開後被刪除, 並且不能夠被任何其
 他進程所存取.
  bash$ bash -c 'lsof -a -p $$ -d0' << EOF
  > EOF
  lsof    1213 bozo    0r   REG    3,5    0 30386 /tmp/t1213-0-sh (deleted)
注意: 某些工具是不能工作在here document中的.
警告: 結束的limit string, 就是here document最後一行的limit string, 必須開始於第一
http://www.818198.com  Page 367
SHELL十三問
 個字符位置. 它的前面不能夠有任何前置的空白. 而在這個limit string後邊的空白也會
 引起異常問題. 空白將會阻止limit string的識別.(譯者注: 下邊這個腳本由於結束
 limit string的問題, 造成腳本無法結束, 所有內容全部被打印出來, 所以註釋就不譯了,
  保持例子腳本的原樣.)
   1 #!/bin/bash
   2
   3 echo "----------------------------------------------------------------------"
   4
   5 cat <<LimitString
   6 echo "This is line 1 of the message inside the here document."
   7 echo "This is line 2 of the message inside the here document."
   8 echo "This is the final line of the message inside the here document."
   9      LimitString
  10 #^^^^Indented limit string. Error! This script will not behave as expected.
  11
  12 echo "----------------------------------------------------------------------"
  13
  14 #  These comments are outside the 'here document',
  15 #+ and should not echo.
  16
  17 echo "Outside the here document."
  18
  19 exit 0
  20
  21 echo "This line had better not echo."  # Follows an 'exit' command.
對於那些使用"here document"得非常複雜的任務, 最好考慮使用expect腳本語言, 這種語言
就是爲了達到向交互程序添加輸入的目的而量身定做的.
17.1. Here Strings
------------------here string 可以被認爲是here document的一種定製形式. 除了COMMAND <<<$WORD 就什麼都
沒有了, $WORD將被擴展並且被送入COMMAND的stdin中.
Example 17-13 在一個文件的開頭添加文本
################################Start Script#######################################
 1 #!/bin/bash
 2 # prepend.sh: 在文件的開頭添加文本.
 3 #
 4 #  Kenny Stauffer所捐助的腳本例子,
 5 #+ 被本文作者作了少量的修改.
 6
 7
 8 E_NOSUCHFILE=65
 9
10 read -p "File: " file   # 'read'命令的 -p 參數顯示提示符.
11 if [ ! -e "$file" ]
12 then   # 如果沒有這個文件那就進來.
13   echo "File $file not found."
14   exit $E_NOSUCHFILE
15 fi
http://www.818198.com  Page 368
SHELL十三問
16
17 read -p "Title: " title
18 cat - $file <<<$title > $file.new
19
20 echo "Modified file is $file.new"
21
22 exit 0
23
24 # 下邊是'man bash'中的一段:
25 # Here Strings
26 #  here document的一種變形,形式如下:
27 #
28 #   <<<word
29 #
30 #   word被擴展並且提供到command的標準輸入中.
################################End Script#########################################
練習: 找出here string的其他用法.
第18章 休息時間
================
這個神奇的暫停可以給讀者一個休息的機會, 可能讀者到了這裏也會會心一笑吧.
Linux同志們, 向你們致敬! 你正在閱讀的這些東西, 將會給你們帶來好運. 把這份文檔發給你
的10個朋友. 在拷貝這份文檔之前, 在信的結尾寫上一個100行的Bash腳本發送給列表上的第一
個人. 然後在信的底部刪除它們的名字並添加你自己的名字.
不要打斷這個鏈條! 並且在48小時之內完成它.
Brooklyn的Wilfred?P.沒有成功的發送他的10個拷貝, 當他第2天早上醒來發現他的工作變成了
"COBOL 程序員". Newport?News的Howard?L.在一個月內才發出了他的10個拷貝, 這個時間足夠
建立一個100個節點的Beowulf cluster來玩Tuxracer了. Chicago的Amelia?V.對這封信付之一
笑並且打斷了這個鏈條, 不久之後, 她的終端爆炸了, 她現在花了好多天時間爲MS Windows寫
文檔.
千萬不要打斷這個鏈條! 今天就把10個拷貝發出去!
[]    
   
高級Bash腳本編程指南(五)(上)
文章整理: 文章來源: 網絡
高級Bash腳本編程指南(五)
 
 
 
第四部分 高級
++++++++++++++++
到了這兒,我們將要準備深入腳本編程中一些難的,不尋常的話題.隨着話題的展開,我們會
以多種方法和檢測邊界條件的方式來“打開信封”,看個明白.(當我們涉足未知領域時會發
生什麼?).
目錄
19. Regular Expressions正則表達式
20. 子shell(Subshells)
21. 受限shell(Restricted Shells)
22. 進程替換
http://www.818198.com  Page 369
SHELL十三問
23. 函數
24. 別名(Aliases)
25. 列表結構
26. 數組
27. /dev和/proc
28. 關於Zeros和Nulls
29. 調試
30. 選項
31. 檢查遺漏(Gotchas)
32. 腳本編程風格
33. 雜項
34. Bash,版本2和3
第19章 正則表達式
==================
爲了充分發揮shell編程的威力, 你需要精通正則表達式. 一些命令和軟件包普遍在腳本編程中
使用正則表達式,例如grep, expr, sed和awk.
19.1 一個簡要的正則表達式介紹
--------------------------------一個正式表達式是一個字符串.字符串裏的字符被稱爲元字符,它們可能表示了比它們字面上看
起來的意思更豐富的含義.例如,一個引用符號可能表示引用一個人演講中的話,或者表示下
面將要講到的引申表示的意思.正則表達式是一個字符或/和元字符組合成的字符集,它們匹配
(或指定)一個模式.
一個正則表達式包含下面一個或多個項:
 1. 一個字符集.
  這裏的字符集裏的字符表示的就是它們字面上的意思.正則表達式最簡單的情況就是僅
  僅由字符集組成,而沒有其他的元字符.
 2. 錨.
  一個錨指明瞭正則表達式在一行文本中要匹配的位置,例如^和$就是錨.
 3. 修飾符
  它們用於展開或縮小(即是修改了)正則表達式匹配文本行的範圍.修飾符包括了星號.
  括號和反斜槓符號.
正則表達是的主要作用是用來文本搜索和字串操作.一個正則表達式匹配一個字符或是一串字
符--完整的一串字符或是另外一個字符串的子串.
星號  -- * -- 匹配前一個字符的任意多次(包括零次).
   "1133*"匹配11 + 一個或更多的3 + 可能的其他字符: 113, 1133, 111312, 等等.
點   -- . -- 匹配除了新行符之外的任意一個字符. [1]
   "13." 匹配13 + 至少一個任意字符(包括空格): 1133, 11333, 但不匹配 13
   (因爲少了附加的至少一個任意字符).
脫字符  -- ^ -- 匹配一行的開頭,但依賴於上下文環境,可能在正則表達式中表示否定
   一個字符集的意思.
美元符  -- $ -- 在正則表達式中匹配行尾.
   "^$" 匹配空行.
方括號  -- [...] -- 在正則表達式中表示匹配括號中的一個字符.
   "[xyz]" 匹配字符x, y, 或z.
   "[c-n]" 匹配從字符c到n之間的任意一個字符.
   "[B-Pk-y]" 匹配從B到P 或從k到y的任意一個字符.
   "[a-z0-9]" 匹配任意小寫字母或數字.
   "[^b-d]" 匹配除了從b到d範圍內所有的字符. 這是正則表達式中反轉意思或取否
http://www.818198.com  Page 370
SHELL十三問
   的一 個例子.(就好像在別的情形中!字符所扮演的角色).
   多個方括號字符集組合使用可以匹配一般的單詞和數字模式."[Yy][Ee][Ss]" 匹
   配yes, Yes, YES, yEs, 等等.
   "[0-9][0-9][0-9]-[0-9][0-9]-[0-9][0-9][0-9][0-9]"匹配社會安全碼
   (Social Security number).
反斜槓字符 -- \ -- 轉義(escapes) 一個特殊的字符,使這個字符表示原來字面上的意思.
   "\$"表示了原來的字面意思"$",而不是在正則表達式中表達的匹配行尾的意思.
   同樣,"\\"也被解釋成了字面上的意思"\".
轉義(escape)"尖角號" -- \<...\> -- 用於表示單詞的邊界.
      尖角號必須被轉義,因爲不這樣做的話它們就表示單純的字面意思
      而已.
      "\<the\>" 匹配單詞"the",但不匹配"them", "there", "other",
      等等.
   bash$ cat textfile
   This is line 1, of which there is only one instance.
   This is the only instance of line 2.
   This is line 3, another line.
   This is line 4.
  
  
   bash$ grep 'the' textfile
   This is line 1, of which there is only one instance.
   This is the only instance of line 2.
   This is line 3, another line.
  
  
   bash$ grep '\<the\>' textfile
   This is the only instance of line 2.
 確定正則表達式能否工作的唯一辦法是測試它.
    1 TEST FILE: tstfile                          # 不匹配.
    2                                             # 不匹配.
    3 Run   grep "1133*"  on this file.           # 匹配.
    4                                             # 不匹配.
    5                                             # 不匹配.
    6 This line contains the number 113.          # 匹配.
    7 This line contains the number 13.           # 不匹配.
    8 This line contains the number 133.          # 不匹配.
    9 This line contains the number 1133.         # 匹配.
   10 This line contains the number 113312.       # 匹配.
   11 This line contains the number 1112.         # 不匹配.
   12 This line contains the number 113312312.    # 匹配.
   13 This line contains no numbers at all.       # 不匹配.
  bash$ grep "1133*" tstfile
  Run   grep "1133*"  on this file.           # 匹配.
  This line contains the number 113.          # 匹配.
  This line contains the number 1133.         # 匹配.
  This line contains the number 113312.       # 匹配.
  This line contains the number 113312312.    # 匹配.
http://www.818198.com  Page 371
SHELL十三問
擴展的正則表達式. 增加了一些元字符到上面提到的基本的元字符集合裏. 它們在egrep,
     awk,和Perl中使用.
問號  -- ? -- 匹配零或一個前面的字符. 它一般用於匹配單個字符.
加號  -- + -- 匹配一個或多個前面的字符.它的作用和*很相似,但唯一的區別是它不
   匹配零個字符的情況.
     1 # GNU 版本的 sed 和 awk 可以使用"+",
     2 # 但它應該轉義一下.
     3
     4 echo a111b | sed -ne '/a1\+b/p'
     5 echo a111b | grep 'a1\+b'
     6 echo a111b | gawk '/a1+b/'
     7 # 上面三句都是等價的效果.
     8
     9 # 多謝, S.C.
轉義"大括號" -- \{ \} -- 指示前面正則表達式匹配的次數.
    要轉義是因爲不轉義的話大括號只是表示他們字面上的意思.這個用法只是
    技巧上的而不是基本正則表達式的內容.
    "[0-9]\{5\}" 精確匹配5個數字 (從 0 到 9的數字).
    注意: 大括號不能在“經典”(不是POSIX兼容)的正則表達式版本的awk中
      使用. 然而, gawk 有一個選項--re-interval來允許使用大括號
      (不必轉義).
       bash$ echo 2222 | gawk --re-interval '/2{3}/'
       2222
      Perl和一些egrep版本不要求轉義大括號.
圓括號  -- ( ) -- 括起一組正則表達式. 它和下面要講的"|"操作符或在用expr進行子字
   符串提取(substring extraction)一起使用很有用.
豎線  -- | -- "或"正則操作符用於匹配一組可選的字符.
    bash$ egrep 're(a|e)d' misc.txt
    People who read seem to be better informed than those who do not.
    The clarinet produces sound by the vibration of its reed.
   注意: 一些sed, ed, 和ex的版本像GNU的軟件版本一樣支持上面描述的擴展正
     則表達式的版本.
POSIX字符類. [:class:]
    這是另外一個可選的用於指定匹配字符範圍的方法.
[:alnum:] 匹配字母和數字.等同於A-Za-z0-9.
[:alpha:] 匹配字母. 等同於A-Za-z.
[:blank:] 匹配一個空格或是一個製表符(tab).
[:cntrl:] 匹配控制字符.
[:digit:] 匹配(十進制)數字. 等同於0-9.
[:graph:] (可打印的圖形字符). 匹配 ASCII 碼值的33 - 126之間的字符. 這和下面提到的
   [:print:]一樣,但是不包括空格字符.
[:lower:] 匹配小寫字母. 等同於a-z.
[:print:] (可打印字符). 匹配 ASCII碼值 32 - 126之間的字符. 這和上面提到的一樣
   [:graph:],但是增多一個空格字符.
[:space:] 匹配空白字符 (空格符和水平製表符).
[:upper:] 匹配大寫字母. 等同於A-Z.
[:xdigit:] 匹配十六進制數字. 等同於0-9A-Fa-f.
注意: POSIX字符類一般都要求用引號或是雙方括號double brackets ([[ ]])引起來.
http://www.818198.com  Page 372
SHELL十三問
   bash$ grep [[:digit:]] test.file
   abc=723
  這些字符類在一個受限的範圍內甚至可能用在能用在通配(globbing)中.
   bash$ ls -l ?[[:digit:]][[:digit:]]?
   -rw-rw-r--    1 bozo  bozo         0 Aug 21 14:47 a33b
  爲了理解POSIX字符類在腳本中的使用,請參考例子 12-18 和 例子 12-19.
Sed, awk, 和Perl在腳本中被用作過濾器, "過濾"或轉換文件/IO流的時候以正則表達式作爲參
數.參考例子 A-12和例子 A-17 來理解這種用法.
在正則表達式這個複雜主題的標準參考是Friedl的Mastering Regular Expressions.由
Dougherty和Robbins寫的 Sed & Awk也給出了一個清晰的正則表達式論述. 查看參考書目找
到這個主題更多的信息.
注意事項:
[1]  因爲sed, awk, 和 grep 通常處理單行,而不能匹配一個新行符. 在要處理多行的一
  個輸入時,可以使用點操作符,它可以匹配新行符.
     1 #!/bin/bash
     2
     3 sed -e 'N;s/.*/[&]/' << EOF   # Here Document
     4 line1
     5 line2
     6 EOF
     7 # 輸出:
     8 # [line1
     9 # line2]
    10
    11
    12
    13 echo
    14
    15 awk '{ $0=$1 "\n" $2; if (/line.1/) {print}}' << EOF
    16 line 1
    17 line 2
    18 EOF
    19 # 輸出:
    20 # line
    21 # 1
    22
    23
    24 # 多謝, S.C.
    25
    26 exit 0
19.1 通配
------------Bash本身沒有正則表達式的功能.在腳本里,使用正則表達式的是命令和軟件包 -- 例如sed和
awk -- 它們可以解釋正則表達式.
Bash所做的是展開文件名擴展 [1] -- 這就是所謂的通配(globbing) -- 但它不是使用標準的
正則表達式. 而是使用通配符. 通配解釋標準的通配符:*和?, 方括號括起來的字符,還有其他
的一些特殊的字符(比如說^用來表示取反匹配).然而通配機制的通配符有很大的侷限性. 包含
有*號的字符串將不會匹配以點開頭的文件,例如.bashrc. [2] 另外,通配機制的? 字符和正則
http://www.818198.com  Page 373
SHELL十三問
表達式中表示的意思不一樣.
 bash$ ls -l
 total 2
 -rw-rw-r--    1 bozo  bozo         0 Aug  6 18:42 a.1
 -rw-rw-r--    1 bozo  bozo         0 Aug  6 18:42 b.1
 -rw-rw-r--    1 bozo  bozo         0 Aug  6 18:42 c.1
 -rw-rw-r--    1 bozo  bozo       466 Aug  6 17:48 t2.sh
 -rw-rw-r--    1 bozo  bozo       758 Jul 30 09:02 test1.txt
 
 bash$ ls -l t?.sh
 -rw-rw-r--    1 bozo  bozo       466 Aug  6 17:48 t2.sh
 
 bash$ ls -l [ab]*
 -rw-rw-r--    1 bozo  bozo         0 Aug  6 18:42 a.1
 -rw-rw-r--    1 bozo  bozo         0 Aug  6 18:42 b.1
 
 bash$ ls -l [a-c]*
 -rw-rw-r--    1 bozo  bozo         0 Aug  6 18:42 a.1
 -rw-rw-r--    1 bozo  bozo         0 Aug  6 18:42 b.1
 -rw-rw-r--    1 bozo  bozo         0 Aug  6 18:42 c.1
 
 bash$ ls -l [^ab]*
 -rw-rw-r--    1 bozo  bozo         0 Aug  6 18:42 c.1
 -rw-rw-r--    1 bozo  bozo       466 Aug  6 17:48 t2.sh
 -rw-rw-r--    1 bozo  bozo       758 Jul 30 09:02 test1.txt
 
 bash$ ls -l {b*,c*,*est*}
 -rw-rw-r--    1 bozo  bozo         0 Aug  6 18:42 b.1
 -rw-rw-r--    1 bozo  bozo         0 Aug  6 18:42 c.1
 -rw-rw-r--    1 bozo  bozo       758 Jul 30 09:02 test1.txt
Bash會對命令行中沒有引號引起來的字符嘗試文件名擴展. echo 命令可以印證這一點.
 bash$ echo *
 a.1 b.1 c.1 t2.sh test1.txt
 
 bash$ echo t*
 t2.sh test1.txt
注意: 可以改變Bash對通配字符進行解釋的行爲. set -f 命令可以禁止通配機制, 並且
  shopt的選項nocaseglob和nullglob 能改變通配的行爲.
參考例子 10-4.
注意事項:
[1]  文件名擴展意思是擴展包含有特殊字符的文件名模式和模板. 例如,example.???可能
  擴展成example.001和/或example.txt.
[2]  文件名擴展能匹配點開頭的文件,但僅在模式字串明確地包含字面意思的點(.)時才
  擴展.
     1 ~/[.]bashrc    #  不會擴展成 ~/.bashrc
     2 ~/?bashrc      #  也不會擴展.
     3                #  通配機制中的通配符和元字符不會擴展點文件
     4                #
http://www.818198.com  Page 374
SHELL十三問
     5
     6 ~/.[b]ashrc    #  會擴展成 ~/.bashrc
     7 ~/.ba?hrc      #  也會.
     8 ~/.bashr*      #  也會.
     9
    10 # 可以使用"dotglob"選項把這個特性禁用.
    11
    12 # 多謝, S.C.
第20章 子shell(Subshells)
==========================
運行一個shell腳本時會啓動另一個命令解釋器. 就好像你的命令是在命令行提示下被解釋的一
樣, 類似於批處理文件裏的一系列命令.每個shell腳本有效地運行在父shell(parent shell)的
一個子進程裏.這個父shell是指在一個控制終端或在一個xterm窗口中給你命令指示符的進程.
shell腳本也能啓動他自已的子進程. 這些子shell(即子進程)使腳本因爲效率而同時進行多個
子任務執行時能做串行處理.
一般來說,腳本里的一個外部命令(external command)能生成(forks)出一個子進程,然而
Bash內建(builtin)的命令卻不這樣做,因此,內建命令比起外部的等價命令執行起來更快.
圓括號裏的命令列表
( 命令1; 命令2; 命令3; ... )
    嵌在圓括號裏的一列命令在一個子shell裏運行.
注意: 在子shell裏的變量不能被這段子shell代碼塊之外外面的腳本訪問.這些變量是不能被
  產生這個子shell的父進程(parent process)存取的,實際上它們是局部變量
  (local variables).
Example 20-1 子shell中的變量作用域
################################Start Script#######################################
 1 #!/bin/bash
 2 # subshell.sh
 3
 4 echo
 5
 6 echo "Subshell level OUTSIDE subshell = $BASH_SUBSHELL"
 7 # Bash, 版本 3, 增加了新的              $BASH_SUBSHELL 變量.
 8 echo
 9
10 outer_variable=Outer
11
12 (
13 echo "Subshell level INSIDE subshell = $BASH_SUBSHELL"
14 inner_variable=Inner
15
16 echo "From subshell, \"inner_variable\" = $inner_variable"
17 echo "From subshell, \"outer\" = $outer_variable"
18 )
19
20 echo
21 echo "Subshell level OUTSIDE subshell = $BASH_SUBSHELL"
22 echo
23
http://www.818198.com  Page 375
SHELL十三問
24 if [ -z "$inner_variable" ]
25 then
26   echo "inner_variable undefined in main body of shell"
27 else
28   echo "inner_variable defined in main body of shell"
29 fi
30
31 echo "From main body of shell, \"inner_variable\" = $inner_variable"
32 #  $inner_variable 會以沒有初始化的變量來打印
33 #+ 因爲變量是在子shell裏定義的"局部變量".
34 #  這個有辦法補救的嗎?
35
36 echo
37
38 exit 0
################################End Script#########################################
參考例子 31-2.
+
在子shell中的目錄更改不會影響到父shell.
Example 20-2 列出用戶的配置文件
################################Start Script#######################################
 1 #!/bin/bash
 2 # allprofs.sh: 打印所有用戶的配置文件
 3
 4 # 由 Heiner Steven編寫, 並由本書作者修改.
 5
 6 FILE=.bashrc  #  在一般的腳本里,包含用戶配置的文件是".profile".
 7               #
 8
 9 for home in `awk -F: '{print $6}' /etc/passwd`
10 do
11   [ -d "$home" ] || continue    # 如果沒有家目錄,跳過此次循環.
12   [ -r "$home" ] || continue    # 如果目錄沒有讀權限,跳過此次循環.
13   (cd $home; [ -e $FILE ] && less $FILE)
14 done
15
16 #  當腳本終止時,不必用'cd'命令返回原來的目錄,
17 #+ 因爲'cd $home'是在子shell中發生的,不影響父shell.
18
19 exit 0
################################End Script#########################################
子shell可用於爲一組命令設定臨時的環境變量.
   1 COMMAND1
   2 COMMAND2
   3 COMMAND3
   4 (
   5   IFS=:
   6   PATH=/bin
http://www.818198.com  Page 376
SHELL十三問
   7   unset TERMINFO
   8   set -C
   9   shift 5
  10   COMMAND4
  11   COMMAND5
  12   exit 3 # 只是從子shell退出.
  13 )
  14 # 父shell不受影響,變量值沒有更改.
  15 COMMAND6
  16 COMMAND7
它的一個應用是測試是否一個變量被定義了.
   1 if (set -u; : $variable) 2> /dev/null
   2 then
   3   echo "Variable is set."
   4 fi     #  變量已經在當前腳本中被設置,
   5        #+ 或是Bash的一個內部變量,
   6        #+ 或是可見環境變量(指已經被導出的環境變量).
   7
   8 # 也可以寫成            [[ ${variable-x} != x || ${variable-y} != y ]]
   9 # 或                    [[ ${variable-x} != x$variable ]]
  10 # 或                    [[ ${variable+x} = x ]]
  11 # 或                    [[ ${variable-x} != x ]]
另一個應用是檢查一個加鎖的文件:
   1 if (set -C; : > lock_file) 2> /dev/null
   2 then
   3   :   # lock_file 不存在,還沒有用戶運行這個腳本
   4 else
   5   echo "Another user is already running that script."
   6 exit 65
   7 fi
   8
   9 #  由St�phane Chazelas編程
  10 #+ 由Paulo Marcel Coelho Aragao修改.
進程在不同的子shell中可以串行地執行.這樣就允許把一個複雜的任務分成幾個小的子問題來
同時地處理.
Example 20-3 在子shell裏進行串行處理
################################Start Script#######################################
 1  (cat list1 list2 list3 | sort | uniq > list123) &
 2  (cat list4 list5 list6 | sort | uniq > list456) &
 3  #列表的合併和排序同時進.
 4  #放到後臺運行可以確保能夠串行執行.
 5  #
 6  #和下面的有相同的作用:
 7  #   cat list1 list2 list3 | sort | uniq > list123 &
 8  #   cat list4 list5 list6 | sort | uniq > list456 &
 9 
10  wait   #在所有的子shell執行完成前不再執行後面的命令.
11 
http://www.818198.com  Page 377
SHELL十三問
12  diff list123 list456
################################End Script#########################################
用"|"管道操作把I/O流重定向到子shell,例如ls -al | (command).
注意:  在一個花括號內的代碼塊不會運行一個子shell.
  { command1; command2; command3; ... }
第21章 受限shell(Restricted Shells)
====================================
在受限shell中禁用的命令
    在受限shell中運行的腳本或腳本的個代碼斷會禁用一些正常shell中可以執行的命令.這是
 限制腳本用戶的權限和最小化運行腳本導致的破壞的安全措施.
    使用cd 命令更改工作目錄.
    更改環境變量$PATH, $SHELL, $BASH_ENV,或$ENV 的值.
    讀或更改shell環境選項變量$SHELLOPTS的值.
    輸出重定向.
    調用的命令路徑中包括有一個或更多個/字符.
    調用exec來把當前的受限shell替換成另外一個不同的進程.
    腳本中許多其他無意中能破壞或搗亂的命令.
    在腳本中企圖脫離受限shell模式的操作.
Example 21-1 在受限的情況下運行腳本
################################Start Script#######################################
 1 #!/bin/bash
 2
 3 #  腳本開頭以"#!/bin/bash -r"來調用
 4 #+ 會使整個腳本在受限模式下運行.
 5
 6 echo
 7
 8 echo "Changing directory."
 9 cd /usr/local
10 echo "Now in `pwd`"
11 echo "Coming back home."
12 cd
13 echo "Now in `pwd`"
14 echo
15
16 # 不受限的模式下,所有操作都能正常成功.
17
18 set -r
19 # set --restricted    也能起相同的作用.
20 echo "==> Now in restricted mode. <=="
21
22 echo
23 echo
24
25 echo "Attempting directory change in restricted mode."
26 cd ..
27 echo "Still in `pwd`"
28
http://www.818198.com  Page 378
SHELL十三問
29 echo
30 echo
31
32 echo "\$SHELL = $SHELL"
33 echo "Attempting to change shell in restricted mode."
34 SHELL="/bin/ash"
35 echo
36 echo "\$SHELL= $SHELL"
37
38 echo
39 echo
40
41 echo "Attempting to redirect output in restricted mode."
42 ls -l /usr/bin > bin.files
43 ls -l bin.files    # Try to list attempted file creation effort.
44
45 echo
46
47 exit 0
################################End Script#########################################
第22章 進程替換
================
進程替換與命令替換(command substitution)很相似. 命令替換把一個命令的結果賦給一個
變量,例如 dir_contents=`ls -al`或xref=$( grep word datafile). 進程替換則是把一個進
程的輸出回饋給另一個進程 (換句話說,它把一個命令的結果發送給另一個命令).
命令替換的一般形式
由圓括號括起的命令
 >(command)
 <(command)
 啓動進程替換. 它是用/dev/fd/<n>文件把在圓括號內的進程的處理結果發送給另外一個進
 程. [1] (譯者注:實際上現代的UNIX類操作系統提供的/dev/fd/n文件是與文件描述相關
 的,整數n指的就是在進程運行時對應數字的文件描述符)
 注意: 在"<" 或or ">" 與圓括號之間是沒有空格的. 如果加了空格將會引起錯誤信息.
 bash$ echo >(true)
 /dev/fd/63
 
 bash$ echo <(true)
 /dev/fd/63
 Bash在兩個文件描述符(file descriptors)之間創建了一個管道, --fIn 和 fOut--. true
命令的標準輸入被連接到fOut(dup2(fOut, 0)), 然後Bash把/dev/fd/fIn作爲參數傳給echo.
如果系統的/dev/fd/<n>文件不夠時,Bash會使用臨時文件. (Thanks, S.C.)
進程替換能比較兩個不同命令之間的輸出,或者甚至相同命令不同選項的輸出.
 bash$ comm <(ls -l) <(ls -al)
 total 12
-rw-rw-r--    1 bozo bozo       78 Mar 10 12:58 File0
-rw-rw-r--    1 bozo bozo       42 Mar 10 12:58 File2
-rw-rw-r--    1 bozo bozo      103 Mar 10 12:58 t2.sh
        total 20
http://www.818198.com  Page 379
SHELL十三問
        drwxrwxrwx    2 bozo bozo     4096 Mar 10 18:10 .
        drwx------   72 bozo bozo     4096 Mar 10 17:58 ..
        -rw-rw-r--    1 bozo bozo       78 Mar 10 12:58 File0
        -rw-rw-r--    1 bozo bozo       42 Mar 10 12:58 File2
        -rw-rw-r--    1 bozo bozo      103 Mar 10 12:58 t2.sh
用進程替換來比較兩個不同目錄的內容 (考察哪些文件名是相同的,哪些是不同的):
   1 diff <(ls $first_directory) <(ls $second_directory)
其他一些進程替換的用法和技巧:
   1 cat <(ls -l)
   2 # 等同於     ls -l | cat
   3
   4 sort -k 9 <(ls -l /bin) <(ls -l /usr/bin) <(ls -l /usr/X11R6/bin)
   5 # 列出系統中3個主要的'bin'目錄的所有文件,並且按文件名排序.
   6 # 注意是三個明顯不同的命令輸出回饋給'sort'.
   7
   8 
   9 diff <(command1) <(command2)    # 給出兩個命令輸出的不同之處.
  10
  11 tar cf >(bzip2 -c > file.tar.bz2) $directory_name
  12 # 調用"tar cf /dev/fd/?? $directory_name",和"bzip2 -c > file.tar.bz2".
  13 #
  14 # 因爲/dev/fd/<n>的系統屬性,
  15 # 所以兩個命令之間的管道不必是命名的.
  16 #
  17 # 這種效果可以模仿出來.
  18 #
  19 bzip2 -c < pipe > file.tar.bz2&
  20 tar cf pipe $directory_name
  21 rm pipe
  22 #        或者
  23 exec 3>&1
  24 tar cf /dev/fd/4 $directory_name 4>&1 >&3 3>&- | bzip2 -c > file.tar.bz2 3>&-  25 exec 3>&-  26
  27
  28 # Thanks, St`phane Chazelas
有個讀者給我發來下面關於進程替換的有趣例子A.
   1 # 摘自SuSE發行版中的代碼片斷:
   2
   3 while read  des what mask iface; do
   4 # 這裏省略了一些命令 ...
   5 done < <(route -n) 
   6
   7
   8 # 爲了測試它,我們來做些動作.
   9 while read  des what mask iface; do
  10   echo $des $what $mask $iface
  11 done < <(route -n) 
http://www.818198.com  Page 380
SHELL十三問
  12
  13 # 輸出:
  14 # Kernel IP routing table
  15 # Destination Gateway Genmask Flags Metric Ref Use Iface
  16 # 127.0.0.0 0.0.0.0 255.0.0.0 U 0 0 0 lo
  17
  18
  19
  20 # 由 St�phane Chazelas給出的,一個更容易理解的等價代碼是:
  21 route -n |
  22   while read des what mask iface; do   # 管道的輸出被賦給了變量.
  23     echo $des $what $mask $iface
  24   done  #  這樣就取回了和上面一樣的輸出.
  25         #  但是, Ulrich Gayer指出 . . .
  26         #+ 這個簡單版本的等價代碼在while循環中使用了一個子shell,
  27         #+ 因此當管道結束後變量會被毀掉.
  28 
  29
  30 
  31 #  更進一步, Filip Moritz解釋了上面兩個例子之間有一個細微的不同之處
  32 #+ 如下所示.
  33
  34 (
  35 route -n | while read x; do ((y++)); done
  36 echo $y # $y 仍然沒有被聲明或設置
  37
  38 while read x; do ((y++)); done < <(route -n)
  39 echo $y # $y的值爲 route -n 輸出的行數
  40 )
  41
  42 # 一般來說
  43 (
  44 : | x=x
  45 # 看上去是啓動了一個子shell
  46 : | ( x=x )
  47 # 但
  48 x=x < <(:)
  49 # 實際上不是
  50 )
  51
  52 # 當解析csv或類似的東西時非常有用.
  53 # 事實上,這就是SuSE原本的代碼片斷所要實現的功能.
注意事項:
[1]  這與命名管道(named pipe)(臨時文件)有相同的作用, 事實上命名管道同樣在進程
  替換中被使用.
第23章 函數
============
和"真正的"編程語言一樣, Bash也有函數,雖然在某些實現方面稍有些限制. 一個函數是一個
http://www.818198.com  Page 381
SHELL十三問
子程序,用於實現一串操作的代碼塊(code block),它是完成特定任務的"黑盒子". 當有重複
代碼, 當一個任務只需要很少的修改就被重複幾次執行時, 這時你應考慮使用函數.
function function_name {
command...
}

function_name () {
command...
}
第二種格式的寫法更深得C程序員的喜歡(並且也是更可移植的).
因爲在C中,函數的左花括號也可以寫在下一行中.
function_name ()
{
command...
}
函數被調用或被觸發, 只需要簡單地用函數名調用.
Example 23-1 簡單函數
################################Start Script#######################################
 1 #!/bin/bash
 2
 3 JUST_A_SECOND=1
 4
 5 funky ()
 6 { # 這是一個最簡單的函數.
 7   echo "This is a funky function."
 8   echo "Now exiting funky function."
 9 } # 函數必須在調用前聲明.
10
11
12 fun ()
13 { # 一個稍複雜的函數.
14   i=0
15   REPEATS=30
16
17   echo
18   echo "And now the fun really begins."
19   echo
20
21   sleep $JUST_A_SECOND    # 嘿, 暫停一秒!
22   while [ $i -lt $REPEATS ]
23   do
24     echo "----------FUNCTIONS---------->"
25     echo "<------------ARE-------------"
26     echo "<------------FUN------------>"
27     echo
28     let "i+=1"
29   done
30 }
http://www.818198.com  Page 382
SHELL十三問
31
32   # 現在,調用兩個函數.
33
34 funky
35 fun
36
37 exit 0
################################End Script#########################################
函數定義必須在第一次調用函數前完成.沒有像C中的函數“聲明”方法.
   1 f1
   2 # 因爲函數"f1"還沒有定義,這會引起錯誤信息.
   3
   4 declare -f f1      # 這樣也沒用.
   5 f1                 # 仍然會引起錯誤.
   6
   7 # 然而...
   8
   9   
  10 f1 ()
  11 {
  12   echo "Calling function \"f2\" from within function \"f1\"."
  13   f2
  14 }
  15
  16 f2 ()
  17 {
  18   echo "Function \"f2\"."
  19 }
  20
  21 f1  #  雖然在它定義前被引用過,
  22     #+ 函數"f2"實際到這兒才被調用.
  23     #  這樣是允許的.
  24    
  25     # Thanks, S.C.
在一個函數內嵌套另一個函數也是可以的,但是不常用.
   1 f1 ()
   2 {
   3
   4   f2 () # nested
   5   {
   6     echo "Function \"f2\", inside \"f1\"."
   7   }
   8
   9 } 
  10
  11 f2  #  引起錯誤.
  12     #  就是你先"declare -f f2"了也沒用.
  13
http://www.818198.com  Page 383
SHELL十三問
  14 echo   
  15
  16 f1  #  什麼也不做,因爲調用"f1"不會自動調用"f2".
  17 f2  #  現在,可以正確的調用"f2"了,
  18     #+ 因爲之前調用"f1"使"f2"在腳本中變得可見了.
  19
  20     # Thanks, S.C.
函數聲明可以出現在看上去不可能出現的地方,那些不可能的地方本該由一個命令出現的地方.
   1 ls -l | foo() { echo "foo"; }  # 允許,但沒什麼用.
   2
   3
   4
   5 if [ "$USER" = bozo ]
   6 then
   7   bozo_greet ()   # 在if/then結構中定義了函數.
   8   {
   9     echo "Hello, Bozo."
  10   }
  11 fi 
  12
  13 bozo_greet        # 只能由Bozo運行, 其他用戶會引起錯誤.
  14
  15
  16
  17 # 在某些上下文,像這樣可能會有用.
  18 NO_EXIT=1   # 將會打開下面的函數定義.
  19
  20 [[ $NO_EXIT -eq 1 ]] && exit() { true; }     # 在"and-list"(and列表)中定義函數.
  21 # 如果 $NO_EXIT 是 1,聲明函數"exit ()".
  22 # 把"exit"取別名爲"true"將會禁用內建的"exit".
  23
  24 exit  # 調用"exit ()"函數, 而不是內建的"exit".
  25
  26 # Thanks, S.C.
23.1. 複雜函數和函數複雜性
--------------------------函數可以處理傳遞給它的參數並且能返回它的退出狀態碼(exit status)給腳本後續使用.
   1 function_name $arg1 $arg2
函數以位置來引用傳遞過來的參數(就好像他們是位置參數(positional parameters)), 例如
$1, $2,以此類推.
Example 23-2 帶着參數的函數
################################Start Script#######################################
 1 #!/bin/bash
 2 # 函數和參數
 3
 4 DEFAULT=default                             # 默認的參數值.
 5
 6 func2 () {
http://www.818198.com  Page 384
SHELL十三問
 7    if [ -z "$1" ]                           # 第一個參數是否長度爲零?
 8    then
 9      echo "-Parameter #1 is zero length.-"  # 則沒有參數傳遞進來.
10    else
11      echo "-Param #1 is \"$1\".-"
12    fi
13
14    variable=${1-$DEFAULT}                   # 
15    echo "variable = $variable"              #  參數替換會表現出什麼?
16                                             #  ---------------------------17                                             #  它用於分辨沒有參數和一個只有NULL值的參數.
18                                             #
19
20    if [ "$2" ]
21    then
22      echo "-Parameter #2 is \"$2\".-"
23    fi
24
25    return 0
26 }
27
28 echo
29   
30 echo "Nothing passed."  
31 func2                          # 沒有參數來調用
32 echo
33
34
35 echo "Zero-length parameter passed."
36 func2 ""                       # 以一個長度爲零的參數調用
37 echo
38
39 echo "Null parameter passed."
40 func2 "$uninitialized_param"   # 以未初始化的參數來調用
41 echo
42
43 echo "One parameter passed."  
44 func2 first           # 用一個參數來調用
45 echo
46
47 echo "Two parameters passed."  
48 func2 first second    # 以二個參數來調用
49 echo
50
51 echo "\"\" \"second\" passed."
52 func2 "" second       # 以第一個參數爲零長度,而第二個參數是一個ASCII碼組成的字符串來調用.
53 echo                  #
54
http://www.818198.com  Page 385
SHELL十三問
55 exit 0
################################End Script#########################################
注意:  shift命令可以工作在傳遞給函數的參數 (參考例子 33-15).
但是,傳給腳本的命令行參數怎麼辦?在函數內部可以看到它們嗎?好,讓我們來弄清楚.
Example 23-3 函數和被傳給腳本的命令行參數
################################Start Script#######################################
 1 #!/bin/bash
 2 # func-cmdlinearg.sh
 3 #  以一個命令行參數來調用這個腳本,
 4 #+ 類似 $0 arg1來調用.
 5
 6
 7 func ()
 8
 9 {
10 echo "$1"
11 }
12
13 echo "First call to function: no arg passed."
14 echo "See if command-line arg is seen."
15 func
16 # 不!命令行參數看不到.
17
18 echo "============================================================"
19 echo
20 echo "Second call to function: command-line arg passed explicitly."
21 func $1
22 # 現在可以看到了!
23
24 exit 0
################################End Script#########################################
與別的編程語言相比,shell腳本一般只傳遞值給函數,變量名(實現上是指針)如果作爲參數傳遞給函數會被看成是字面上字符
串的意思.函數解釋參數是以字面上的意思來解釋的.
間接變量引用(Indirect variable references) (參考例子 34-2)提供了傳遞變量指針給函數的一個笨拙的機制.
Example 23-4 傳遞間接引用給函數
################################Start Script#######################################
 1 #!/bin/bash
 2 # ind-func.sh: 傳遞間接引用給函數.
 3
 4 echo_var ()
 5 {
 6 echo "$1"
 7 }
 8
 9 message=Hello
10 Hello=Goodbye
11
12 echo_var "$message"        # Hello
http://www.818198.com  Page 386
SHELL十三問
13 # 現在,讓我們傳遞一個間接引用給函數.
14 echo_var "${!message}"     # Goodbye
15
16 echo "-------------"
17
18 # 如果我們改變"hello"變量的值會發生什麼?
19 Hello="Hello, again!"
20 echo_var "$message"        # Hello
21 echo_var "${!message}"     # Hello, again!
22
23 exit 0
################################End Script#########################################
下一個邏輯問題是:在傳遞參數給函數之後是否能解除參數的引用.
Example 23-5 解除傳遞給函數的參數引用
################################Start Script#######################################
 1 #!/bin/bash
 2 # dereference.sh
 3 # 給函數傳遞不同的參數.
 4 # Bruce W. Clare編寫.
 5
 6 dereference ()
 7 {
 8      y=\$"$1"   # 變量名.
 9      echo $y    # $Junk
10
11      x=`eval "expr \"$y\" "`
12      echo $1=$x
13      eval "$1=\"Some Different Text \""  # 賦新值.
14 }
15
16 Junk="Some Text"
17 echo $Junk "before"    # Some Text before
18
19 dereference Junk
20 echo $Junk "after"     # Some Different Text after
21
22 exit 0
################################End Script#########################################
Example 23-6 再次嘗試解除傳遞給函數的參數引用
################################Start Script#######################################
 1 #!/bin/bash
 2 # ref-params.sh: 解除傳遞給函數的參數引用.
 3 #                (複雜例子)
 4
 5 ITERATIONS=3  # 取得輸入的次數.
 6 icount=1
 7
 8 my_read () {
http://www.818198.com  Page 387
SHELL十三問
 9   #  用my_read varname來調用,
10   #+ 輸出用括號括起的先前的值作爲默認值,
11   #+ 然後要求輸入一個新值.
12
13   local local_var
14
15   echo -n "Enter a value "
16   eval 'echo -n "[$'$1'] "'  #  先前的值.
17 # eval echo -n "[\$$1] "     #  更好理解,
18                              #+ 但會丟失用戶輸入在尾部的空格.
19   read local_var
20   [ -n "$local_var" ] && eval $1=\$local_var
21
22   # "and列表(And-list)": 如果變量"local_var"測試成功則把變量"$1"的值賦給它.
23 }
24
25 echo
26
27 while [ "$icount" -le "$ITERATIONS" ]
28 do
29   my_read var
30   echo "Entry #$icount = $var"
31   let "icount += 1"
32   echo
33 done 
34
35
36 # 多謝Stephane Chazelas提供的示範例子.
37
38 exit 0
################################End Script#########################################
退出和返回
退出狀態(exit status)
    函數返回一個被稱爲退出狀態的值. 退出狀態可以由return來指定statement, 否則函數的
 退出狀態是函數最後一個執行命令的退出狀態(0表示成功,非0表示出錯代碼). 退出狀態
 (exit status)可以在腳本中由$? 引用. 這個機制使腳本函數也可以像C函數一樣有一個"
 返回值".
return
    終止一個函數.return 命令[1]可選地帶一個整數參數,這個整數作爲函數的"返回值"返回
 給調用此函數的腳本,並且這個值也被賦給變量$?.
Example 23-7 兩個數中的最大者
################################Start Script#######################################
 1 #!/bin/bash
 2 # max.sh: 兩個整數中的最大者.
 3
 4 E_PARAM_ERR=-198    # 如果傳給函數的參數少於2個時的返回值.
 5 EQUAL=-199          # 如果兩個整數值相等的返回值.
 6 #  任一個傳給函數的參數值溢出
http://www.818198.com  Page 388
SHELL十三問
 7 #
 8
 9 max2 ()             # 返回兩個整數的較大值.
10 {                   # 注意: 參與比較的數必須小於257.
11 if [ -z "$2" ]
12 then
13   return $E_PARAM_ERR
14 fi
15
16 if [ "$1" -eq "$2" ]
17 then
18   return $EQUAL
19 else
20   if [ "$1" -gt "$2" ]
21   then
22     return $1
23   else
24     return $2
25   fi
26 fi
27 }
28
29 max2 33 34
30 return_val=$?
31
32 if [ "$return_val" -eq $E_PARAM_ERR ]
33 then
34   echo "Need to pass two parameters to the function."
35 elif [ "$return_val" -eq $EQUAL ]
36   then
37     echo "The two numbers are equal."
38 else
39     echo "The larger of the two numbers is $return_val."
40 fi 
41
42  
43 exit 0
44
45 #  練習 (容易):
46 #  ---------------47 #  把這個腳本轉化成交互式的腳本,
48 #+ 也就是說,讓腳本可以要求調用者輸入兩個整數.
################################End Script#########################################
 注意: 爲了函數可以返回字符串或是數組,用一個可在函數外可見的變量.
    1 count_lines_in_etc_passwd()
    2 {
    3   [[ -r /etc/passwd ]] && REPLY=$(echo $(wc -l < /etc/passwd))
    4   #  如果/etc/passwd可讀,則把REPLY設置成文件的行數.
http://www.818198.com  Page 389
SHELL十三問
    5   #  返回一個參數值和狀態信息.
    6   #  'echo'好像沒有必要,但 . . .
    7   #+ 它的作用是刪除輸出中的多餘空白字符.
    8 }
    9
   10 if count_lines_in_etc_passwd
   11 then
   12   echo "There are $REPLY lines in /etc/passwd."
   13 else
   14   echo "Cannot count lines in /etc/passwd."
   15 fi 
   16
   17 # Thanks, S.C.
Example 23-8 把數字轉化成羅馬數字
################################Start Script#######################################
 1 #!/bin/bash
 2
 3 # 阿拉伯數字轉化爲羅馬數字
 4 # 轉化範圍: 0 - 200
 5 # 這是比較粗糙的,但可以工作.
 6
 7 # 擴展可接受的範圍來作爲腳本功能的擴充,這個作爲練習完成.
 8
 9 # 用法: roman number-to-convert
10
11 LIMIT=200
12 E_ARG_ERR=65
13 E_OUT_OF_RANGE=66
14
15 if [ -z "$1" ]
16 then
17   echo "Usage: `basename $0` number-to-convert"
18   exit $E_ARG_ERR
19 fi 
20
21 num=$1
22 if [ "$num" -gt $LIMIT ]
23 then
24   echo "Out of range!"
25   exit $E_OUT_OF_RANGE
26 fi 
27
28 to_roman ()   # 在第一次調用函數前必須先定義.
29 {
30 number=$1
31 factor=$2
32 rchar=$3
33 let "remainder = number - factor"
http://www.818198.com  Page 390
SHELL十三問
34 while [ "$remainder" -ge 0 ]
35 do
36   echo -n $rchar
37   let "number -= factor"
38   let "remainder = number - factor"
39 done 
40
41 return $number
42        # 練習:
43        # --------44        # 解釋這個函數是怎麼工作的.
45        # 提示: 靠不斷地除來分割數字.
46 }
47   
48
49 to_roman $num 100 C
50 num=$?
51 to_roman $num 90 LXXXX
52 num=$?
53 to_roman $num 50 L
54 num=$?
55 to_roman $num 40 XL
56 num=$?
57 to_roman $num 10 X
58 num=$?
59 to_roman $num 9 IX
60 num=$?
61 to_roman $num 5 V
62 num=$?
63 to_roman $num 4 IV
64 num=$?
65 to_roman $num 1 I
66
67 echo
68
69 exit 0
################################End Script#########################################
 請參考例子 10-28.
 注意: 函數最大可返回的正整數爲255. return 命令與退出狀態(exit status)的概念聯
   系很緊密,而退出狀態的值受此限制.幸運地是有多種(工作區workarounds)來對
   付這種要求函數返回大整數的情況.
Example 23-9 測試函數最大的返回值
################################Start Script#######################################
 1 #!/bin/bash
 2 # return-test.sh
 3
 4 # 一個函數最大可能返回的值是255.
 5
http://www.818198.com  Page 391
SHELL十三問
 6 return_test ()         # 無論傳給函數什麼都返回它.
 7 {
 8   return $1
 9 }
10
11 return_test 27         # o.k.
12 echo $?                # 返回 27.
13  
14 return_test 255        # 仍然 o.k.
15 echo $?                # 返回 255.
16
17 return_test 257        # 錯誤!
18 echo $?                # 返回 1 (返回代碼指示錯誤).
19
20 # ======================================================
21 return_test -151896    # 能夠返回這個非常大的負數麼?
22 echo $?                # 會返回-151896?
23                        # 不! 它將返回168.
24 #  2.05b版本之前的Bash是允許
25 #+ 超大負整數作爲返回值的.
26 #  但是比它更新一點的版本修正了這個漏洞.
27 #  這將破壞比較老的腳本.
28 #  慎用!
29 # ======================================================
30
31 exit 0
################################End Script#########################################
 如果你非常想使用超大整數作爲"返回值"的話, 那麼只能通過將你想返回的返回值直接的
 傳遞到一個全局變量中的手段來達到目的.
    1 Return_Val=   # 全局變量, 用來保存函數中需要返回的超大整數.
    2
    3 alt_return_test ()
    4 {
    5   fvar=$1
    6   Return_Val=$fvar
    7   return   # Returns 0 (success).
    8 }
    9
   10 alt_return_test 1
   11 echo $?                              # 0
   12 echo "return value = $Return_Val"    # 1
   13
   14 alt_return_test 256
   15 echo "return value = $Return_Val"    # 256
   16
   17 alt_return_test 257
   18 echo "return value = $Return_Val"    # 257
   19
http://www.818198.com  Page 392
SHELL十三問
   20 alt_return_test 25701
   21 echo "return value = $Return_Val"    #25701
 一種更優雅的方法是讓函數echo出它的返回值, 輸出到stdout上, 然後再通過"命令替換"
 的手段來捕獲它. 參考Section 33.7關於這個問題的討論.
Example 23-10 比較兩個大整數
################################Start Script#######################################
 1 #!/bin/bash
 2 # max2.sh: 取兩個超大整數中最大的.
 3
 4 #  這個腳本與前面的"max.sh"例子作用相同,
 5 #+ 經過修改可以適用於比較超大整數.
 6
 7 EQUAL=0             # 如果兩個參數相同的返回值.
 8 E_PARAM_ERR=-99999  # 沒有足夠的參數傳遞到函數中.
 9 #           ^^^^^^    也可能是傳遞到函數中的某個參數超出範圍了.
10
11 max2 ()             # 從這兩個數中"返回"更大一些的.
12 {
13 if [ -z "$2" ]
14 then
15   echo $E_PARAM_ERR
16   return
17 fi
18
19 if [ "$1" -eq "$2" ]
20 then
21   echo $EQUAL
22   return
23 else
24   if [ "$1" -gt "$2" ]
25   then
26     retval=$1
27   else
28     retval=$2
29   fi
30 fi
31
32 echo $retval        # echo(到stdout), 而不是使用返回值.
33                     # 爲什麼?
34 }
35
36
37 return_val=$(max2 33001 33997)
38 #            ^^^^             函數名
39 #                 ^^^^^ ^^^^^ 這是傳遞進來的參數
40 #  這事實上是一個命令替換的形式:
41 #+ 會把這個函數當作一個命令來處理,
42 #+ 並且分配這個函數的stdout到變量"return_val"中.
http://www.818198.com  Page 393
SHELL十三問
43
44
45 # ========================= OUTPUT ========================
46 if [ "$return_val" -eq "$E_PARAM_ERR" ]
47   then
48   echo "Error in parameters passed to comparison function!"
49 elif [ "$return_val" -eq "$EQUAL" ]
50   then
51     echo "The two numbers are equal."
52 else
53     echo "The larger of the two numbers is $return_val."
54 fi
55 # =========================================================
56  
57 exit 0
58
59 #  練習:
60 #  -----61 #  1) 找出一種更優雅的方法來測試
62 #+    傳遞到函數中的參數.
63 #  2) 在"OUTPUT"的時候簡化if/then結構.
64 #  3) 重寫這個腳本使其能夠從命令行參數中來獲取輸入.
################################End Script#########################################
 下邊是獲得一個函數的"返回值"的另一個例子. 想要了解這個例子需要一些awk的知識.
    1 month_length ()  # 以月份數作爲參數.
    2 {                # 返回這個月有幾天.
    3 monthD="31 28 31 30 31 30 31 31 30 31 30 31"  # 作爲局部變量來聲明?
    4 echo "$monthD" | awk '{ print $'"${1}"' }'    # 有技巧的.
    5 #                             ^^^^^^^^^
    6 # 先將參數傳遞到函數中  ($1 -- 月份號), 然後就到awk了.
    7 # Awk將會根據傳遞進來的月份號來決定打印"print $1 . . . print $12"中的哪個 (依賴於月份號)
    8 # 傳遞參數到內嵌awk腳本的模版:
    9 #                                 $'"${script_parameter}"'
   10
   11 #  需要錯誤檢查來修正參數的範圍(1-12)
   12 #+ 並且要處理閏年的特殊的2月.
   13 }
   14
   15 # ----------------------------------------------   16 # 用例:
   17 month=4        # 拿4月來舉個例子.
   18 days_in=$(month_length $month)
   19 echo $days_in  # 30
   20 # ----------------------------------------------
 也參考例子 A-7.
 練習: 用我們已經學到的擴展先前羅馬數字那個例子腳本能接受任意大的輸入.
重定向
重定向函數的標準輸入
http://www.818198.com  Page 394
SHELL十三問
 函數本質上是一個代碼塊(code block), 這樣意思着它的標準輸入可以被重定向
 (就像在例子 3-1中顯示的).
Example 23-11 用戶名的真實名
################################Start Script#######################################
 1 #!/bin/bash
 2 # realname.sh
 3 #
 4 # 由用戶名而從/etc/passwd取得"真實名".
 5
 6
 7 ARGCOUNT=1       # 需要一個參數.
 8 E_WRONGARGS=65
 9
10 file=/etc/passwd
11 pattern=$1
12
13 if [ $# -ne "$ARGCOUNT" ]
14 then
15   echo "Usage: `basename $0` USERNAME"
16   exit $E_WRONGARGS
17 fi 
18
19 file_excerpt ()  # 以要求的模式來掃描文件,然後打印文件相關的部分.
20 {
21 while read line  # "while" does not necessarily need "[ condition ]"
22 do
23   echo "$line" | grep $1 | awk -F":" '{ print $5 }'  # awk指定使用":"爲界定符.
24 done
25 } <$file  # 重定向函數的標準輸入.
26
27 file_excerpt $pattern
28
29 # Yes, this entire script could be reduced to
30 #       grep PATTERN /etc/passwd | awk -F":" '{ print $5 }'
31 # or
32 #       awk -F: '/PATTERN/ {print $5}'
33 # or
34 #       awk -F: '($1 == "username") { print $5 }' # real name from username
35 # 但是,這些可能起不到示例的作用.
36
37 exit 0
################################End Script#########################################
 還有一個辦法,可能是更好理解的重定向函數標準輸入方法.它爲函數內的一個括號內的
 代碼塊調用標準輸入重定向.
   1 # 用下面的代替:
   2 Function ()
   3 {
   4  ...
http://www.818198.com  Page 395
SHELL十三問
   5  } < file
   6
   7 # 也試一下這個:
   8 Function ()
   9 {
  10   {
  11     ...
  12    } < file
  13 }
  14
  15 # 同樣,
  16
  17 Function ()  # 可以工作.
  18 {
  19   {
  20    echo $*
  21   } | tr a b
  22 }
  23
  24 Function ()  # 這個不會工作
  25 {
  26   echo $*
  27 } | tr a b   # 這兒的內嵌代碼塊是強制的.
  28
  29
  30 # Thanks, S.C.
注意事項:
[1]  return命令是Bash內建(builtin)的.
23.2. 局部變量
--------------怎麼樣使一個變量變成局部的?
局部變量
 如果變量用local來聲明,那麼它只能在該變量聲明的代碼塊(block of code)中可見.
 這個代碼塊就是局部"範圍". 在一個函數內,局部變量意味着只能在函數代碼塊內它才
 有意義.
Example 23-12 局部變量的可見範圍
################################Start Script#######################################
 1 #!/bin/bash
 2 # 在函數內部的全局和局部變量.
 3
 4 func ()
 5 {
 6   local loc_var=23       # 聲明爲局部變量.
 7   echo                   # 使用內建的'local'關鍵字.
 8   echo "\"loc_var\" in function = $loc_var"
 9   global_var=999         # 沒有聲明爲局部變量.
10                          # 默認爲全局變量.
11   echo "\"global_var\" in function = $global_var"
http://www.818198.com  Page 396
SHELL十三問
12 } 
13
14 func
15
16 # 現在,來看看是否局部變量"loc_var"能否在函數外面可見.
17
18 echo
19 echo "\"loc_var\" outside function = $loc_var"
20                                       # $loc_var outside function =
21                                       # 不, $loc_var不是全局可訪問的.
22 echo "\"global_var\" outside function = $global_var"
23                                       # $global_var outside function = 999
24                                       # $global_var 是全局可訪問的.
25 echo         
26
27 exit 0
28 #  與In contrast to C相比, 在函數內聲明的Bash變量只有在
29 #+ 它被明確聲明成局部的變量時纔是局部的.
################################End Script#########################################
 注意: 在函數調用之前,所有在函數內聲明且沒有明確聲明爲local的變量都可在函數體
   外可見.
    1 #!/bin/bash
    2
    3 func ()
    4 {
    5 global_var=37    #  在函數還沒有被調用前
    6                  #+ 變量只在函數內可見.
    7 }                #  函數結束
    8
    9 echo "global_var = $global_var"  # global_var =
   10                                  #  函數"func"還沒有被調用,
   11                                  #+ 所以變量$global_var還不能被訪問.
   12
   13 func
   14 echo "global_var = $global_var"  # global_var = 37
   15                                  # 已經在函數調用時設置了值.
23.2.1. 局部變量使遞歸變得可能.
-------------------------------局部變量可以遞歸, [1] 但這個辦法會產生大量的計算,因此它在shell腳本中是被明確表明
不推薦的. [2]
Example 23-13 用局部變量來遞歸
################################Start Script#######################################
 1 #!/bin/bash
 2
 3 #               階乘
 4 #               --------- 5
 6
http://www.818198.com  Page 397
SHELL十三問
 7 # bash允許遞歸嗎?
 8 # 嗯, 允許, 但是...
 9 # 它太慢以致你難以忍受.
10
11
12 MAX_ARG=5
13 E_WRONG_ARGS=65
14 E_RANGE_ERR=66
15
16
17 if [ -z "$1" ]
18 then
19   echo "Usage: `basename $0` number"
20   exit $E_WRONG_ARGS
21 fi
22
23 if [ "$1" -gt $MAX_ARG ]
24 then
25   echo "Out of range (5 is maximum)."
26   #  現在讓我們來了解實際情況.
27   #  如果你想求比這個更大的範圍的階乘,
28   #+ 應該重新用一個真正的編程語言來寫.
29   exit $E_RANGE_ERR
30 fi 
31
32 fact ()
33 {
34   local number=$1
35   #  變量"number"必須聲明爲局部,
36   #+ 否則它不會工作.
37   if [ "$number" -eq 0 ]
38   then
39     factorial=1    # 0的階乘爲1.
40   else
41     let "decrnum = number - 1"
42     fact $decrnum  # 遞歸調用(函數內部調用自己本身).
43     let "factorial = $number * $?"
44   fi
45
46   return $factorial
47 }
48
49 fact $1
50 echo "Factorial of $1 is $?."
51
52 exit 0
################################End Script#########################################
也請參考例子 A-16的腳本遞歸的例子. 必須意識到遞歸也意味着巨大的資源消耗和緩慢的運
http://www.818198.com  Page 398
SHELL十三問
行,因此它不適合在腳本中使用.
注意事項:
[1]  Herbert Mayer 給遞歸下的定義是". . . expressing an algorithm by using a
  simpler version of that same algorithm(用一個相同算法的版本來表示一個算法)
  . . ." 遞歸函數是調用它自己本身的函數.
[2]  太多層的遞歸可能會引起腳本段錯誤而崩潰.
     1 #!/bin/bash
     2
     3 #  警告: 運行這個腳本可能使你的系統失去響應!
     4 #  如果你運氣不錯,在它使用完所有可用內存之前會段錯誤而退出.
     5
     6 recursive_function ()    
     7 {
     8 echo "$1"     # 使函數做些事情以加速產生段錯誤.
     9 (( $1 < $2 )) && recursive_function $(( $1 + 1 )) $2;
    10 #  當第一個參數比第二個參數少時,
    11 #+ 把第1個參數增1再次遞歸.
    12 }
    13
    14 recursive_function 1 50000  # 遞歸 50,000 次!
    15 #  非常可能段錯誤 (依賴於棧的大小,它由ulimit -m設置).
    16
    17 #  這種深度的遞歸甚至可能由於耗盡棧的內存大小而引起C程序的段錯誤.
    18 #
    19
    20
    21 echo "This will probably not print."
    22 exit 0  # 這個腳本將不會從這兒正常退出.
    23
    24 #  多謝, St`phane Chazelas.
23.3. 不使用局部變量的遞歸
--------------------------函數甚至可以不使用局部變量來調用自己.
Example 23-14 漢諾塔
################################Start Script#######################################
  1 #! /bin/bash
  2 #
  3 # 漢諾塔(The Towers Of Hanoi)
  4 # Bash script
  5 # Copyright (C) 2000 Amit Singh. All Rights Reserved.
  6 # http://hanoi.kernelthread.com
  7 #
  8 # 在bash version 2.05b.0(13)-release下測試通過
  9 #
 10 #  經過作者同意後在"Advanced Bash Scripting Guide"書中使用
 11 #
 12 #  由ABS的作者做了少許修改.
 13
http://www.818198.com  Page 399
SHELL十三問
 14 #=================================================================#
 15 #  漢諾塔是由Edouard Lucas提出的數學謎題 ,
 16 #+ 他是19世紀的法國數學家.
 17 #
 18 #  有三個直立的柱子豎在地面上.
 19 #  第一個柱子有一組的盤子套在上面.
 20 #  這些盤子是平整的,中間帶着孔,
 21 #+ 因此它們才能套在柱子上面.
 22 #  這組盤子有不同的直徑,它們是依照直徑從小到大來從高到低放置.
 23 #
 24 #  最小的盤在最高,最大的盤在最底部.
 25 #
 26 #  現在的任務是要把這一組的盤子從一個柱子全部地搬到另一個柱子上.
 27 #
 28 #  你只能一次從一個柱子上移動一個盤子到另一個柱子.
 29 #  允許把盤子重新移回到它原來的最初位置.
 30 #  你可以把一個小的盤子放在大的盤子上面,
 31 #+ 但不能把大的盤子放在小的盤子上面.
 32 #  請注意這一點.
 33 #
 34 #  對於這一組盤子,數量少時,只需要移動很少的次數就能達到要求.
 35 #+ 但隨着這組盤子的數量的增加,
 36 #+ 移動的次數幾乎成倍增長的,
 37 #+ 而移動的策略變得愈加複雜.
 38 #
 39 #  想了解更多的信息, 請訪問 http://hanoi.kernelthread.com.
 40 #
 41 #
 42 #         ...                   ...                    ...
 43 #         | |                   | |                    | |
 44 #        _|_|_                  | |                    | |
 45 #       |_____|                 | |                    | |
 46 #      |_______|                | |                    | |
 47 #     |_________|               | |                    | |
 48 #    |___________|              | |                    | |
 49 #   |             |             | |                    | |
 50 # .--------------------------------------------------------------.
 51 # |**************************************************************|
 52 #          #1                   #2                      #3
 53 #
 54 #=================================================================#
 55
 56
 57 E_NOPARAM=66  # 沒有參數傳給腳本.
 58 E_BADPARAM=67 # 傳給腳本的盤子數不合法.
 59 Moves=        # 保存移動次數的全局變量.
 60               # 這兒修改了原腳本.
 61
http://www.818198.com  Page 400
SHELL十三問
 62 dohanoi() {   # 遞歸函數.
 63     case $1 in
 64     0)
 65         ;;
 66     *)
 67         dohanoi "$(($1-1))" $2 $4 $3
 68         echo move $2 "-->" $3
 69  let "Moves += 1"  # 這兒修改了原腳本.
 70         dohanoi "$(($1-1))" $4 $3 $2
 71         ;;
 72     esac
 73 }
 74
 75 case $# in
 76 1)
 77     case $(($1>0)) in     # 至少要有一個盤子.
 78     1)
 79         dohanoi $1 1 3 2
 80         echo "Total moves = $Moves"
 81         exit 0;
 82         ;;
 83     *)
 84         echo "$0: illegal value for number of disks";
 85         exit $E_BADPARAM;
 86         ;;
 87     esac
 88     ;;
 89 *)
 90     echo "usage: $0 N"
 91     echo "       Where \"N\" is the number of disks."
 92     exit $E_NOPARAM;
 93     ;;
 94 esac
 95
 96 # 練習:
 97 # --------- 98 # 1) 從現在這個位置以下的命令會不會總是被執行?
 99 #    爲什麼? (容易)
100 # 2) 解釋這個可運行的"dohanoi"函數的原理.
101 #    (難)
################################End Script#########################################
第24章 別名(Aliases)
=====================
Bash別名本質上是一個簡稱, 縮寫, 這可避免鍵入過長的命令序列. 例如,如果我們添加
alias lm="ls -l | more" 這一行到文件~/.bashrc file裏, 然後每次在命令行鍵入lm 將會
自動被替換成ls -l | more. 這使用戶在命令行不必鍵冗長的命令序列也避免了記憶複雜的命
令及衆多選項. 設置alias rm="rm -i" (交互式刪除)可以使你犯下錯誤時不必過度悲傷,它
能避免你不小心刪除重要文件.
http://www.818198.com  Page 401
SHELL十三問
在腳本里,別名機制不是非常的有用. 如果把別名機制想像成C預處理器的某些功能將會非常
好,比如宏擴展,但是,不幸的是Bash不能在別名中擴展參數. [1] 而且,別名不能在“混合
型的結構”中使用,比如if/then語句, 循環, 和函數. 還有一個限制是別名不能遞歸地擴展.
大多數情況Almost invariably, 我們想讓別名完成的工作都能被函數更高效地完成.
Example 24-1 腳本中的別名
################################Start Script#######################################
 1 #!/bin/bash
 2 # alias.sh
 3
 4 shopt -s expand_aliases
 5 # 必須設置這個選項,否則腳本不會擴展別名功能.
 6
 7
 8 # 首先, 來點有趣的.
 9 alias Jesse_James='echo "\"Alias Jesse James\" was a 1959 comedy starring Bob Hope."'
10 Jesse_James
11
12 echo; echo; echo;
13
14 alias ll="ls -l"
15 # 可以使用單引號(')或雙引號(")來定義一個別名.
16
17 echo "Trying aliased \"ll\":"
18 ll /usr/X11R6/bin/mk*   #* 別名工作了.
19
20 echo
21
22 directory=/usr/X11R6/bin/
23 prefix=mk*  # 看通配符會不會引起麻煩.
24 echo "Variables \"directory\" + \"prefix\" = $directory$prefix"
25 echo
26
27 alias lll="ls -l $directory$prefix"
28
29 echo "Trying aliased \"lll\":"
30 lll         # 詳細列出在/usr/X11R6/bin目錄下所有以mk開頭的文件.
31 # 別名能處理連接變量 -- 包括通配符 -- o.k.
32
33
34
35
36 TRUE=1
37
38 echo
39
40 if [ TRUE ]
41 then
42   alias rr="ls -l"
http://www.818198.com  Page 402
SHELL十三問
43   echo "Trying aliased \"rr\" within if/then statement:"
44   rr /usr/X11R6/bin/mk*   #* 引起錯誤信息!
45   # 別名不能在混合結構中使用.
46   echo "However, previously expanded alias still recognized:"
47   ll /usr/X11R6/bin/mk*
48 fi 
49
50 echo
51
52 count=0
53 while [ $count -lt 3 ]
54 do
55   alias rrr="ls -l"
56   echo "Trying aliased \"rrr\" within \"while\" loop:"
57   rrr /usr/X11R6/bin/mk*   #* 在這兒,別名也不會擴展.
58                            #  alias.sh: line 57: rrr: command not found
59   let count+=1
60 done
61
62 echo; echo
63
64 alias xyz='cat $0'   # 腳本打印自身內容.
65                      # 注意是單引號(強引用).
66 xyz
67 #  雖然Bash的文檔它是不會工作的,但好像它是可以工作的.
68 #
69 #
70 #  然而,就像 Steve Jacobson指出,
71 #+ 參數"$0"立即擴展成了這個別名的聲明.
72
73 exit 0
################################End Script#########################################
unalias 命令刪除先前設置的別名.
Example 24-2 unalias: 設置和刪除別名
################################Start Script#######################################
 1 #!/bin/bash
 2 # unalias.sh
 3
 4 shopt -s expand_aliases  # 打開別名功能擴展.
 5
 6 alias llm='ls -al | more'
 7 llm
 8
 9 echo
10
11 unalias llm              # 刪除別名.
12 llm
13 # 引起錯誤信息,因爲'llm'已經不再有效了.
http://www.818198.com  Page 403
SHELL十三問
14
15 exit 0
################################End Script#########################################
 bash$ ./unalias.sh
 total 6
drwxrwxr-x    2 bozo     bozo         3072 Feb  6 14:04 .
drwxr-xr-x   40 bozo     bozo         2048 Feb  6 14:04 ..
-rwxr-xr-x    1 bozo     bozo          199 Feb  6 14:04 unalias.sh
./unalias.sh: llm: command not found
注意事項:
[1]  但是, 別名好像能擴展位置參數.
第25章 列表結構
================
"與列表(and list)"和"或列表(or list)" 結構提供一種處理一串連續命令的方法. 它們能有
效地替代複雜的嵌套if/then語句甚至可以代替case語句.
連接命令
與列表(and list)
    1 command-1 && command-2 && command-3 && ... command-n
 如果每個命令都返回真值(0)將會依次執行下去. 當某個命令返回假值(非零值), 整個命
 令鏈就會結束執行(第一個返回假的命令將會是最後一個執行的命令,後面的都不再執行).
Example 25-1 使用"與列表(and list)"來測試命令行參數
################################Start Script#######################################
 1 #!/bin/bash
 2 # "and list"
 3
 4 if [ ! -z "$1" ] && echo "Argument #1 = $1" && [ ! -z "$2" ] && echo "Argument #2 = $2"
 5 then
 6   echo "At least 2 arguments passed to script."
 7   # 所有連接起來的命令都返回真.
 8 else
 9   echo "Less than 2 arguments passed to script."
10   # 整個命令列表中至少有一個命令返回假值.
11 fi 
12 # 注意"if [ ! -z $1 ]" 可以工作,但它是有所假定的等價物,
13 #   if [ -n $1 ] 不會工作.
14 #     但是, 加引用可以讓它工作.
15 #  if [ -n "$1" ] 就可以了.
16 #     小心!
17 # 最好總是引起要測試的變量.
18
19
20 # 這是使用"純粹"的 if/then 語句完成的同等功能.
21 if [ ! -z "$1" ]
22 then
23   echo "Argument #1 = $1"
24 fi
25 if [ ! -z "$2" ]
26 then
http://www.818198.com  Page 404
SHELL十三問
27   echo "Argument #2 = $2"
28   echo "At least 2 arguments passed to script."
29 else
30   echo "Less than 2 arguments passed to script."
31 fi
32 # 這會更長且不如"與列表"精緻.
33
34
35 exit 0
################################End Script#########################################
Example 25-2 用"與列表"的另一個命令行參數測試
################################Start Script#######################################
 1 #!/bin/bash
 2
 3 ARGS=1        # 期望的參數個數.
 4 E_BADARGS=65  # 如果用戶給出不正確的參數個數的退出碼.
 5
 6 test $# -ne $ARGS && echo "Usage: `basename $0` $ARGS argument(s)" && exit $E_BADARGS
 7 #  如果 條件1 測試爲真(表示傳給腳本的參數不對),
 8 #+ 則餘下的命令會被執行,並且腳本結束運行.
 9
10 # 下面的代碼只有當上面的測試失敗時纔會執行.
11 echo "Correct number of arguments passed to this script."
12
13 exit 0
14
15 # 爲了檢查退出碼,腳本結束後用"echo $?"來查看退出碼.
################################End Script#########################################
 當然,一個與列表也能給變量設置默認值.
    1 arg1=$@       # 不管怎樣,設置變量$arg1爲命令行參數.
    2
    3 [ -z "$arg1" ] && arg1=DEFAULT
    4               # 如果沒有在命令行上指定參數則把$arg1設置爲DEFAULT.
或列表(or list)
    1 command-1 || command-2 || command-3 || ... command-n
 只要前一個命令返回假命令鏈就會依次執行下去. 一旦有一個命令返回真, 命令鏈就會結
 束(第一個返回真的命令將會是最後一個執行的命令). 這顯然和"與列表"正好相反.
Example 25-3 "或列表"和"與列表"的結合使用
################################Start Script#######################################
 1 #!/bin/bash
 2
 3 #  delete.sh, 不是很聰明的文件刪除功能.
 4 #  用法: delete filename
 5
 6 E_BADARGS=65
 7
 8 if [ -z "$1" ]
 9 then
http://www.818198.com  Page 405
SHELL十三問
10   echo "Usage: `basename $0` filename"
11   exit $E_BADARGS  # 沒有參數? 跳出腳本.
12 else 
13   file=$1          # 設置文件名.
14 fi 
15
16
17 [ ! -f "$file" ] && echo "File \"$file\" not found. \
18 Cowardly refusing to delete a nonexistent file."
19 # 與列表, 用於文件不存在時給出一個錯誤信息.
20 # 注意 echo 命令的參數用了一個轉義符繼續使第二行也是這個命令的參數.
21
22 [ ! -f "$file" ] || (rm -f $file; echo "File \"$file\" deleted.")
23 # 或列表, 用於存在文件時刪除此文件.
24
25 # 注意上面兩個相反的邏輯.
26 # 與列表爲真時才執行, 或列表爲假時執行.
27
28 exit 0
################################End Script#########################################
 注意: 如果在與列表的第一個命令返回真時,它會執行.
   1 # ==> 下面的片斷摘自Miquel van Smoorenburg寫的 /etc/rc.d/init.d/single 腳本
   2 #+==> 示例與和或列表的使用.
   3 # ==> "箭頭"的註釋由本書作者添加.
   4
   5 [ -x /usr/bin/clear ] && /usr/bin/clear
   6   # ==> 如果 /usr/bin/clear 存在, 則調用它.
   7   # ==> 在調用一個命令前檢查它是否存在,
   8   #+==> 以避免產生錯誤信息和其他難讀懂的結果.
   9
  10   # ==> . . .
  11
  12 # 如果他們想在單用戶模式下運行某些程序, 可能也會運行這個...
  13 for i in /etc/rc1.d/S[0-9][0-9]* ; do
  14         # 檢查腳本是否可執行.
  15         [ -x "$i" ] || continue
  16   # ==> 如果在目錄$PWD中相應的文件沒有發現,
  17   #+==> 則會跳過此次循環.
  18
  19         # 不接受備份文件和由rpm產生的文件.
  20         case "$1" in
  21                 *.rpmsave|*.rpmorig|*.rpmnew|*~|*.orig)
  22                         continue;;
  23         esac
  24         [ "$i" = "/etc/rc1.d/S00single" ] && continue
  25   # ==> 設置腳本名,但還不執行它.
  26         $i start
  27 done
http://www.818198.com  Page 406
SHELL十三問
  28
  29   # ==> . . .
注意: 與列表或是或列表的退出狀態是最後一個執行命令的退出狀態.
靈活地組合"與"和"或"列表是允許的,但這樣邏輯會很容易變得費解並且需要較多的測試.
   1 false && true || echo false         # false
   2
   3 # 結果等同
   4 ( false && true ) || echo false     # false
   5 # 但不同與
   6 false && ( true || echo false )     # (沒有輸出)
   7
   8 #  注意是從左到右來分組並求值的,
   9 #+ 因爲邏輯操作符"&&"和"||"有相同的優先處理權.
  10
  11 #  最好避免這種複雜,除非你確實知道你在做什麼.
  12
  13 #  Thanks, S.C.
參考例子 A-7和例子 7-4 演示的使用與/或列表測試變量的例子.
第26章 數組
============
較新的Bash版本支持一維數組. 數組元素可以用符號variable[xx]來初始化. 另外,腳本可以
用declare -a variable語句來清楚地指定一個數組. 要訪問一個數組元素,可以使用花括號
來訪問,即${variable[xx]}.
Example 26-1 簡單的數組用法
################################Start Script#######################################
 1 #!/bin/bash
 2
 3
 4 area[11]=23
 5 area[13]=37
 6 area[51]=UFOs
 7
 8 #  數組成員不必一定要連貫或連續的.
 9
10 #  數組的一部分成員允許不被初始化.
11 #  數組中空缺元素是允許的.
12 #  實際上,保存着稀疏數據的數組(“稀疏數組”)在電子表格處理軟件中非常有用.
13 #
14
15
16 echo -n "area[11] = "
17 echo ${area[11]}    #  {大括號}是需要的.
18
19 echo -n "area[13] = "
20 echo ${area[13]}
21
22 echo "Contents of area[51] are ${area[51]}."
23
http://www.818198.com  Page 407
SHELL十三問
24 # 沒有初始化內容的數組元素打印空值(NULL值).
25 echo -n "area[43] = "
26 echo ${area[43]}
27 echo "(area[43] unassigned)"
28
29 echo
30
31 # 兩個數組元素的和被賦值給另一個數組元素
32 area[5]=`expr ${area[11]} + ${area[13]}`
33 echo "area[5] = area[11] + area[13]"
34 echo -n "area[5] = "
35 echo ${area[5]}
36
37 area[6]=`expr ${area[11]} + ${area[51]}`
38 echo "area[6] = area[11] + area[51]"
39 echo -n "area[6] = "
40 echo ${area[6]}
41 # 這裏會失敗是因爲整數和字符串相加是不允許的.
42
43 echo; echo; echo
44
45 # -----------------------------------------------------------------46 # 另一個數組, "area2".
47 # 另一種指定數組元素的值的辦法...
48 # array_name=( XXX YYY ZZZ ... )
49
50 area2=( zero one two three four )
51
52 echo -n "area2[0] = "
53 echo ${area2[0]}
54 # 啊哈, 從0開始計數(即數組的第一個元素是[0], 而不是 [1]).
55
56 echo -n "area2[1] = "
57 echo ${area2[1]}    # [1] 是數組的第二個元素.
58 # -----------------------------------------------------------------59
60 echo; echo; echo
61
62 # -----------------------------------------------63 # 第三種數組, "area3".
64 # 第三種指定數組元素值的辦法...
65 # array_name=([xx]=XXX [yy]=YYY ...)
66
67 area3=([17]=seventeen [24]=twenty-four)
68
69 echo -n "area3[17] = "
70 echo ${area3[17]}
71
http://www.818198.com  Page 408
SHELL十三問
72 echo -n "area3[24] = "
73 echo ${area3[24]}
74 # -----------------------------------------------75
76 exit 0
################################End Script#########################################
注意: Bash 允許把變量當成數組來操作,即使這個變量沒有明確地被聲明爲數組.
     1 string=abcABC123ABCabc
     2 echo ${string[@]}               # abcABC123ABCabc
     3 echo ${string[*]}               # abcABC123ABCabc
     4 echo ${string[0]}               # abcABC123ABCabc
     5 echo ${string[1]}               # 沒有輸出!
     6                                 # 爲什麼?
     7 echo ${#string[@]}              # 1
     8                                 # 數組中只有一個元素.
     9                                 # 且是這個字符串本身.
    10
    11 # Thank you, Michael Zick, for pointing this out.
  類似的示範請參考Bash variables are untyped.
Example 26-2 格式化一首詩
################################Start Script#######################################
 1 #!/bin/bash
 2 # poem.sh: 排印出作者喜歡的一首詩.
 3
 4 # 詩的行數 (一小節詩).
 5 Line[1]="I do not know which to prefer,"
 6 Line[2]="The beauty of inflections"
 7 Line[3]="Or the beauty of innuendoes,"
 8 Line[4]="The blackbird whistling"
 9 Line[5]="Or just after."
10
11 # 出處.
12 Attrib[1]=" Wallace Stevens"
13 Attrib[2]="\"Thirteen Ways of Looking at a Blackbird\""
14 # 此詩是公衆的 (版權期已經到期了).
15
16 echo
17
18 for index in 1 2 3 4 5    # 5行.
19 do
20   printf "     %s\n" "${Line[index]}"
21 done
22
23 for index in 1 2          # 打印兩行出處行.
24 do
25   printf "          %s\n" "${Attrib[index]}"
26 done
27
http://www.818198.com  Page 409
SHELL十三問
28 echo
29
30 exit 0
31
32 # 練習:
33 # --------34 # 修改這個腳本使其從一個文本文件中提取內容打印一首行.
################################End Script#########################################
數組元素有它們獨有的語法, 並且甚至Bash命令和操作符有特殊的選項可以支持數組使用.
Example 26-3 多種數組操作
################################Start Script#######################################
 1 #!/bin/bash
 2 # array-ops.sh: 數組更多有趣的用法.
 3
 4
 5 array=( zero one two three four five )
 6 # 元素    0   1   2    3     4    5
 7
 8 echo ${array[0]}       #  zero
 9 echo ${array:0}        #  zero
10                        #  第一個元素的參數擴展,
11                        #+ 從位置0開始 (即第一個字符).
12 echo ${array:1}        #  ero
13                        #  第一個元素的參數擴展,
14                        #+ 從位置1開始 (即第二個字符).
15
16 echo "--------------"
17
18 echo ${#array[0]}      #  4
19                        #  數組第一個元素的長度.
20 echo ${#array}         #  4
21                        #  數組第一個元素的長度.
22                        #  (另一種寫法)
23
24 echo ${#array[1]}      #  3
25                        #  數組第二個元素的長度.
26                        #  Bash的數組是0開始索引的.
27
28 echo ${#array[*]}      #  6
29                        #  數組中元素的個數.
30 echo ${#array[@]}      #  6
31                        #  數組中元素的個數.
32
33 echo "--------------"
34
35 array2=( [0]="first element" [1]="second element" [3]="fourth element" )
36
37 echo ${array2[0]}      # 第一個元素
http://www.818198.com  Page 410
SHELL十三問
38 echo ${array2[1]}      # 第二個元素
39 echo ${array2[2]}      #
40                        # 因爲初始化時沒有指定,因此值爲空(null).
41 echo ${array2[3]}      # 第四個元素
42
43
44 exit 0
################################End Script#########################################
大部分標準的字符串操作符 可以用於數組操作.
Example 26-4 用於數組的字符串操作符
################################Start Script#######################################
  1 #!/bin/bash
  2 # array-strops.sh: 用於數組的字符串操作符.
  3 # 由Michael Zick編碼.
  4 # 已徵得作者的同意.
  5
  6 #  一般來說,任何類似 ${name ... } 寫法的字符串操作符
  7 #+ 都能在一個數組的所有字符串元素中使用
  8 #+ 像${name[@] ... } 或 ${name[*] ...} 的寫法.
  9
 10
 11 arrayZ=( one two three four five five )
 12
 13 echo
 14
 15 # 提取尾部的子串
 16 echo ${arrayZ[@]:0}     # one two three four five five
 17                         # 所有的元素.
 18
 19 echo ${arrayZ[@]:1}     # two three four five five
 20                         # 在第一個元素 element[0]後面的所有元素.
 21
 22 echo ${arrayZ[@]:1:2}   # two three
 23                         # 只提取在元素 element[0]後面的兩個元素.
 24
 25 echo "-----------------------"
 26
 27 #  子串刪除
 28 #  從字符串的前部刪除最短的匹配,
 29 #+ 匹配字串是一個正則表達式.
 30
 31 echo ${arrayZ[@]#f*r}   # one two three five five
 32                         # 匹配表達式作用於數組所有元素.
 33                         # 匹配了"four"並把它刪除.
 34
 35 # 字符串前部最長的匹配
 36 echo ${arrayZ[@]##t*e}  # one two four five five
 37                         # 匹配表達式作用於數組所有元素.
http://www.818198.com  Page 411
SHELL十三問
 38                         # 匹配"three"並把它刪除.
 39
 40 # 字符串尾部的最短匹配
 41 echo ${arrayZ[@]%h*e}   # one two t four five five
 42                         # 匹配表達式作用於數組所有元素.
 43                         # 匹配"hree"並把它刪除.
 44
 45 # 字符串尾部的最長匹配
 46 echo ${arrayZ[@]%%t*e}  # one two four five five
 47                         # 匹配表達式作用於數組所有元素.
 48                         # 匹配"three"並把它刪除.
 49
 50 echo "-----------------------"
 51
 52 # 子串替換
 53
 54 # 第一個匹配的子串會被替換
 55 echo ${arrayZ[@]/fiv/XYZ}   # one two three four XYZe XYZe
 56                             # 匹配表達式作用於數組所有元素.
 57
 58 # 所有匹配的子串會被替換
 59 echo ${arrayZ[@]//iv/YY}    # one two three four fYYe fYYe
 60                             # 匹配表達式作用於數組所有元素.
 61
 62 # 刪除所有的匹配子串
 63 # 沒有指定代替字串意味着刪除
 64 echo ${arrayZ[@]//fi/}      # one two three four ve ve
 65                             # 匹配表達式作用於數組所有元素.
 66
 67 # 替換最前部出現的字串
 68 echo ${arrayZ[@]/#fi/XY}    # one two three four XYve XYve
 69                             # 匹配表達式作用於數組所有元素.
 70
 71 # 替換最後部出現的字串
 72 echo ${arrayZ[@]/%ve/ZZ}    # one two three four fiZZ fiZZ
 73                             # 匹配表達式作用於數組所有元素.
 74
 75 echo ${arrayZ[@]/%o/XX}     # one twXX three four five five
 76                             # 爲什麼?
 77
 78 echo "-----------------------"
 79
 80
 81 # 在從awk(或其他的工具)取得數據之前 -- 82 # 記得:
 83 #   $( ... ) 是命令替換.
 84 #   函數以子進程運行.
 85 #   函數將輸出打印到標準輸出.
http://www.818198.com  Page 412
SHELL十三問
 86 #   用read來讀取函數的標準輸出.
 87 #   name[@]的寫法指定了一個"for-each"的操作.
 88
 89 newstr() {
 90     echo -n "!!!"
 91 }
 92
 93 echo ${arrayZ[@]/%e/$(newstr)}
 94 # on!!! two thre!!! four fiv!!! fiv!!!
 95 # Q.E.D: 替換部分的動作實際上是一個'賦值'.
 96
 97 #  使用"For-Each"型的
 98 echo ${arrayZ[@]//*/$(newstr optional_arguments)}
 99 #  現在Now, 如果if Bash只傳遞匹配$0的字符串給要調用的函數. . .
100 #
101
102 echo
103
104 exit 0
################################End Script#########################################
命令替換能創建數組的新的單個元素.
Example 26-5 將腳本的內容傳給數組
################################Start Script#######################################
 1 #!/bin/bash
 2 # script-array.sh: 把此腳本的內容傳進數組.
 3 # 從Chris Martin的e-mail中得到靈感 (多謝!).
 4
 5 script_contents=( $(cat "$0") )  #  把這個腳本($0)的內容存進數組.
 6                                  #
 7
 8 for element in $(seq 0 $((${#script_contents[@]} - 1)))
 9   do                #  ${#script_contents[@]}
10                     #+ 表示數組中元素的個數.
11                     #
12                     #  問題:
13                     #  爲什麼需要  seq 0  ?
14                     #  試試更改成 seq 1.
15   echo -n "${script_contents[$element]}"
16                     # 將腳本的每行列成一個域.
17   echo -n " -- "    # 使用" -- "作爲域分隔符.
18 done
19
20 echo
21
22 exit 0
23
24 # 練習:
25 # --------http://www.818198.com  Page 413
SHELL十三問
26 #  修改這個腳本使它能按照它原本的格式輸出,
27 #+ 連同空白符,換行,等等.
28 #
################################End Script#########################################
在數組的環境裏, 一些 Bash 內建的命令 含義有一些輕微的改變. 例如, unset 會刪除數組
元素, 或甚至刪除整個數組.
Example 26-6 一些數組專用的工具
################################Start Script#######################################
 1 #!/bin/bash
 2
 3 declare -a colors
 4 #  所有腳本後面的命令都會把
 5 #+ 變量"colors"作爲數組對待.
 6
 7 echo "Enter your favorite colors (separated from each other by a space)."
 8
 9 read -a colors    # 鍵入至少3種顏色以用於下面的示例.
10 #  指定'read'命令的選項,
11 #+ 允許指定數組元素.
12
13 echo
14
15 element_count=${#colors[@]}
16 # 專用語法來提取數組元素的個數.
17 #     element_count=${#colors[*]} 也可以.
18 #
19 #  "@"變量允許分割引號內的單詞
20 #+ (依靠空白字符來分隔變量).
21 #
22 #  這就像"$@" 和"$*"在位置參數中表現出來的一樣.
23 #
24
25 index=0
26
27 while [ "$index" -lt "$element_count" ]
28 do    # List all the elements in the array.
29   echo ${colors[$index]}
30   let "index = $index + 1"
31 done
32 # 每個數組元素被列爲單獨的一行.
33 # 如果這個沒有要求, 可以用  echo -n "${colors[$index]} "
34 #
35 # 可以用一個"for"循環來做:
36 #   for i in "${colors[@]}"
37 #   do
38 #     echo "$i"
39 #   done
40 # (Thanks, S.C.)
http://www.818198.com  Page 414
SHELL十三問
41
42 echo
43
44 # 再次列出數組中所有的元素, 但使用更優雅的做法.
45   echo ${colors[@]}          # echo ${colors[*]} 也可以.
46
47 echo
48
49 # "unset"命令刪除一個數組元素或是整個數組.
50 unset colors[1]              # 刪除數組的第二個元素.
51                              # 作用等同於   colors[1]=
52 echo  ${colors[@]}           # 再列出數組,第二個元素沒有了.
53
54 unset colors                 # 刪除整個數組.
55                              #  unset colors[*] 或
56                              #+ unset colors[@] 都可以.
57 echo; echo -n "Colors gone."     
58 echo ${colors[@]}            # 再列出數組, 則爲空了.
59
60 exit 0
################################End Script#########################################
正如在前面的例子中看到的, ${array_name[@]}和${array_name[*]} 都與數組的所有元素相
關. 同樣地, 爲了計算數組的元素個數, 可以用${#array_name[@]} 或${#array_name[*]}.
${#array_name} 是數組第一個元素${array_name[0]}的長度(字符數) .
Example 26-7 關於空數組和空數組元素
################################Start Script#######################################
  1 #!/bin/bash
  2 # empty-array.sh
  3
  4 #  多謝 Stephane Chazelas 製作這個例子最初的版本,
  5 #+ 並由 Michael Zick 擴展了.
  6
  7
  8 # 空數組不同與含有空值元素的數組.
  9
 10 array0=( first second third )
 11 array1=( '' )   # "array1" 由一個空元素組成.
 12 array2=( )      # 沒有元素 . . . "array2" 是空的.
 13
 14 echo
 15 ListArray()
 16 {
 17 echo
 18 echo "Elements in array0:  ${array0[@]}"
 19 echo "Elements in array1:  ${array1[@]}"
 20 echo "Elements in array2:  ${array2[@]}"
 21 echo
 22 echo "Length of first element in array0 = ${#array0}"
http://www.818198.com  Page 415
SHELL十三問
 23 echo "Length of first element in array1 = ${#array1}"
 24 echo "Length of first element in array2 = ${#array2}"
 25 echo
 26 echo "Number of elements in array0 = ${#array0[*]}"  # 3
 27 echo "Number of elements in array1 = ${#array1[*]}"  # 1  (驚奇!)
 28 echo "Number of elements in array2 = ${#array2[*]}"  # 0
 29 }
 30
 31 # ===================================================================
 32
 33 ListArray
 34
 35 # 嘗試擴展這些數組.
 36
 37 # 增加一個元素到數組.
 38 array0=( "${array0[@]}" "new1" )
 39 array1=( "${array1[@]}" "new1" )
 40 array2=( "${array2[@]}" "new1" )
 41
 42 ListArray
 43
 44 # 或
 45 array0[${#array0[*]}]="new2"
 46 array1[${#array1[*]}]="new2"
 47 array2[${#array2[*]}]="new2"
 48
 49 ListArray
 50
 51 # 當像上面的做法增加數組時,數組像 '棧'
 52 # 上面的做法是 'push(壓棧)'
 53 # 棧高是:
 54 height=${#array2[@]}
 55 echo
 56 echo "Stack height for array2 = $height"
 57
 58 # 'pop(出棧)' 是:
 59 unset array2[${#array2[@]}-1]   # 數組是以0開始索引的,
 60 height=${#array2[@]}            #+ 這就意味着第一個元素下標是 0.
 61 echo
 62 echo "POP"
 63 echo "New stack height for array2 = $height"
 64
 65 ListArray
 66
 67 # 只列出數組array0的第二和第三個元素.
 68 from=1   #是以0開始的數字
 69 to=2  #
 70 array3=( ${array0[@]:1:2} )
http://www.818198.com  Page 416
SHELL十三問
 71 echo
 72 echo "Elements in array3:  ${array3[@]}"
 73
 74 # 像一個字符串一樣處理(字符的數組).
 75 # 試試其他的字符串格式.
 76
 77 # 替換:
 78 array4=( ${array0[@]/second/2nd} )
 79 echo
 80 echo "Elements in array4:  ${array4[@]}"
 81
 82 # 替換所有匹配通配符的字符串.
 83 array5=( ${array0[@]//new?/old} )
 84 echo
 85 echo "Elements in array5:  ${array5[@]}"
 86
 87 # 當你開始覺得對此有把握的時候 . . .
 88 array6=( ${array0[@]#*new} )
 89 echo # 這個可能會使你感到驚奇.
 90 echo "Elements in array6:  ${array6[@]}"
 91
 92 array7=( ${array0[@]#new1} )
 93 echo # 數組array6之後就沒有驚奇了.
 94 echo "Elements in array7:  ${array7[@]}"
 95
 96 # 這看起來非常像 . . .
 97 array8=( ${array0[@]/new1/} )
 98 echo
 99 echo "Elements in array8:  ${array8[@]}"
100
101 #  那麼我們怎麼總結它呢So what can one say about this?
102
103 #  字符串操作在數組var[@]的每一個元素中執行.
104 #
105 #  因此Therefore : 如果結果是一個零長度的字符串,
106 #+ Bash支持字符串向量操作,
107 #+ 元素會在結果賦值中消失不見.
108
109 #  提問, 這些字符串是強還是弱引用?
110
111 zap='new*'
112 array9=( ${array0[@]/$zap/} )
113 echo
114 echo "Elements in array9:  ${array9[@]}"
115
116 # 當你還在想你在Kansas州的何處時 . . .
117 array10=( ${array0[@]#$zap} )
118 echo
http://www.818198.com  Page 417
SHELL十三問
119 echo "Elements in array10:  ${array10[@]}"
120
121 # 把 array7 和 array10比較.
122 # 把 array8 和 array9比較.
123
124 # 答案: 必須用弱引用.
125
126 exit 0
################################End Script#########################################
${array_name[@]}和${array_name[*]} 的關係類似於$@ and $*. 這種數組用法非常有用.
   1 # 複製一個數組.
   2 array2=( "${array1[@]}" )
   3 # 或
   4 array2="${array1[@]}"
   5
   6 # 給數組增加一個元素.
   7 array=( "${array[@]}" "new element" )
   8 # 或
   9 array[${#array[*]}]="new element"
  10
  11 # Thanks, S.C.
注意: array=( element1 element2 ... elementN ) 初始化操作, 依賴於命令替換
  (command substitution)使將一個文本內容加載進數組成爲可能.
   1 #!/bin/bash
   2
   3 filename=sample_file
   4
   5 #            cat sample_file
   6 #
   7 #            1 a b c
   8 #            2 d e fg
   9
  10
  11 declare -a array1
  12
  13 array1=( `cat "$filename"`)                #  加載$filename文件的內容進數組array1.
  14 #         打印文件到標準輸出               #
  15 #
  16 #  array1=( `cat "$filename" | tr '\n' ' '`)
  17 #                            把文件裏的換行變爲空格.
  18 #  這是沒必要的,因爲Bash做單詞分割時會把換行變爲空格.
  19 #
  20
  21 echo ${array1[@]}            # 打印數組.
  22 #                              1 a b c 2 d e fg
  23 #
  24 #  文件中每個由空白符分隔開的“詞”都被存在數組的一個元素裏
  25 #
http://www.818198.com  Page 418
SHELL十三問
  26
  27 element_count=${#array1[*]}
  28 echo $element_count          # 8
出色的技巧使數組的操作技術又多了一種.
Example 26-8 初始化數組
################################Start Script#######################################
 1 #! /bin/bash
 2 # array-assign.bash
 3
 4 #  數組操作是Bash特有的,
 5 #+ 因此腳本名用".bash"結尾.
 6
 7 # Copyright (c) Michael S. Zick, 2003, All rights reserved.
 8 # 許可證: 沒有任何限制,可以用於任何目的的反覆使用.
 9 # Version: $ID$
10 #
11 # 由William Park添加註釋.
12
13 #  基於Stephane Chazelas提供在本書中的一個例子
14 #
15
16 # 'times' 命令的輸出格式:
17 # User CPU <空格> System CPU
18 # User CPU of dead children <空格> System CPU of dead children
19
20 #  Bash賦一個數組的所有元素給新的數組變量有兩種辦法.
21 #
22 #  在Bash版本2.04, 2.05a 和 2.05b,
23 #+ 這兩種辦法都對NULL的值的元素全部丟棄.
24 #  另一種數組賦值辦法是維護[下標]=值之間的關係將會在新版本的Bash支持.
25 #
26
27 #  可以用外部命令來構造一個大數組,
28 #+ 但幾千個元素的數組如下就可以構造了.
29 #
30
31 declare -a bigOne=( /dev/* )
32 echo
33 echo 'Conditions: Unquoted, default IFS, All-Elements-Of'
34 echo "Number of elements in array is ${#bigOne[@]}"
35
36 # set -vx
37
38
39
40 echo
41 echo '- - testing: =( ${array[@]} ) - -'
42 times
http://www.818198.com  Page 419
SHELL十三問
43 declare -a bigTwo=( ${bigOne[@]} )
44 #                 ^              ^
45 times
46
47 echo
48 echo '- - testing: =${array[@]} - -'
49 times
50 declare -a bigThree=${bigOne[@]}
51 # 這次沒有用括號.
52 times
53
54 #  正如Stephane Chazelas指出的那樣比較輸出的數組可以瞭解第二種格式的賦值比第三和第四的times的更快
55 #
56 #
57 #  William Park 解釋explains:
58 #+ bigTwo 數組是被賦值了一個單字符串,
59 #+ bigThree 則賦值時一個一個元素的賦值.
60 #  所以, 實際上的情況是:
61 #                   bigTwo=( [0]="... ... ..." )
62 #                   bigThree=( [0]="..." [1]="..." [2]="..." ... )
63
64
65 #  我在本書的例子中仍然會繼續用第一種格式,
66 #+ 因爲我認爲這會對說明清楚更有幫助.
67
68 #  我的例子中的可複用的部分實際上還是會使用第二種格式,
69 #+ 因爲這種格式更快一些.
70
71 # MSZ: 很抱歉早先的失誤(應是指本書的先前版本).
72
73
74 #  注:
75 #  ----76 #  在31和43行的"declare -a"語句不是必須的,
77 #+ 因爲會在使用Array=( ... )賦值格式時暗示它是數組.
78 #
79 #  但是, 省略這些聲明會導致後面腳本的相關操作更慢一些.
80 #
81 #  試一下, 看有什麼變化.
82
83 exit 0
################################End Script#########################################
注意: 對變量增加 declare -a  語句聲明可以加速後面的數組操作速度.
Example 26-9 複製和連接數組
################################Start Script#######################################
 1 #! /bin/bash
 2 # CopyArray.sh
 3 #
http://www.818198.com  Page 420
SHELL十三問
 4 # 由 Michael Zick編寫.
 5 # 在本書中使用已得到許可.
 6
 7 #  怎麼傳遞變量名和值處理,返回就用使用該變量,
 8 #+ 或說"創建你自己的賦值語句".
 9
10
11 CpArray_Mac() {
12
13 # 創建賦值命令語句
14
15     echo -n 'eval '
16     echo -n "$2"                    # 目的變量名
17     echo -n '=( ${'
18     echo -n "$1"                    # 源名字
19     echo -n '[@]} )'
20
21 # 上面的全部會合成單個命令.
22 # 這就是函數所有的功能.
23 }
24
25 declare -f CopyArray                # 函數"指針"
26 CopyArray=CpArray_Mac               # 建立命令
27
28 Hype()
29 {
30
31 # 要複製的數組名爲 $1.
32 # (接合數組,幷包含尾部的字符串"Really Rocks".)
33 # 返回結果的數組名爲 $2.
34
35     local -a TMP
36     local -a hype=( Really Rocks )
37
38     $($CopyArray $1 TMP)
39     TMP=( ${TMP[@]} ${hype[@]} )
40     $($CopyArray TMP $2)
41 }
42
43 declare -a before=( Advanced Bash Scripting )
44 declare -a after
45
46 echo "Array Before = ${before[@]}"
47
48 Hype before after
49
50 echo "Array After = ${after[@]}"
51
http://www.818198.com  Page 421
SHELL十三問
52 # 有多餘的字符串?
53
54 echo "What ${after[@]:3:2}?"
55
56 declare -a modest=( ${after[@]:2:1} ${after[@]:3:2} )
57 #                    ----     子串提取       ----58
59 echo "Array Modest = ${modest[@]}"
60
61 # 'before'變量變成什麼了 ?
62
63 echo "Array Before = ${before[@]}"
64
65 exit 0
################################End Script#########################################
Example 26-10 關於連接數組的更多信息
################################Start Script#######################################
  1 #! /bin/bash
  2 # array-append.bash
  3
  4 # Copyright (c) Michael S. Zick, 2003, All rights reserved.
  5 # 許可: 可以無限制的以任何目的任何格式重複使用.
  6 # 版本: $ID$
  7 #
  8 # 格式上由M.C做了輕微的修改.
  9
 10
 11 # 數組操作是Bash特有的屬性.
 12 # 原來的 UNIX /bin/sh 沒有類似的功能.
 13
 14
 15 #  把此腳本的輸出管道輸送給 'more'
 16 #+ 以便輸出不會滾過終端屏幕.
 17
 18
 19 # 下標依次使用.
 20 declare -a array1=( zero1 one1 two1 )
 21 # 下標有未使用的 ([1] 沒有被定義).
 22 declare -a array2=( [0]=zero2 [2]=two2 [3]=three2 )
 23
 24 echo
 25 echo '- Confirm that the array is really subscript sparse. -'
 26 echo "Number of elements: 4"        # 這兒是舉例子就用硬編碼.
 27 for (( i = 0 ; i < 4 ; i++ ))
 28 do
 29     echo "Element [$i]: ${array2[$i]}"
 30 done
 31 # 也可以參考basics-reviewed.bash更多的常見代碼.
http://www.818198.com  Page 422
SHELL十三問
 32
 33
 34 declare -a dest
 35
 36 # 組合 (添加) 兩個數組到第三個數組.
 37 echo
 38 echo 'Conditions: Unquoted, default IFS, All-Elements-Of operator'
 39 echo '- Undefined elements not present, subscripts not maintained. -'
 40 # # 那些未定義的元素不存在; 組合時會丟棄這些元素.
 41
 42 dest=( ${array1[@]} ${array2[@]} )
 43 # dest=${array1[@]}${array2[@]}     # 奇怪的結果, 或者叫臭蟲.
 44
 45 # 現在, 打印出結果.
 46 echo
 47 echo '- - Testing Array Append - -'
 48 cnt=${#dest[@]}
 49
 50 echo "Number of elements: $cnt"
 51 for (( i = 0 ; i < cnt ; i++ ))
 52 do
 53     echo "Element [$i]: ${dest[$i]}"
 54 done
 55
 56 # 把一個數組賦值給另一個數組的單個元素 (兩次).
 57 dest[0]=${array1[@]}
 58 dest[1]=${array2[@]}
 59
 60 # 列出結果.
 61 echo
 62 echo '- - Testing modified array - -'
 63 cnt=${#dest[@]}
 64
 65 echo "Number of elements: $cnt"
 66 for (( i = 0 ; i < cnt ; i++ ))
 67 do
 68     echo "Element [$i]: ${dest[$i]}"
 69 done
 70
 71 # 檢測第二個元素的改變.
 72 echo
 73 echo '- - Reassign and list second element - -'
 74
 75 declare -a subArray=${dest[1]}
 76 cnt=${#subArray[@]}
 77
 78 echo "Number of elements: $cnt"
 79 for (( i = 0 ; i < cnt ; i++ ))
http://www.818198.com  Page 423
SHELL十三問
 80 do
 81     echo "Element [$i]: ${subArray[$i]}"
 82 done
 83
 84 #  用 '=${ ... }' 把整個數組的值賦給另一個數組的單個元素
 85 #+ 使數組所有元素值被轉換成了一個字符串,各元素的值由一個空格分開(其實是IFS的第一個字符).
 86 #
 87 #
 88
 89 # 如果原先的元素沒有包含空白符 . . .
 90 # 如果原先的數組下標都是連續的 . . .
 91 # 我們就能取回最初的數組結構.
 92
 93 # 恢復第二個元素的修改回元素.
 94 echo
 95 echo '- - Listing restored element - -'
 96
 97 declare -a subArray=( ${dest[1]} )
 98 cnt=${#subArray[@]}
 99
100 echo "Number of elements: $cnt"
101 for (( i = 0 ; i < cnt ; i++ ))
102 do
103     echo "Element [$i]: ${subArray[$i]}"
104 done
105 echo '- - Do not depend on this behavior. - -'
106 echo '- - This behavior is subject to change - -'
107 echo '- - in versions of Bash newer than version 2.05b - -'
108
109 # MSZ: 很抱歉早先時混淆的幾個要點(譯者注:應該是指本書早先的版本).
110
111 exit 0
################################End Script#########################################
--數組允許在腳本中實現一些常見的熟悉算法.這是否是必要的好想法在此不討論,留給讀者自
行判斷.
Example 26-11 一位老朋友: 冒泡排序
################################Start Script#######################################
 1 #!/bin/bash
 2 # bubble.sh: 排序法之冒泡排序.
 3
 4 # 回憶冒泡排序法. 在這個版本中要實現它...
 5
 6 #  靠連續地多次比較數組元素來排序,
 7 #+ 比較兩個相鄰的元素,如果排序順序不對,則交換兩者的順序.
 8 #  當第一輪比較結束後,最"重"的元素就被排到了最底部.
 9 #  當第二輪比較結束後,第二"重"的元素就被排到了次底部的位置.
10 #  以此類推.
http://www.818198.com  Page 424
SHELL十三問
11 #  這意味着每輪的比較不需要比較先前已"沉澱"好的數據.
12 #  因此你會注意到後面數據的打印會比較快一些.
13
14
15 exchange()
16 {
17   # 交換數組的兩個元素.
18   local temp=${Countries[$1]} #  臨時保存要交換的一個元素.
19                               #
20   Countries[$1]=${Countries[$2]}
21   Countries[$2]=$temp
22  
23   return
24 } 
25
26 declare -a Countries  #  聲明數組,
27                       #+ 在此是可選的,因爲下面它會被按數組來初始化.
28
29 #  是否允許用轉義符(\)將數組的各變量值放到幾行上?
30 #
31 #  是的.
32
33 Countries=(Netherlands Ukraine Zaire Turkey Russia Yemen Syria \
34 Brazil Argentina Nicaragua Japan Mexico Venezuela Greece England \
35 Israel Peru Canada Oman Denmark Wales France Kenya \
36 Xanadu Qatar Liechtenstein Hungary)
37
38 # "Xanadu" 是個虛擬的充滿美好的神話之地.
39 #
40
41
42 clear                      # 開始之前清除屏幕.
43
44 echo "0: ${Countries[*]}"  # 從0索引的元素開始列出整個數組.
45
46 number_of_elements=${#Countries[@]}
47 let "comparisons = $number_of_elements - 1"
48
49 count=1 # 傳遞數字.
50
51 while [ "$comparisons" -gt 0 ]          # 開始外部的循環
52 do
53
54   index=0  # 每輪開始前重設索引值爲0.
55
56   while [ "$index" -lt "$comparisons" ] # 開始內部循環
57   do
58     if [ ${Countries[$index]} \> ${Countries[`expr $index + 1`]} ]
http://www.818198.com  Page 425
SHELL十三問
59     #  如果原來的排序次序不對...
60     #  回想一下 \> 在單方括號裏是is ASCII 碼的比較操作符.
61     #
62
63     #  if [[ ${Countries[$index]} > ${Countries[`expr $index + 1`]} ]]
64     #+ 也可以.
65     then
66       exchange $index `expr $index + 1`  # 交換.
67     fi 
68     let "index += 1"
69   done # 內部循環結束
70
71 # ----------------------------------------------------------------------72 # Paulo Marcel Coelho Aragao 建議使用更簡單的for-loops.
73 #
74 # for (( last = $number_of_elements - 1 ; last > 1 ; last-- ))
75 # do
76 #     for (( i = 0 ; i < last ; i++ ))
77 #     do
78 #         [[ "${Countries[$i]}" > "${Countries[$((i+1))]}" ]] \
79 #             && exchange $i $((i+1))
80 #     done
81 # done
82 # ----------------------------------------------------------------------83  
84
85 let "comparisons -= 1" #  因爲最"重"的元素冒到了最底部,
86                        #+ 我們可以每輪少做一些比較.
87
88 echo
89 echo "$count: ${Countries[@]}"  # 每輪結束後,打印一次數組.
90 echo
91 let "count += 1"                # 增加傳遞計數.
92
93 done                            # 外部循環結束
94                                 # 完成.
95
96 exit 0
################################End Script#########################################
--在數組內嵌一個數組有可能做到嗎?
   1 #!/bin/bash
   2 # "內嵌" 數組.
   3
   4 #  Michael Zick 提供這個例子,
   5 #+ 由William Park作了些糾正和解釋.
   6
   7 AnArray=( $(ls --inode --ignore-backups --almost-all \
http://www.818198.com  Page 426
SHELL十三問
   8  --directory --full-time --color=none --time=status \
   9  --sort=time -l ${PWD} ) )  # 命令及選項.
  10
  11 # 空格是有意義的 . . . 不要在上面引號引用任何東西.
  12
  13 SubArray=( ${AnArray[@]:11:1}  ${AnArray[@]:6:5} )
  14 #  這個數組有6個元素:
  15 #+     SubArray=( [0]=${AnArray[11]} [1]=${AnArray[6]} [2]=${AnArray[7]}
  16 #      [3]=${AnArray[8]} [4]=${AnArray[9]} [5]=${AnArray[10]} )
  17 #
  18 #  Bash中的數組像是字符串(char *)型的(循環)鏈表.
  19 #
  20 #  因此, 這實際上不是內嵌的數組,
  21 #+ 但它的功能是相似的.
  22
  23 echo "Current directory and date of last status change:"
  24 echo "${SubArray[@]}"
  25
  26 exit 0
--內嵌數組和間接引用(indirect references) 的組合使用產生了一些有趣的用法.
Example 26-12 內嵌數組和間接引用
################################Start Script#######################################
 1 #!/bin/bash
 2 # embedded-arrays.sh
 3 # 內嵌數組和間接引用.
 4
 5 # 由Dennis Leeuw編寫.
 6 # 已獲使用許可.
 7 # 由本文作者修改.
 8
 9
10 ARRAY1=(
11         VAR1_1=value11
12         VAR1_2=value12
13         VAR1_3=value13
14 )
15
16 ARRAY2=(
17         VARIABLE="test"
18         STRING="VAR1=value1 VAR2=value2 VAR3=value3"
19         ARRAY21=${ARRAY1[*]}
20 )       # 把ARRAY1數組嵌到這個數組裏.
21
22 function print () {
23         OLD_IFS="$IFS"
24         IFS=$'\n'       #  這是爲了在每個行打印一個數組元素.
25                         #
http://www.818198.com  Page 427
SHELL十三問
26         TEST1="ARRAY2[*]"
27         local ${!TEST1} # 試下刪除這行會發生什麼.
28         #  間接引用.
29  #  這使 $TEST1只在函數內存取.
30  #
31
32
33         #  我們看看還能幹點什麼.
34         echo
35         echo "\$TEST1 = $TEST1"       #  變量的名稱.
36         echo; echo
37         echo "{\$TEST1} = ${!TEST1}"  #  變量的內容.
38                                       #  這就是間接引用的作用.
39                                       #
40         echo
41         echo "-------------------------------------------"; echo
42         echo
43
44
45         # 打印變量
46         echo "Variable VARIABLE: $VARIABLE"
47 
48         # 打印一個字符串元素
49         IFS="$OLD_IFS"
50         TEST2="STRING[*]"
51         local ${!TEST2}      # 間接引用 (像上面一樣).
52         echo "String element VAR2: $VAR2 from STRING"
53
54         # 打印一個字符串元素
55         TEST2="ARRAY21[*]"
56         local ${!TEST2}      # 間接引用 (像上面一樣).
57         echo "Array element VAR1_1: $VAR1_1 from ARRAY21"
58 }
59
60 print
61 echo
62
63 exit 0
64
65 #   腳本作者注,
66 #+ "你可以很容易地將其擴展成Bash的一個能創建hash的腳本."
67 #   (難) 留給讀者的練習: 實現它.
################################End Script#########################################
--數組使埃拉托色尼素數篩子有了shell腳本的實現. 當然, 如果是追求效率的應用自然應該用
一種編譯型的語言,例如用C. 這種腳本運行實在是太慢.
Example 26-13 複雜數組應用: 埃拉托色尼素數篩子
################################Start Script#######################################
http://www.818198.com  Page 428
SHELL十三問
  1 #!/bin/bash
  2 # sieve.sh (ex68.sh)
  3
  4 # 埃拉托色尼素數篩子
  5 # 找素數的經典算法.
  6
  7 #  在同等數量的數值內這個腳本比用C寫的版本慢很多.
  8 #
  9
 10 LOWER_LIMIT=1       # 從1開始.
 11 UPPER_LIMIT=1000    # 到 1000.
 12 # (如果你很有時間的話,你可以把它設得更高 . . . )
 13
 14 PRIME=1
 15 NON_PRIME=0
 16
 17 let SPLIT=UPPER_LIMIT/2
 18 # 優化:
 19 # 只需要測試中間到最大之間的值 (爲什麼?).
 20
 21
 22 declare -a Primes
 23 # Primes[] 是一個數組.
 24
 25
 26 initialize ()
 27 {
 28 # 初始化數組.
 29
 30 i=$LOWER_LIMIT
 31 until [ "$i" -gt "$UPPER_LIMIT" ]
 32 do
 33   Primes[i]=$PRIME
 34   let "i += 1"
 35 done
 36 #  假定所有的數組成員都是需要檢查的 (素數)
 37 #+ 一直到檢查完成前.
 38 }
 39
 40 print_primes ()
 41 {
 42 # 打印出所有Primes[]數組中被標記爲素數的元素.
 43
 44 i=$LOWER_LIMIT
 45
 46 until [ "$i" -gt "$UPPER_LIMIT" ]
 47 do
 48
http://www.818198.com  Page 429
SHELL十三問
 49   if [ "${Primes[i]}" -eq "$PRIME" ]
 50   then
 51     printf "%8d" $i
 52     # 每個數字打印前先打印8個空格, 數字是在偶數列打印的.
 53   fi
 54  
 55   let "i += 1"
 56  
 57 done
 58
 59 }
 60
 61 sift () # 查出非素數.
 62 {
 63
 64 let i=$LOWER_LIMIT+1
 65 # 我們都知道1是素數, 所以我們從2開始.
 66
 67 until [ "$i" -gt "$UPPER_LIMIT" ]
 68 do
 69
 70 if [ "${Primes[i]}" -eq "$PRIME" ]
 71 # 不要處理已經過濾過的數字 (被標識爲非素數).
 72 then
 73
 74   t=$i
 75
 76   while [ "$t" -le "$UPPER_LIMIT" ]
 77   do
 78     let "t += $i "
 79     Primes[t]=$NON_PRIME
 80     # 標識爲非素數.
 81   done
 82
 83 fi 
 84
 85   let "i += 1"
 86 done 
 87
 88
 89 }
 90
 91
 92 # ==============================================
 93 # main ()
 94 # 繼續調用函數.
 95 initialize
 96 sift
http://www.818198.com  Page 430
SHELL十三問
 97 print_primes
 98 # 這就是被稱爲結構化編程的東西了.
 99 # ==============================================
100
101 echo
102
103 exit 0
104
105
106
107 # -------------------------------------------------------- #
108 # 因爲前面的一個'exit',所以下面的代碼不會被執行.
109
110 #  下面是Stephane Chazelas寫的一個埃拉托色尼素數篩子的改進版本,
111 #+ 運行會稍微快一點.
112
113 # 必須在命令行上指定參數(尋找素數的限制範圍).
114
115 UPPER_LIMIT=$1                  # 值來自命令行.
116 let SPLIT=UPPER_LIMIT/2         # 從中間值到最大值.
117
118 Primes=( '' $(seq $UPPER_LIMIT) )
119
120 i=1
121 until (( ( i += 1 ) > SPLIT ))  # 僅需要從中間值檢查.
122 do
123   if [[ -n $Primes[i] ]]
124   then
125     t=$i
126     until (( ( t += i ) > UPPER_LIMIT ))
127     do
128       Primes[t]=
129     done
130   fi 
131 done 
132 echo ${Primes[*]}
133
134 exit 0
################################End Script#########################################
比較這個用數組的素數產生器和另一種不用數組的例子 A-16.
--
數組可以做一定程度的擴展,以模擬支持Bash原本不支持的數據結構.
Example 26-14 模擬下推的堆棧
################################Start Script#######################################
  1 #!/bin/bash
  2 # stack.sh: 下推的堆棧模擬
  3
  4 #  類似於CPU棧, 下推的堆棧依次保存數據項,
http://www.818198.com  Page 431
SHELL十三問
  5 #+ 但取出時則反序進行, 後進先出.
  6
  7 BP=100            #  棧數組的基點指針.
  8                   #  從元素100開始.
  9
 10 SP=$BP            #  棧指針.
 11                   #  初始化棧底.
 12
 13 Data=             #  當前棧的內容. 
 14                   #  必須定義成全局變量,
 15                   #+ 因爲函數的返回整數有範圍限制.
 16
 17 declare -a stack
 18
 19
 20 push()            # 把一個數據項壓入棧.
 21 {
 22 if [ -z "$1" ]    # 沒有可壓入的?
 23 then
 24   return
 25 fi
 26
 27 let "SP -= 1"     # 更新堆棧指針.
 28 stack[$SP]=$1
 29
 30 return
 31 }
 32
 33 pop()                    # 從棧中彈出一個數據項.
 34 {
 35 Data=                    # 清空保存數據項中間變量.
 36
 37 if [ "$SP" -eq "$BP" ]   # 已經沒有數據可彈出?
 38 then
 39   return
 40 fi                       #  這使SP不會超過100,
 41                          #+ 例如, 這可保護一個失控的堆棧.
 42
 43 Data=${stack[$SP]}
 44 let "SP += 1"            # 更新堆棧指針.
 45 return
 46 }
 47
 48 status_report()          # 打印堆棧的當前狀態.
 49 {
 50 echo "-------------------------------------"
 51 echo "REPORT"
 52 echo "Stack Pointer = $SP"
http://www.818198.com  Page 432
SHELL十三問
 53 echo "Just popped \""$Data"\" off the stack."
 54 echo "-------------------------------------"
 55 echo
 56 }
 57
 58
 59 # =======================================================
 60 # 現在,來點樂子.
 61
 62 echo
 63
 64 # 看你是否能從空棧裏彈出數據項來.
 65 pop
 66 status_report
 67
 68 echo
 69
 70 push garbage
 71 pop
 72 status_report     # 壓入garbage, 彈出garbage.     
 73
 74 value1=23; push $value1
 75 value2=skidoo; push $value2
 76 value3=FINAL; push $value3
 77
 78 pop              # FINAL
 79 status_report
 80 pop              # skidoo
 81 status_report
 82 pop              # 23
 83 status_report    # 後進, 先出!
 84
 85 #  注意堆棧指針每次壓棧時減,
 86 #+ 每次彈出時加一.
 87
 88 echo
 89
 90 exit 0
 91
 92 # =======================================================
 93
 94
 95 # 練習:
 96 # --------- 97
 98 # 1)  修改"push()"函數,使其調用一次就能夠壓入多個數據項.
 99 #
100
http://www.818198.com  Page 433
SHELL十三問
101 # 2)  修改"pop()"函數,使其調用一次就能彈出多個數據項.
102 #
103
104 # 3)  給那些有臨界操作的函數增加出錯檢查.
105 #     即是指是否一次完成操作或沒有完成操作返回相應的代碼,
106 #   + 沒有完成要啓動合適的處理動作.
107 #
108
109 # 4)  這個腳本爲基礎,
110 #   + 寫一個棧實現的四則運算計算器.
################################End Script#########################################
--
要想操作數組的下標需要中間變量. 如果確實要這麼做, 可以考慮使用一種更強功能的編程語
言, 例如 Perl 或 C.
Example 26-15 複雜的數組應用: 列出一種怪異的數學序列
################################Start Script#######################################
 1 #!/bin/bash
 2
 3 # Douglas Hofstadter的有名的"Q-series":
 4
 5 # Q(1) = Q(2) = 1
 6 # Q(n) = Q(n - Q(n-1)) + Q(n - Q(n-2)), 當 n>2 時
 7
 8 # 這是令人感到陌生的也是沒有規律的"亂序"整數序列.
 9 # 序列的頭20個如下所示:
10 # 1 1 2 3 3 4 5 5 6 6 6 8 8 8 10 9 10 11 11 12
11
12 #  參考Hofstadter的書, "Goedel, Escher, Bach: An Eternal Golden Braid",
13 #+ 頁碼 137.
14
15
16 LIMIT=100     # 計算數的個數.
17 LINEWIDTH=20  # 很行要打印的數的個數.
18
19 Q[1]=1        # 序列的頭2個是 1.
20 Q[2]=1
21
22 echo
23 echo "Q-series [$LIMIT terms]:"
24 echo -n "${Q[1]} "             # 打印頭2個數.
25 echo -n "${Q[2]} "
26
27 for ((n=3; n <= $LIMIT; n++))  # C風格的循環條件.
28 do   # Q[n] = Q[n - Q[n-1]] + Q[n - Q[n-2]]  當 n>2 時
29 #  需要將表達式分步計算,
30 #+ 因爲Bash不擅長處理此類複雜計算.
31
32   let "n1 = $n - 1"        # n-1
http://www.818198.com  Page 434
SHELL十三問
33   let "n2 = $n - 2"        # n-2
34  
35   t0=`expr $n - ${Q[n1]}`  # n - Q[n-1]
36   t1=`expr $n - ${Q[n2]}`  # n - Q[n-2]
37  
38   T0=${Q[t0]}              # Q[n - Q[n-1]]
39   T1=${Q[t1]}              # Q[n - Q[n-2]]
40
41 Q[n]=`expr $T0 + $T1`      # Q[n - Q[n-1]] + Q[n - Q[n-2]]
42 echo -n "${Q[n]} "
43
44 if [ `expr $n % $LINEWIDTH` -eq 0 ]    # 格式化輸出.
45 then   #      ^ 取模操作
46   echo # 把行分成內部的塊.
47 fi
48
49 done
50
51 echo
52
53 exit 0
54
55 # 這是Q-series問題的迭代實現.
56 # 更直接明瞭的遞歸實現留給讀者完成.
57 # 警告: 遞歸地計算這個序列會花很長的時間.
################################End Script#########################################
--
Bash 只支持一維數組,但有一些技巧可用來模擬多維數組.
Example 26-16 模擬二維數組,並使它傾斜
################################Start Script#######################################
  1 #!/bin/bash
  2 # twodim.sh: 模擬二維數組.
  3
  4 # 一維數組由單行組成.
  5 # 二維數組由連續的行組成.
  6
  7 Rows=5
  8 Columns=5
  9 # 5 X 5 的數組Array.
 10
 11 declare -a alpha     # char alpha [Rows] [Columns];
 12                      # 不必要的聲明. 爲什麼?
 13
 14 load_alpha ()
 15 {
 16 local rc=0
 17 local index
 18
http://www.818198.com  Page 435
SHELL十三問
 19 for i in A B C D E F G H I J K L M N O P Q R S T U V W X Y
 20 do     # 如果你高興,可以使用不同的符號.
 21   local row=`expr $rc / $Columns`
 22   local column=`expr $rc % $Rows`
 23   let "index = $row * $Rows + $column"
 24   alpha[$index]=$i
 25 # alpha[$row][$column]
 26   let "rc += 1"
 27 done 
 28
 29 #  更簡單的辦法
 30 #+   declare -a alpha=( A B C D E F G H I J K L M N O P Q R S T U V W X Y )
 31 #+ 但這就缺少了二維數組的感覺了.
 32 }
 33
 34 print_alpha ()
 35 {
 36 local row=0
 37 local index
 38
 39 echo
 40
 41 while [ "$row" -lt "$Rows" ]   #  以行順序爲索引打印行的各元素:
 42 do                             #+ 即數組列值變化快,
 43                                #+ 行值變化慢.
 44   local column=0
 45
 46   echo -n "       "            #  依行傾斜打印正方形的數組.
 47  
 48   while [ "$column" -lt "$Columns" ]
 49   do
 50     let "index = $row * $Rows + $column"
 51     echo -n "${alpha[index]} "  # alpha[$row][$column]
 52     let "column += 1"
 53   done
 54
 55   let "row += 1"
 56   echo
 57
 58 done 
 59
 60 # 等同於
 61 #     echo ${alpha[*]} | xargs -n $Columns
 62
 63 echo
 64 }
 65
 66 filter ()     # 過濾出負數的數組索引.
http://www.818198.com  Page 436
SHELL十三問
 67 {
 68
 69 echo -n "  "  # 產生傾斜角度.
 70               # 解釋怎麼辦到的.
 71
 72 if [[ "$1" -ge 0 &&  "$1" -lt "$Rows" && "$2" -ge 0 && "$2" -lt "$Columns" ]]
 73 then
 74     let "index = $1 * $Rows + $2"
 75     # Now, print it rotated現在,打印旋轉角度.
 76     echo -n " ${alpha[index]}"
 77     #           alpha[$row][$column]
 78 fi   
 79
 80 }
 81  
 82
 83
 84
 85 rotate ()  #  旋轉數組 45 度 -- 86 {          #+ 在左下角"平衡"圖形.
 87 local row
 88 local column
 89
 90 for (( row = Rows; row > -Rows; row-- ))
 91   do       # 從後面步進數組. 爲什麼?
 92
 93   for (( column = 0; column < Columns; column++ ))
 94   do
 95
 96     if [ "$row" -ge 0 ]
 97     then
 98       let "t1 = $column - $row"
 99       let "t2 = $column"
100     else
101       let "t1 = $column"
102       let "t2 = $column + $row"
103     fi 
104
105     filter $t1 $t2   # 過濾出負數數組索引.
106                      # 如果你不這樣做會怎麼樣?
107   done
108
109   echo; echo
110
111 done
112
113 #  數組旋轉靈感源於Herbert Mayer寫的
114 #+ "Advanced C Programming on the IBM PC," 的例子 (頁碼. 143-146)
http://www.818198.com  Page 437
SHELL十三問
115 #+ (看參考書目附錄).
116 #  這也能看出C能做的事情有多少能用shell腳本做到.
117 #
118
119 }
120
121
122 #---------------   現在, 可以開始了.     ------------#
123 load_alpha     # 加載數組.
124 print_alpha    # 打印數組. 
125 rotate         # 反時鐘旋轉數組45度.
126 #-----------------------------------------------------#
127
128 exit 0
129
130 # 這是有點做作,不太優雅.
131
132 # 練習:
133 # ---------134 # 1)  重寫數組加載和打印函數,
135 #     使其更直觀和容易瞭解.
136 #
137 # 2)  指出數組旋轉函數是什麼原理.
138 #     Hint索引: 思考數組從尾向前索引的實現.
139 #
140 # 3)  重寫腳本使其可以處理非方形數組Rewrite this script to handle a non-square array,
141 #     例如 6 X 4 的數組.
142 #     嘗試旋轉數組時做到最小"失真".
################################End Script#########################################
二維數組本質上等同於一維數組, 而只增加了使用行和列的位置來引用和操作元素的尋址模式.
關於二維數組更好的例子, 請參考例子 A-10.
--
另一個有趣的使用數組的腳本:
    * 例子 14-3
[]    
   
高級Bash腳本編程指南(五)(下)
文章整理: 文章來源: 網絡
第27章 /dev 和 /proc
=====================
Linux 或 UNIX 機器都帶有/dev和/proc目錄用於特殊目的.
27.1. /dev
----------在 /dev 目錄內包含以或不以硬件形式出現的物理設備條目. [1] 包含被掛載的文件系統的硬
設備分區在/dev目錄下都有對應的條目, 就像df命令所展示的.
 bash$ df
 Filesystem           1k-blocks      Used Available Use%
 Mounted on
http://www.818198.com  Page 438
SHELL十三問
 /dev/hda6               495876    222748    247527  48% /
 /dev/hda1                50755      3887     44248   9% /boot
 /dev/hda8               367013     13262    334803   4% /home
 /dev/hda5              1714416   1123624    503704  70% /usr
在其他方面, /dev 目錄也包含環回設備(loopback devices) , 例如/dev/loop0. 環回設備是
一個使普通文件能被像對待塊設備一樣來進行存取的機制. [2] 這使我們可以將一個大文件內
的整個文件系統掛載到系統目錄下. 參考例子 13-8和例子 13-7.
/dev還有少量的僞設備用於特殊的用途, 例如/dev/null, /dev/zero, /dev/urandom,
/dev/sda1, /dev/udp, 和/dev/tcp.
例如:
爲了掛載(mount) 一個USB閃盤設備, 將下面一行添加到/etc/fstab. [3]
   1 /dev/sda1    /mnt/flashdrive    auto    noauto,user,noatime    0 0
(也請參考例子 A-23.)
當對/dev/tcp/$host/$port 僞設備文件執行一個命令時, Bash會打開一個相關的TCP的socket.
[4]
從nist.gov得到時間:
 bash$ cat </dev/tcp/time.nist.gov/13
 53082 04-03-18 04:26:54 68 0 0 502.3 UTC(NIST) *
       
[Mark貢獻了上面的例子.]
下載一個 URL:
 bash$ exec 5<>/dev/tcp/www.net.cn/80
 bash$ echo -e "GET / HTTP/1.0\n" >&5
 bash$ cat <&5
       
[Thanks, Mark 和 Mihai Maties.]
Example 27-1 利用/dev/tcp 來檢修故障
################################Start Script#######################################
 1 #!/bin/bash
 2 # dev-tcp.sh: 用/dev/tcp 重定向來檢查Internet連接.
 3
 4 # Troy Engel編寫.
 5 # 已得到作者允許.
 6 
 7 TCP_HOST=www.dns-diy.com   # 一個已知的 ISP.
 8 TCP_PORT=80                # http的端口是80 .
 9  
10 # 嘗試連接. (有些像 'ping' . . .)
11 echo "HEAD / HTTP/1.0" >/dev/tcp/${TCP_HOST}/${TCP_PORT}
12 MYEXIT=$?
13
14 : <<EXPLANATION
15 If bash was compiled with --enable-net-redirections, it has the capability of
16 using a special character device for both TCP and UDP redirections. These
17 redirections are used identically as STDIN/STDOUT/STDERR. The device entries
18 are 30,36 for /dev/tcp:
19
20   mknod /dev/tcp c 30 36
http://www.818198.com  Page 439
SHELL十三問
21
22 >From the bash reference:
23 /dev/tcp/host/port
24     If host is a valid hostname or Internet address, and port is an integer
25 port number or service name, Bash attempts to open a TCP connection to the
26 corresponding socket.
27 EXPLANATION
28
29   
30 if [ "X$MYEXIT" = "X0" ]; then
31   echo "Connection successful. Exit code: $MYEXIT"
32 else
33   echo "Connection unsuccessful. Exit code: $MYEXIT"
34 fi
35
36 exit $MYEXIT
################################End Script#########################################
譯者補充上面這個例子輸出的解釋(EXPLANATION)譯文:
如果bash以--enable-net- redirections選項來編譯,它就擁有了使用一個特殊字符設備來完
成TCP和UDP重定向功能的能力.這種重定向能力就像 STDIN/STDOUT/STDERR一樣被標識.該字
符設備/dev/tcp的主次設備號是30,36:
mknod /dev/tcp c 30 36
>摘自bash參考手冊:
/dev/tcp/host/port
如果host是一個有效的主機名或因特網有效地址,並且port是一個整數的端口號或是服務名稱
,Bash會嘗試打開一個相對應的TCP連接socket.
注意事項:
[1]  /dev目錄中的條目是爲各種物理設備和虛擬設備提供的掛載點. 這些條目使用非常少
  的設備空間.
  一些像/dev/null, /dev/zero, 和 /dev/urandom的設備是虛擬的. 它們不是真正的
  物理設備,而只是存在於軟件的虛擬設備.
[2]  塊設備讀或寫(或兩者兼之)數據都是以塊爲單位的進行的, 與之相對應的字符設備
  則使用字符爲單位來進行存取.塊設備典型的有硬盤和CD-ROM設備,字符設備典型的
  例子如鍵盤.
[3]  當然,掛載點/mnt/flashdrive必須存在,如果不存在,以root用戶來執行
  mkdir /mnt/flashdrive. 
  爲了最終能掛載設備,用下面的命令: mount /mnt/flashdrive
  較新的Linux發行版自動把閃盤設備掛載到/media目錄.
[4]  socket是一種特殊的用於通信的I/O端口. 它允許同一臺主機內不同硬件設備間的數
  據傳輸,允許在相同網絡中的主機間的數據傳輸,也允許穿越不同網絡的主機間的數
  據傳輸,當然,也允許在Internet上不同位置主機間的數據傳輸.
27.2. /proc
-----------/proc目錄實際上是一個僞文件系統 . 在 /proc 目錄裏的文件是當前運行系統和內核進程及
它們的相關信息和統計.
 bash$ cat /proc/devices
 Character devices:
   1 mem
http://www.818198.com  Page 440
SHELL十三問
   2 pty
   3 ttyp
   4 ttyS
   5 cua
   7 vcs
  10 misc
  14 sound
  29 fb
  36 netlink
 128 ptm
 136 pts
 162 raw
 254 pcmcia
 Block devices:
   1 ramdisk
   2 fd
   3 ide0
   9 md
 
 
 bash$ cat /proc/interrupts
            CPU0      
   0:      84505          XT-PIC  timer
   1:       3375          XT-PIC  keyboard
   2:          0          XT-PIC  cascade
   5:          1          XT-PIC  soundblaster
   8:          1          XT-PIC  rtc
  12:       4231          XT-PIC  PS/2 Mouse
  14:     109373          XT-PIC  ide0
 NMI:          0
 ERR:          0
 
 
 bash$ cat /proc/partitions
 major minor  #blocks  name     rio rmerge rsect ruse wio wmerge wsect wuse running use aveq
    3     0    3007872 hda 4472 22260 114520 94240 3551 18703 50384 549710 0 111550 644030
    3     1      52416 hda1 27 395 844 960 4 2 14 180 0 800 1140
    3     2          1 hda2 0 0 0 0 0 0 0 0 0 0 0
    3     4     165280 hda4 10 0 20 210 0 0 0 0 0 210 210
    ...
 
 
 bash$ cat /proc/loadavg
 0.13 0.42 0.27 2/44 1119
 
 
 bash$ cat /proc/apm
 1.16 1.2 0x03 0x01 0xff 0x80 -1% -1 ?
http://www.818198.com  Page 441
SHELL十三問
Shell 腳本可以從/proc目錄中的一些文件裏提取數據. [1]
   1 FS=iso                       # ISO 文件系統是否被內核支持?
   2
   3 grep $FS /proc/filesystems   # iso9660
   1 kernel_version=$( awk '{ print $3 }' /proc/version )
   1 CPU=$( awk '/model name/ {print $4}' < /proc/cpuinfo )
   2
   3 if [ $CPU = Pentium ]
   4 then
   5   run_some_commands
   6   ...
   7 else
   8   run_different_commands
   9   ...
  10 fi
   1 devfile="/proc/bus/usb/devices"
   2 USB1="Spd=12"
   3 USB2="Spd=480"
   4
   5
   6 bus_speed=$(grep Spd $devfile | awk '{print $9}')
   7
   8 if [ "$bus_speed" = "$USB1" ]
   9 then
  10   echo "USB 1.1 port found."
  11   # 這兒開始操作USB 1.1相關的動作.
  12 fi
/proc目錄下有許多不相同的數字命名的子目錄. 這些子目錄的數字名字都映射對應的當前正
在運行的進程的進程號(process ID) . 這些子目錄裏面有許多文件用於保存對應進程的信息.
文件 stat 和 status 保存着進程運行時的各項統計, the cmdline文件保存該進程的被調用
時的命令行參數, 而and the exe 文件是該運行進程完整路徑名的符號鏈接. 還有其他一些文
件,但從腳本的觀點來看它們都非常的有意思.
Example 27-2 搜索與一個PID相關的進程
################################Start Script#######################################
 1 #!/bin/bash
 2 # pid-identifier.sh: 給出指定PID的進程的程序全路徑.
 3
 4 ARGNO=1  # 此腳本期望的參數個數.
 5 E_WRONGARGS=65
 6 E_BADPID=66
 7 E_NOSUCHPROCESS=67
 8 E_NOPERMISSION=68
 9 PROCFILE=exe
10
11 if [ $# -ne $ARGNO ]
12 then
13   echo "Usage: `basename $0` PID-number" >&2  # 幫助信息重定向到標準出錯.
14   exit $E_WRONGARGS
http://www.818198.com  Page 442
SHELL十三問
15 fi 
16
17 pidno=$( ps ax | grep $1 | awk '{ print $1 }' | grep $1 )
18 # 搜索命令"ps"輸出的第一列.
19 # 然後再次確認是真正我們要尋找的進程,而不是這個腳本調用而產生的進程.
20 # 後一個"grep $1"會濾掉這個可能產生的進程.
21 #
22 #    pidno=$( ps ax | awk '{ print $1 }' | grep $1 )
23 #    也可以, 由 Teemu Huovila指出.
24
25 if [ -z "$pidno" ]  # 如果過濾完後結果是一個空字符串,
26 then                # 沒有對應的PID進程在運行.
27   echo "No such process running."
28   exit $E_NOSUCHPROCESS
29 fi 
30
31 # 也可以用:
32 #   if ! ps $1 > /dev/null 2>&1
33 #   then                # 沒有對應的PID進程在運行.
34 #     echo "No such process running."
35 #     exit $E_NOSUCHPROCESS
36 #    fi
37
38 # 爲了簡化整個進程,使用"pidof".
39
40
41 if [ ! -r "/proc/$1/$PROCFILE" ]  # 檢查讀權限.
42 then
43   echo "Process $1 running, but..."
44   echo "Can't get read permission on /proc/$1/$PROCFILE."
45   exit $E_NOPERMISSION  # 普通用戶不能存取/proc目錄的某些文件.
46 fi 
47
48 # 最後兩個測試可以用下面的代替:
49 #    if ! kill -0 $1 > /dev/null 2>&1 # '0'不是一個信號,
50                                       # 但這樣可以測試是否可以
51                                       # 向該進程發送信號.
52 #    then echo "PID doesn't exist or you're not its owner" >&2
53 #    exit $E_BADPID
54 #    fi
55
56
57
58 exe_file=$( ls -l /proc/$1 | grep "exe" | awk '{ print $11 }' )
59 # 或       exe_file=$( ls -l /proc/$1/exe | awk '{print $11}' )
60 #
61 # /proc/pid-number/exe 是進程程序全路徑的符號鏈接.
62 #
http://www.818198.com  Page 443
SHELL十三問
63
64 if [ -e "$exe_file" ]  # 如果 /proc/pid-number/exe 存在 ...
65 then                 # 則相應的進程存在.
66   echo "Process #$1 invoked by $exe_file."
67 else
68   echo "No such process running."
69 fi 
70
71
72 # 這個被詳細講解的腳本幾乎可以用下面的命令代替:
73 # ps ax | grep $1 | awk '{ print $5 }'
74 # 然而, 這樣並不會工作...
75 # 因爲'ps'輸出的第5列是進程的argv[0](即命令行第一個參數,調用時程序用的程序路徑本身),
76 # 但不是可執行文件.
77 #
78 # 然而, 下面的兩個都可以工作.
79 #       find /proc/$1/exe -printf '%l\n'
80 #       lsof -aFn -p $1 -d txt | sed -ne 's/^n//p'
81
82 # 由Stephane Chazelas附加註釋.
83
84 exit 0
################################End Script#########################################
Example 27-3 網絡連接狀態
################################Start Script#######################################
 1 #!/bin/bash
 2
 3 PROCNAME=pppd        # ppp 守護進程
 4 PROCFILENAME=status  # 在這兒尋找信息.
 5 NOTCONNECTED=65
 6 INTERVAL=2           # 兩秒刷新一次.
 7
 8 pidno=$( ps ax | grep -v "ps ax" | grep -v grep | grep $PROCNAME | awk '{ print $1 }' )
 9 # 搜索ppp守護進程'pppd'的進程號.
10 # 一定要過濾掉由搜索進程產生的該行進程.
11 #
12 #  正如Oleg Philon指出的那樣,
13 #+ 使用"pidof"命令會相當的簡單.
14 #  pidno=$( pidof $PROCNAME )
15 #
16 #  頗有良心的建議:
17 #+ 當命令序列變得複雜的時候,去尋找更簡潔的辦法. .
18
19
20 if [ -z "$pidno" ]   # 如果沒有找到此進程號,則進程沒有運行.
21 then
22   echo "Not connected."
23   exit $NOTCONNECTED
http://www.818198.com  Page 444
SHELL十三問
24 else
25   echo "Connected."; echo
26 fi
27
28 while [ true ]       # 死循環,這兒可以有所改進.
29 do
30
31   if [ ! -e "/proc/$pidno/$PROCFILENAME" ]
32   # 進程運行時,對應的"status"文件會存在.
33   then
34     echo "Disconnected."
35     exit $NOTCONNECTED
36   fi
37
38 netstat -s | grep "packets received"  # 取得一些連接統計.
39 netstat -s | grep "packets delivered"
40
41
42   sleep $INTERVAL
43   echo; echo
44
45 done
46
47 exit 0
48
49 # 當要停止它時,可以用Control-C終止.
50
51 #    練習:
52 #    ---------53 #    改進這個腳本,使它能按"q"鍵退出.
54 #    給腳本更友好的界面.
################################End Script#########################################
注意: 一般來說, 寫/proc目錄裏的文件是危險 ,因爲這樣會破壞這個文件系統或摧毀機器.
注意事項:
[1]  一些系統命令, 例如 procinfo, free, vmstat, lsdev, 和uptime也能做類似的事情.
第28章 關於Zeros和Nulls
========================
/dev/zero和/dev/null
使用/dev/null
    把/dev/null看作"黑洞". 它非常等價於一個只寫文件. 所有寫入它的內容都會永遠丟失.
而嘗試從它那兒讀取內容則什麼也讀不到. 然而, /dev/null對命令行和腳本都非常的有用.
    禁止標準輸出.
   1 cat $filename >/dev/null
   2 # 文件內容丟失,而不會輸出到標準輸出.
    禁止標準錯誤 (來自例子 12-3).
   1 rm $badname 2>/dev/null
   2 #           這樣錯誤信息[標準錯誤]就被丟到太平洋去了.
    禁止標準輸出和標準錯誤的輸出.
http://www.818198.com  Page 445
SHELL十三問
   1 cat $filename 2>/dev/null >/dev/null
   2 # 如果"$filename"不存在,將不會有任何錯誤信息提示.
   3 # 如果"$filename"存在, 文件的內容不會打印到標準輸出.
   4 # 因此Therefore, 上面的代碼根本不會輸出任何信息.
   5 #
   6 #  當只想測試命令的退出碼而不想有任何輸出時非常有用.
   7 #
   8 #
   9 # cat $filename &>/dev/null
  10 #     也可以, 由 Baris Cicek 指出.
 刪除一個文件的內容, 但是保留文件本身, 和所有的文件權限(來自於Example 2-1和
 Example 2-3):
   1 cat /dev/null > /var/log/messages
   2 #  : > /var/log/messages   有同樣的效果, 但不會產生新的進程.(因爲:是內建的)
   3
   4 cat /dev/null > /var/log/wtmp
    自動清空日誌文件的內容 (特別適合處理這些由商業Web站點發送的討厭的"cookies"):
Example 28-1 隱藏cookie而不再使用
################################Start Script#######################################
1 if [ -f ~/.netscape/cookies ]  # 如果存在則刪除.
2 then
3   rm -f ~/.netscape/cookies
4 fi
5
6 ln -s /dev/null ~/.netscape/cookies
7 # 現在所有的cookies都會丟入黑洞而不會保存在磁盤上了.
################################End Script#########################################
使用/dev/zero
 像/dev/null一樣, /dev/zero也是一個僞文件, 但它實際上產生連續不斷的null的流
 (二進制的零流,而不是ASCII型的). 寫入它的輸出會丟失不見, 而從/dev/zero讀出一
 連串的null也比較困難, 雖然這也能通過od或一個十六進制編輯器來做到. /dev/zero主
 要的用處是用來創建一個指定長度用於初始化的空文件,就像臨時交換文件.
Example 28-2 用/dev/zero創建一個交換臨時文件
################################Start Script#######################################
 1 #!/bin/bash
 2 # 創建一個交換文件.
 3
 4 ROOT_UID=0         # Root 用戶的 $UID 是 0.
 5 E_WRONG_USER=65    # 不是 root?
 6
 7 FILE=/swap
 8 BLOCKSIZE=1024
 9 MINBLOCKS=40
10 SUCCESS=0
11
12
13 # 這個腳本必須用root來運行.
14 if [ "$UID" -ne "$ROOT_UID" ]
http://www.818198.com  Page 446
SHELL十三問
15 then
16   echo; echo "You must be root to run this script."; echo
17   exit $E_WRONG_USER
18 fi 
19  
20
21 blocks=${1:-$MINBLOCKS}          #  如果命令行沒有指定,
22                                  #+ 則設置爲默認的40塊.
23 # 上面這句等同如:
24 # --------------------------------------------------25 # if [ -n "$1" ]
26 # then
27 #   blocks=$1
28 # else
29 #   blocks=$MINBLOCKS
30 # fi
31 # --------------------------------------------------32
33
34 if [ "$blocks" -lt $MINBLOCKS ]
35 then
36   blocks=$MINBLOCKS              # 最少要有 40 個塊長.
37 fi 
38
39
40 echo "Creating swap file of size $blocks blocks (KB)."
41 dd if=/dev/zero of=$FILE bs=$BLOCKSIZE count=$blocks  # 把零寫入文件.
42
43 mkswap $FILE $blocks             # 將此文件建爲交換文件(或稱交換分區).
44 swapon $FILE                     # 激活交換文件.
45
46 echo "Swap file created and activated."
47
48 exit $SUCCESS
################################End Script#########################################
關於 /dev/zero 的另一個應用是爲特定的目的而用零去填充一個指定大小的文件, 如掛載一個
文件系統到環回設備 (loopback device) (參考例子 13-8) 或"安全地" 刪除一個文件
(參考例子 12-55).
Example 28-3 創建ramdisk
################################Start Script#######################################
 1 #!/bin/bash
 2 # ramdisk.sh
 3
 4 #  "ramdisk"是系統RAM內存的一段,
 5 #+ 它可以被當成是一個文件系統來操作.
 6 #  它的優點是存取速度非常快 (包括讀和寫).
 7 #  缺點: 易失性, 當計算機重啓或關機時會丟失數據.
 8 #+       會減少系統可用的RAM.
http://www.818198.com  Page 447
SHELL十三問
 9 #
10 #  那麼ramdisk有什麼作用呢?
11 #  保存一個較大的數據集在ramdisk, 比如一張表或字典,
12 #+ 這樣可以加速數據查詢, 因爲在內存裏查找比在磁盤裏查找快得多.
13
14
15 E_NON_ROOT_USER=70             # 必須用root來運行.
16 ROOTUSER_NAME=root
17
18 MOUNTPT=/mnt/ramdisk
19 SIZE=2000                      # 2K 個塊 (可以合適的做修改)
20 BLOCKSIZE=1024                 # 每塊有1K (1024 byte) 的大小
21 DEVICE=/dev/ram0               # 第一個 ram 設備
22
23 username=`id -nu`
24 if [ "$username" != "$ROOTUSER_NAME" ]
25 then
26   echo "Must be root to run \"`basename $0`\"."
27   exit $E_NON_ROOT_USER
28 fi
29
30 if [ ! -d "$MOUNTPT" ]         #  測試掛載點是否已經存在了,
31 then                           #+ 如果這個腳本已經運行了好幾次了就不會再建這個目錄了
32   mkdir $MOUNTPT               #+ 因爲前面已經建立了.
33 fi
34
35 dd if=/dev/zero of=$DEVICE count=$SIZE bs=$BLOCKSIZE  # 把RAM設備的內容用零填充.
36                                                       # 爲何需要這麼做?
37 mke2fs $DEVICE                 # 在RAM設備上創建一個ext2文件系統.
38 mount $DEVICE $MOUNTPT         # 掛載設備.
39 chmod 777 $MOUNTPT             # 使普通用戶也可以存取這個ramdisk.
40                                # 但是, 只能由root來缷載它.
41
42 echo "\"$MOUNTPT\" now available for use."
43 # 現在 ramdisk 即使普通用戶也可以用來存取文件了.
44
45 #  注意, ramdisk是易失的, 所以當計算機系統重啓或關機時ramdisk裏的內容會消失.
46 #
47 #  拷貝所有你想保存文件到一個常規的磁盤目錄下.
48
49 # 重啓之後, 運行這個腳本再次建立起一個 ramdisk.
50 # 僅重新加載 /mnt/ramdisk 而沒有其他的步驟將不會正確工作.
51
52 #  如果加以改進, 這個腳本可以放在 /etc/rc.d/rc.local,
53 #+ 以使系統啓動時能自動設立一個ramdisk.
54 #  這樣很合適速度要求高的數據庫服務器.
55
56 exit 0
http://www.818198.com  Page 448
SHELL十三問
################################End Script#########################################
 最後值得一提的是, ELF二進制文件利用了/dev/zero.
第29章 調試
============
Debugging is twice as hard as writing the code in the first place. Therefore,
if you write the code as cleverly as possible, you are, by definition, not smart
enough to debug it.
             Brian Kernighan
Bash shell 沒有自帶調試器, 甚至沒有任何調試類型的命令或結構. [1]  腳本里的語法錯誤
或拼寫錯誤會產生含糊的錯誤信息,通常這些在調試非功能性的腳本時沒什麼幫助.
Example 29-1 一個錯誤的腳本
################################Start Script#######################################
 1 #!/bin/bash
 2 # ex74.sh
 3
 4 # 這是一個錯誤的腳本.
 5 # 哪裏有錯?
 6
 7 a=37
 8
 9 if [$a -gt 27 ]
10 then
11   echo $a
12 fi 
13
14 exit 0
################################End Script#########################################
腳本的輸出:
 ./ex74.sh: [37: command not found
上面的腳本有什麼錯誤(線索: 注意if的後面)?
Example 29-2 丟失關鍵字(keyword)
################################Start Script#######################################
1 #!/bin/bash
2 # missing-keyword.sh: 會產生什麼樣的錯誤信息?
3
4 for a in 1 2 3
5 do
6   echo "$a"
7 # done     # 第7行的必需的關鍵字 'done' 被註釋掉了.
8
9 exit 0 
################################End Script#########################################
腳本的輸出:
 missing-keyword.sh: line 10: syntax error: unexpected end of file
注意錯誤信息中說明的錯誤行不必一定要參考, 但那行是Bash解釋器最終認識到是個錯誤的
地方.
出錯信息可能在報告語法錯誤的行號時會忽略腳本的註釋行.
如果腳本可以執行,但不是你所期望的那樣工作怎麼辦? 這大多是由於常見的邏輯錯誤產生的.
http://www.818198.com  Page 449
SHELL十三問
Example 29-3 另一個錯誤腳本
################################Start Script#######################################
 1 #!/bin/bash
 2
 3 #  這個腳本目的是爲了刪除當前目錄下的所有文件,包括文件名含有空格的文件.
 4 #
 5 #  但不能工作.
 6 #  爲什麼?
 7
 8
 9 badname=`ls | grep ' '`
10
11 # 試試這個:
12 # echo "$badname"
13
14 rm "$badname"
15
16 exit 0
################################End Script#########################################
爲了找出 例子 29-3  的錯誤可以把echo "$badname" 行的註釋去掉. echo 出來的信息對你
判斷是否腳本以你希望的方式運行時很有幫助.
在這個實際的例子裏, rm "$badname" 不會達到想要的結果,因爲$badname 沒有引用起來.
加上引號以保證rm 命令只有一個參數(這就只能匹配一個文件名). 一個不完善的解決辦法是
刪除A partial fix is to remove to quotes from $badname and to reset $IFS to contain only a newline, IFS=$'\n'. 不過, 存在更簡單的
辦法.<rojy bug>
   1 # 修正刪除包含空格文件名時出錯的辦法.
   2 rm *\ *
   3 rm *" "*
   4 rm *' '*
   5 # Thank you. S.C.
總結該腳本的症狀,
   1. 終止於一個"syntax error"(語法錯誤)的信息, 或
   2. 它能運行, 但不是按期望的那樣運行(邏輯錯誤).
   3. 它能運行,運行的和期望的一樣, 但有討厭的副作用 (邏輯炸彈).
用來調試不能工作的腳本的工具包括
   1.  echo 語句可用在腳本中的有疑問的點上以跟蹤瞭解變量的值, 並且也可以瞭解後續腳
  本的動作.
    注意: 最好只在調試時才使用echo語句.
     1 ### debecho (debug-echo), by Stefano Falsetto ###
     2 ### 只有變量 DEBUG 設置了值時纔會打印傳遞進來的變量值. ###
     3 debecho () {
     4   if [ ! -z "$DEBUG" ]; then
     5      echo "$1" >&2
     6      #         ^^^ 打印到標準出錯
     7   fi
     8 }
     9
    10 DEBUG=on
http://www.818198.com  Page 450
SHELL十三問
    11 Whatever=whatnot
    12 debecho $Whatever   # whatnot
    13
    14 DEBUG=
    15 Whatever=notwhat
    16 debecho $Whatever   # (這兒就不會打印了.)
   2.  使用 tee 過濾器來檢查臨界點的進程或數據流.
   3.  設置選項 -n -v -x
      sh -n scriptname 不會實際運行腳本,而只是檢查腳本的語法錯誤. 這等同於把
  set -n 或 set -o noexec 插入腳本中. 注意還是有一些語法錯誤不能被這種檢查找
  出來.
      sh -v scriptname 在實際執行一個命令前打印出這個命令. 這也等同於在腳本里設置
   set -v 或 set -o verbose.
      選項 -n 和 -v 可以一塊使用. sh -nv scriptname 會打印詳細的語法檢查.
      sh -x scriptname 打印每個命令的執行結果, 但只用在某些小的方面. 它等同於腳本
  中插入 set -x 或 set -o xtrace.
      把 set -u 或 set -o nounset 插入到腳本里並運行它, 就會在每個試圖使用沒有申明
  過的變量的地方打印出一個錯誤信息.
   4.  使用一個"assert"(斷言) 函數在腳本的臨界點上測試變量或條件.
  (這是從C語言中借用來的.)
Example 29-4 用"assert"測試條件
################################Start Script#######################################
 1 #!/bin/bash
 2 # assert.sh
 3
 4 assert ()                 #  如果條件測試失敗,
 5 {                         #+ 則打印錯誤信息並退出腳本.
 6   E_PARAM_ERR=98
 7   E_ASSERT_FAILED=99
 8
 9
10   if [ -z "$2" ]          # 沒有傳遞足夠的參數.
11   then
12     return $E_PARAM_ERR   # 什麼也不做就返回.
13   fi
14
15   lineno=$2
16
17   if [ ! $1 ]
18   then
19     echo "Assertion failed:  \"$1\""
20     echo "File \"$0\", line $lineno"
21     exit $E_ASSERT_FAILED
22   # else
23   #   return
24   #   返回並繼續執行腳本後面的代碼.
25   fi 
26 }   
http://www.818198.com  Page 451
SHELL十三問
27
28
29 a=5
30 b=4
31 condition="$a -lt $b"     #  會錯誤信息並從腳本退出.
32                           #  把這個“條件”放在某個地方,
33                           #+ 然後看看有什麼現象.
34
35 assert "$condition" $LINENO
36 # 腳本以下的代碼只有當"assert"成功時纔會繼續執行.
37
38
39 # 其他的命令.
40 # ...
41 echo "This statement echoes only if the \"assert\" does not fail."
42 # ...
43 # 餘下的其他命令.
44
45 exit 0
################################End Script#########################################
 5.  用變量$LINENO和內建的caller.
 6.  捕捉exit.
  腳本中的The exit 命令會觸發信號0,終結進程,即腳本本身. [2] 這常用來捕捉
  exit命令做某事, 如強制打印變量值. trap 命令必須是腳本中第一個命令.
捕捉信號
trap
 當收到一個信號時指定一個處理動作; 這在調試時也很有用.
 注意: 信號是發往一個進程的非常簡單的信息, 要麼是由內核發出要麼是由另一個進程,
   以告訴接收進程採取一些指定的動作 (一般是中止). 例如, 按Control-C, 發送
   一個用戶中斷( 即 INT 信號)到運行中的進程.
    1 trap '' 2
    2 # 忽略信號 2 (Control-C), 沒有指定處理動作.
    3
    4 trap 'echo "Control-C disabled."' 2
    5 # 當按 Control-C 時顯示一行信息.
Example 29-5 捕捉 exit
################################Start Script#######################################
 1 #!/bin/bash
 2 # 用trap捕捉變量值.
 3
 4 trap 'echo Variable Listing --- a = $a  b = $b' EXIT
 5 #  EXIT 是腳本中exit命令產生的信號的信號名.
 6 #
 7 #  由"trap"指定的命令不會被馬上執行,只有當發送了一個適應的信號時纔會執行.
 8 #
 9
10 echo "This prints before the \"trap\" --"
11 echo "even though the script sees the \"trap\" first."
http://www.818198.com  Page 452
SHELL十三問
12 echo
13
14 a=39
15
16 b=36
17
18 exit 0
19 #  注意到註釋掉上面一行的'exit'命令也沒有什麼不同,
20 #+ 這是因爲執行完所有的命令腳本都會退出.
################################End Script#########################################
Example 29-6 在Control-C後清除垃圾
################################Start Script#######################################
 1 #!/bin/bash
 2 # logon.sh: 簡陋的檢查你是否還處於連線的腳本.
 3
 4 umask 177  # 確定臨時文件不是全部用戶都可讀的.
 5
 6
 7 TRUE=1
 8 LOGFILE=/var/log/messages
 9 #  注意 $LOGFILE 必須是可讀的
10 #+ (用 root來做:chmod 644 /var/log/messages).
11 TEMPFILE=temp.$$
12 #  創建一個"唯一的"臨時文件名, 使用腳本的進程ID.
13 #     用 'mktemp' 是另一個可行的辦法.
14 #     舉例:
15 #     TEMPFILE=`mktemp temp.XXXXXX`
16 KEYWORD=address
17 #  上網時, 把"remote IP address xxx.xxx.xxx.xxx"這行
18 #                      加到 /var/log/messages.
19 ONLINE=22
20 USER_INTERRUPT=13
21 CHECK_LINES=100
22 #  日誌文件中有多少行要檢查.
23
24 trap 'rm -f $TEMPFILE; exit $USER_INTERRUPT' TERM INT
25 #  如果腳本被control-c中斷了,則清除臨時文件.
26
27 echo
28
29 while [ $TRUE ]  #死循環.
30 do
31   tail -$CHECK_LINES $LOGFILE> $TEMPFILE
32   #  保存系統日誌文件的最後100行到臨時文件.
33   #  這是需要的, 因爲新版本的內核在登錄網絡時產生許多日誌文件信息.
34   search=`grep $KEYWORD $TEMPFILE`
35   #  檢查"IP address" 短語是不是存在,
36   #+ 它指示了一次成功的網絡登錄.
http://www.818198.com  Page 453
SHELL十三問
37
38   if [ ! -z "$search" ] #  引號是必須的,因爲變量可能會有一些空白符.
39   then
40      echo "On-line"
41      rm -f $TEMPFILE    #  清除臨時文件.
42      exit $ONLINE
43   else
44      echo -n "."        #  -n 選項使echo不會產生新行符,
45                         #+ 這樣你可以從該行的繼續打印.
46   fi
47
48   sleep 1 
49 done 
50
51
52 #  注: 如果你更改KEYWORD變量的值爲"Exit",
53 #+ 這個腳本就能用來在網絡登錄後檢查掉線
54 #
55
56 # 練習: 修改腳本,像上面所說的那樣,並修正得更好
57 #
58
59 exit 0
60
61
62 # Nick Drage 建議用另一種方法:
63
64 while true
65   do ifconfig ppp0 | grep UP 1> /dev/null && echo "connected" && exit 0
66   echo -n "."   # 在連接上之前打印點 (.....).
67   sleep 2
68 done
69
70 # 問題: 用 Control-C來終止這個進程可能是不夠的.
71 #+         (點可能會繼續被打印.)
72 # 練習: 修復這個問題.
73
74
75
76 # Stephane Chazelas 也提出了另一個辦法:
77
78 CHECK_INTERVAL=1
79
80 while ! tail -1 "$LOGFILE" | grep -q "$KEYWORD"
81 do echo -n .
82    sleep $CHECK_INTERVAL
83 done
84 echo "On-line"
http://www.818198.com  Page 454
SHELL十三問
85
86 # 練習: 討論這幾個方法的優缺點.
87 #
################################End Script#########################################
注意: trap 的DEBUG參數在每個命令執行完後都會引起一個指定的執行動作,例如,這可用來
 跟蹤變量.
Example 29-7 跟蹤變量
################################Start Script#######################################
 1 #!/bin/bash
 2
 3 trap 'echo "VARIABLE-TRACE> \$variable = \"$variable\""' DEBUG
 4 # 在每個命令行顯示變量$variable 的值.
 5
 6 variable=29
 7
 8 echo "Just initialized \"\$variable\" to $variable."
 9
10 let "variable *= 3"
11 echo "Just multiplied \"\$variable\" by 3."
12
13 exit $?
14
15 #  "trap 'command1 . . . command2 . . .' DEBUG" 的結構適合複雜腳本的環境
16 #+ 在這種情況下多次"echo $variable"比較沒有技巧並且也耗時.
17 #
18 #
19
20 # Thanks, Stephane Chazelas 指出這一點.
21
22
23 腳本的輸出:
24
25 VARIABLE-TRACE> $variable = ""
26 VARIABLE-TRACE> $variable = "29"
27 Just initialized "$variable" to 29.
28 VARIABLE-TRACE> $variable = "29"
29 VARIABLE-TRACE> $variable = "87"
30 Just multiplied "$variable" by 3.
31 VARIABLE-TRACE> $variable = "87"
################################End Script#########################################
當然, trap 命令除了調試還有其他的用處.
Example 29-8 運行多進程 (在多處理器的機器裏)
################################Start Script#######################################
  1 #!/bin/bash
  2 # parent.sh
  3 # 在多處理器的機器裏運行多進程.
  4 # 作者: Tedman Eng
  5
http://www.818198.com  Page 455
SHELL十三問
  6 #  這是要介紹的兩個腳本的第一個,
  7 #+ 這兩個腳本都在要在相同的工作目錄下.
  8
  9
 10
 11
 12 LIMIT=$1         # 要啓動的進程總數
 13 NUMPROC=4        # 當前進程數 (forks?)
 14 PROCID=1         # 啓動的進程ID
 15 echo "My PID is $$"
 16
 17 function start_thread() {
 18         if [ $PROCID -le $LIMIT ] ; then
 19                 ./child.sh $PROCID&
 20                 let "PROCID++"
 21         else
 22            echo "Limit reached."
 23            wait
 24            exit
 25         fi
 26 }
 27
 28 while [ "$NUMPROC" -gt 0 ]; do
 29         start_thread;
 30         let "NUMPROC--"
 31 done
 32
 33
 34 while true
 35 do
 36
 37 trap "start_thread" SIGRTMIN
 38
 39 done
 40
 41 exit 0
 42
 43
 44
 45 # ======== 下面是第二個腳本 ========
 46
 47
 48 #!/bin/bash
 49 # child.sh
 50 # 在多處理器的機器裏運行多進程.
 51 # 這個腳本由parent.sh腳本調用(即上面的腳本).
 52 # 作者: Tedman Eng
 53
http://www.818198.com  Page 456
SHELL十三問
 54 temp=$RANDOM
 55 index=$1
 56 shift
 57 let "temp %= 5"
 58 let "temp += 4"
 59 echo "Starting $index  Time:$temp" "$@"
 60 sleep ${temp}
 61 echo "Ending $index"
 62 kill -s SIGRTMIN $PPID
 63
 64 exit 0
 65
 66
 67 # ======================= 腳本作者注 ======================= #
 68 #  這不是完全沒有bug的腳本.
 69 #  我運行LIMIT = 500 ,在過了開頭的一二百個循環後,
 70 #+ 這些進程有一個消失了!
 71 #  不能確定是不是因爲捕捉信號產生碰撞還是其他的原因.
 72 #  一但信號捕捉到,在下一個信號設置之前,
 73 #+ 會有一個短暫的時間來執行信號處理程序,
 74 #+ 這段時間內很可能會丟失一個信號捕捉,因此失去生成一個子進程的機會.
 75
 76 #  毫無疑問會有人能找出這個bug的原因,並且修復它
 77 #+ . . . 在將來的某個時候.
 78
 79
 80
 81 # ===================================================================== #
 82
 83
 84
 85 # ----------------------------------------------------------------------#
 86
 87
 88
 89 #################################################################
 90 # 下面的腳本由Vernia Damiano原創.
 91 # 不幸地是, 它不能正確工作.
 92 #################################################################
 93
 94 #!/bin/bash
 95
 96 #  必須以最少一個整數參數來調用這個腳本
 97 #+ (這個整數是協作進程的數目).
 98 #  所有的其他參數被傳給要啓動的進程.
 99
100
101 INDICE=8        # 要啓動的進程數目
http://www.818198.com  Page 457
SHELL十三問
102 TEMPO=5         # 每個進程最大的睡眼時間
103 E_BADARGS=65    # 沒有參數傳給腳本的錯誤值.
104
105 if [ $# -eq 0 ] # 檢查是否至少傳了一個參數給腳本.
106 then
107   echo "Usage: `basename $0` number_of_processes [passed params]"
108   exit $E_BADARGS
109 fi
110
111 NUMPROC=$1              # 協作進程的數目
112 shift
113 PARAMETRI=( "$@" )      # 每個進程的參數
114
115 function avvia() {
116          local temp
117          local index
118          temp=$RANDOM
119          index=$1
120          shift
121          let "temp %= $TEMPO"
122          let "temp += 1"
123          echo "Starting $index Time:$temp" "$@"
124          sleep ${temp}
125          echo "Ending $index"
126          kill -s SIGRTMIN $$
127 }
128
129 function parti() {
130          if [ $INDICE -gt 0 ] ; then
131               avvia $INDICE "${PARAMETRI[@]}" &
132                 let "INDICE--"
133          else
134                 trap : SIGRTMIN
135          fi
136 }
137
138 trap parti SIGRTMIN
139
140 while [ "$NUMPROC" -gt 0 ]; do
141          parti;
142          let "NUMPROC--"
143 done
144
145 wait
146 trap - SIGRTMIN
147
148 exit $?
149
http://www.818198.com  Page 458
SHELL十三問
150 : <<SCRIPT_AUTHOR_COMMENTS
151 我需要運行能指定選項的一個程序,
152 能接受許多不同的文件,並在一個多處理器的機器上運行
153 所以我想(我也將會)使指定數目的進程運行,並且每個進程終止後都能啓動一個新的
154
155
156 "wait"命令沒什麼幫助, 因爲它是等候一個指定的或所有的後臺進程.
157  所以我寫了這個使用了trap指令的bash腳本來做這個任務.
158
159   --Vernia Damiano
160 SCRIPT_AUTHOR_COMMENTS
################################End Script#########################################
注意: trap '' SIGNAL (兩個引號引空) 在腳本中禁用了 SIGNAL 信號的動作(即忽略了).
  trap SIGNAL 則恢復了 SIGNAL 信號前次的處理動作. 這在保護腳本的某些臨界點的
  位置不受意外的中斷影響時很有用.
   1  trap '' 2  # 信號 2是  Control-C, 現在被忽略了.
   2  command
   3  command
   4  command
   5  trap 2     # 再啓用Control-C
   6  
 Bash的版本 3 增加了下面的特殊變量用於調試.
   1.  $BASH_ARGC
   2.  $BASH_ARGV
   3.  $BASH_COMMAND
   4.  $BASH_EXECUTION_STRING
   5.  $BASH_LINENO
   6.  $BASH_SOURCE
   7.  $BASH_SUBSHELL
注意事項:
[1]  Rocky Bernstein的 Bash debugger 實際上填補了這個空白.
[2]  依據慣例,信號0 被指定爲退出(exit).
第30章 選項
============
選項用來更改shell或/和腳本行爲的機制.
set 命令用來在腳本里激活各種選項. 在腳本中任何你想讓選項生效的地方,插入
set -o option-name 或, 用更簡短的格式, set -option-abbrev. 這兩種格式都是等價的.
   1       #!/bin/bash
   2
   3       set -o verbose
   4       # 執行前打印命令.
   1       #!/bin/bash
   2
   3       set -v
   4       # 和上面的有完全相同的效果.
注意: 爲了在腳本里停用一個選項, 插入 set +o option-name 或 set +option-abbrev.
   1       #!/bin/bash
   2
http://www.818198.com  Page 459
SHELL十三問
   3       set -o verbose
   4       # 激活命令回顯.
   5       command
   6       ...
   7       command
   8
   9       set +o verbose
  10       # 停用命令回顯.
  11       command
  12       # 沒有回顯命令了.
  13
  14
  15       set -v
  16       # 激活命令回顯.
  17       command
  18       ...
  19       command
  20
  21       set +v
  22       # 停用命令回顯.
  23       command
  24
  25       exit 0
  26      
另一個在腳本里啓用選項的方法是在腳本頭部的#!後面指定選項.
   1       #!/bin/bash -x
   2       #
   3       # 下面是腳本的主要內容.
   4      
從命令行來激活腳本的選項也是可以辦到的. 一些不能和set一起用的選項可以用在命令行指
定. -i是其中之一, 可以使腳本以交互方式運行.
bash -v script-name
bash -o verbose script-name
下面的表格列舉了一些有用的選項. 它們都可以用簡短格式來指定(以一個短橫線開頭)也可
以用完整的名字來指定(用雙短橫線開頭或用-o來指定).
table 30-1 Bash 選項
==================================================================================
| 縮寫      | 名稱   | 作用
==================================================================================
| -C     | noclobber   | 防止重定向時覆蓋文件 (此作用會被>|覆蓋)
==================================================================================
| -D     | (none)   | 列出雙引號引起的含有$前綴的字符串,但不執行腳本
|      |    | 中的命令
==================================================================================
| -a     | allexport  | 導出所有定義的變量到環境變量中
==================================================================================
| -b     | notify   | 當後臺任務終止時給出通知 (在腳本中用的不多)
==================================================================================
http://www.818198.com  Page 460
SHELL十三問
| -c...     | (none)   | 從...讀命令
==================================================================================
| -e     | errexit  | 腳本發生第一個錯誤時就中止腳本運行,即當一個命令
|      |    | 返回非零值時退出腳本 (除了until 或 while loops,
|      |    | if-tests, list constructs)
==================================================================================
| -f     | noglob   | 文件名替換停用(指像*這樣的符號不能替換爲文件名了)
==================================================================================
| -i     | interactive | 使腳本以交互式方式運行
==================================================================================
| -n     | noexec   | 從腳本里讀命令但不執行它們(語法檢查)
==================================================================================
| -o Option-Name  | (none)   | 調用Option-Name 選項
==================================================================================
| -o posix    | POSIX   | 更改Bash或腳本的行爲,使之符合POSIX標準.
==================================================================================
| -p     | privileged  | 腳本作爲"suid"程序來運行 (小心!)
==================================================================================
| -r     | restricted  | 腳本在受限模式中運行 (參考第21章).
==================================================================================
| -s     | stdin   | 從標準輸入讀命令
==================================================================================
| -t     | (none)   | 第一個命令後就退出
==================================================================================
| -u     | nounset  | 當使用一個未定義的變量時產生一個錯誤信息,並強制
|      |    | 退出腳本.
==================================================================================
| -v     | verbose  | 執行命令之前打印命令到標準輸出
==================================================================================
| -x     | xtrace   | 與-v相似, 但打印完整的命令
==================================================================================
| -      | (none)   | 選項列表結束的標誌. 後面的參數是位置參數.
==================================================================================
| --     | (none)   | 釋放位置參數. 如果參數列表被指定了(-- arg1 arg2),
|      |    | 則位置參數被依次設置爲參數列表中的值.
==================================================================================
第31章 Gotchas
===============
Turandot: Gli enigmi sono tre, la morte una!
Caleph: No, no! Gli enigmi sono tre, una la vita!
            Puccini
將保留字和字符聲明爲變量名.
   1 case=value0       # 引發錯誤.
   2 23skidoo=value1   # 也會有錯誤.
   3 # 以數字開頭的變量名是由shell保留使用的.
   4 # 試試 _23skidoo=value1. 用下劃線開頭的變量名是允許的.
   5
   6 # 但是 . . .   僅使用下劃線來用做變量名也是不行的.
http://www.818198.com  Page 461
SHELL十三問
   7 _=25
   8 echo $_           # $_ 是一個特殊的變量,被設置爲最後命令的最後一個參數.
   9
  10 xyz((!*=value2    # 引起嚴重的錯誤.
  11 # 在第三版的Bash, 標點不能在變量名中出現.
用連字符或其他保留字符當做變量名(或函數名).
   1 var-1=23
   2 # 用 'var_1' 代替.
   3
   4 function-whatever ()   # 錯誤
   5 # 用 'function_whatever ()' 代替.
   6
   7 
   8 # 在第三版的 Bash, 標點不能在函數名中使用.
   9 function.whatever ()   # 錯誤
  10 # 用 'functionWhatever ()' 代替.
給變量和函數使用相同的名字. 這會使腳本不能分辨兩者.
   1 do_something ()
   2 {
   3   echo "This function does something with \"$1\"."
   4 }
   5
   6 do_something=do_something
   7
   8 do_something do_something
   9
  10 # 這些都是合法的,但讓人混淆.
不適當地使用寬白符(whitespace). 和其它的編程語言相比,Bash非常講究空白字符的使用.
   1 var1 = 23   # 'var1=23' 正確.
   2 # 上面一行,Bash試圖執行命令"var1"
   3 # 並且它的參數是"="和"23".
   4 
   5 let c = $a - $b   # 'let c=$a-$b' 或 'let "c = $a - $b"'是正確的.
   6
   7 if [ $a -le 5]    # if [ $a -le 5 ]   是正確的.
   8 # if [ "$a" -le 5 ]   會更好.
   9 # [[ $a -le 5 ]] 也可以.
未初始化的變量(指賦值前的變量)被認爲是NULL值的,而不是有零值.
   1 #!/bin/bash
   2
   3 echo "uninitialized_var = $uninitialized_var"
   4 # uninitialized_var =
混淆測試裏的= 和 -eq 操作符. 請記住, = 是比較字符變量而 -eq 比較整數.
   1 if [ "$a" = 273 ]      # $a 是一個整數還是一個字符串?
   2 if [ "$a" -eq 273 ]    # 如果$a 是一個整數,用這個表達式.
   3
   4 # 有時你能混用 -eq 和 = 而沒有不利的結果.
   5 # 然而 . . .
http://www.818198.com  Page 462
SHELL十三問
   6
   7
   8 a=273.0   # 不是一個整數.
   9    
  10 if [ "$a" = 273 ]
  11 then
  12   echo "Comparison works."
  13 else 
  14   echo "Comparison does not work."
  15 fi    # Comparison does not work.
  16
  17 # 與   a=" 273"  和 a="0273" 一樣.
  18
  19
  20 # 同樣, 問題仍然是試圖對非整數值使用 "-eq" 測試.
  21    
  22 if [ "$a" -eq 273.0 ]
  23 then
  24   echo "a = $a"
  25 fi  # 因錯誤信息而中斷. 
  26 # test.sh: [: 273.0: integer expression expected
誤用字符串比較操作符.
Example 31-1 數字和字符串比較是不相等同的
################################Start Script#######################################
 1 #!/bin/bash
 2 # bad-op.sh: 在整數比較中使用字符串比較.
 3
 4 echo
 5 number=1
 6
 7 # 下面的 "while" 循環有兩個錯誤:
 8 #+ 一個很明顯,另一個比較隱蔽.
 9
10 while [ "$number" < 5 ]    # 錯誤! 應該是:  while [ "$number" -lt 5 ]
11 do
12   echo -n "$number "
13   let "number += 1"
14 done 
15 #  嘗試運行時會收到錯誤信息而退出:
16 #+ bad-op.sh: line 10: 5: No such file or directory
17 #  在單括號裏, "<" 需要轉義,
18 #+ 而即使是如此, 對此整數比較它仍然是錯的.
19
20
21 echo "---------------------"
22
23
24 while [ "$number" \< 5 ]    #  1 2 3 4
http://www.818198.com  Page 463
SHELL十三問
25 do                          #
26   echo -n "$number "        #  看起來好像是能工作的, 但 . . .
27   let "number += 1"         #+ 它其實是在對 ASCII 碼的比較,
28 done                        #+ 而非是對數值的比較.
29
30 echo; echo "---------------------"
31
32 # 下面這樣便會引起問題了. 例如:
33
34 lesser=5
35 greater=105
36
37 if [ "$greater" \< "$lesser" ]
38 then
39   echo "$greater is less than $lesser"
40 fi                          # 105 is less than 5
41 #  事實上, "105" 小於 "5"
42 #+ 是因爲使用了字符串比較 (以ASCII碼的排序順序比較).
43
44 echo
45
46 exit 0
################################End Script#########################################
有時在測試時的方括號([ ])裏的變量需要引用起來(雙引號). 如果沒有這麼做可能會引起不
可預料的結果. 參考例子 7-6, 例子 16-5, 和 例子 9-6.
在腳本里的命令可能會因爲腳本沒有運行權限而導致運行失敗. 如果用戶不能在命令行裏調用
一個命令,即使把這個命令加到一個腳本中也一樣會失敗. 這時可以嘗試更改訪命令的屬性,
甚至可能給它設置suid位(當然是以root來設置).
試圖用 - 來做重定向操作(事實上它不是操作符)會導致令人討厭的意外.
   1 command1 2> - | command2  # 試圖把command1的錯誤重定向到一個管道里...
   2 #    ...不會工作.
   3
   4 command1 2>& - | command2  # 也沒有效果.
   5
   6 Thanks, S.C.
用 Bash 版本 2+ 的功能可以當有錯誤信息時引發修復動作. 老一些的 Linux機器可能默認的
安裝是 1.XX 版本的Bash.
   1 #!/bin/bash
   2
   3 minimum_version=2
   4 # 因爲 Chet Ramey 經常給Bash增加新的特性,
   5 # 你把 $minimum_version 設爲 2.XX比較合適,或者是其他合適的值.
   6 E_BAD_VERSION=80
   7
   8 if [ "$BASH_VERSION" \< "$minimum_version" ]
   9 then
  10   echo "This script works only with Bash, version $minimum or greater."
  11   echo "Upgrade strongly recommended."
http://www.818198.com  Page 464
SHELL十三問
  12   exit $E_BAD_VERSION
  13 fi
  14
  15 ...
在非Linux的機器上使用Bourne shell腳本(#!/bin/sh)的Bash專有功能可能會引起不可預料的
行爲. Linux系統通常都把sh 取別名爲 bash, 但在其他的常見的UNIX系統卻不一定是這樣.
使用Bash中沒有文檔化的屬性是危險的嘗試. 在這本書的前幾版中有幾個腳本依賴於exit或
return的值沒有限制不能用負整數(雖然限制了exit或return 的最大值是255). 不幸地是,
在版本 2.05b 以上這種情況就消失了. 參考See 例子 23-9.
一個帶有DOS風格新行符 (\r\n) 的腳本會執行失敗, 因爲#!/bin/bash\r\n 不是合法的,不同
於合法的#!/bin/bash\n. 解決辦法就是把腳本轉換成UNIX風格的新行符.
   1 #!/bin/bash
   2
   3 echo "Here"
   4
   5 unix2dos $0    # 腳本先把自己改成DOS格式.
   6 chmod 755 $0   # 更改回執行權限.
   7                # 'unix2dos'命令會刪除執行權限.
   8
   9 ./$0           # 腳本嘗試再次運行自己本身.
  10                # 但它是一個DOS文件而不會正常工作了.
  11
  12 echo "There"
  13
  14 exit 0
shell腳本以 #!/bin/sh 行開頭將不會在Bash兼容的模式下運行. 一些Bash專有的功能可能會
被禁用掉. 那些需要完全使用Bash專有擴展特性的腳本應該用#!/bin/bash開頭.
腳本里在 here document 的終結輸入的字符串前加入空白字符會引起不可預料的結果.
腳本不能export(導出)變量到它的父進程(parent process),或父進程的環境裏. 就像我
們學的生物一樣,一個子進程可以從父進程裏繼承但不能去影響父進程.
   1 WHATEVER=/home/bozo
   2 export WHATEVER
   3 exit 0
 bash$ echo $WHATEVER
 
 bash$
可以確定, 回到命令提示符, $WHATEVER 變量仍然沒有設置.
在子SHELL(subshell)設置和操作變量 , 然後嘗試在子SHELL的作用範圍外使用相同名的變
量將會導致非期望的結果.
Example 31-2 子SHELL缺陷
################################Start Script#######################################
 1 #!/bin/bash
 2 # 在子SHELL中的變量缺陷.
 3
 4 outer_variable=outer
 5 echo
 6 echo "outer_variable = $outer_variable"
 7 echo
http://www.818198.com  Page 465
SHELL十三問
 8
 9 (
10 # 子SHELL開始
11
12 echo "outer_variable inside subshell = $outer_variable"
13 inner_variable=inner  # Set
14 echo "inner_variable inside subshell = $inner_variable"
15 outer_variable=inner  # Will value change globally?
16 echo "outer_variable inside subshell = $outer_variable"
17
18 # 導出變量會有什麼不同嗎?
19 #    export inner_variable
20 #    export outer_variable
21 # 試試看.
22
23 # 子SHELL結束
24 )
25
26 echo
27 echo "inner_variable outside subshell = $inner_variable"  # Unset.
28 echo "outer_variable outside subshell = $outer_variable"  # Unchanged.
29 echo
30
31 exit 0
32
33 # 如果你沒有註釋第 19 和 20行會怎麼樣?
34 # 會有什麼不同嗎?
################################End Script#########################################
把 echo 的輸出用管道(Piping)輸送給read命令可能會產生不可預料的結果. 在這個情況下,
 read  表現地好像它是在一個子SHELL裏一樣. 可用set 命令代替 (就像在例子 11-16裏的一
樣).
Example 31-3 把echo的輸出用管道輸送給read命令
################################Start Script#######################################
 1 #!/bin/bash
 2 #  badread.sh:
 3 #  嘗試用 'echo 和 'read'
 4 #+ 來達到不用交互地給變量賦值的目的.
 5
 6 a=aaa
 7 b=bbb
 8 c=ccc
 9
10 echo "one two three" | read a b c
11 # 試圖重新給 a, b, 和 c賦值.
12
13 echo
14 echo "a = $a"  # a = aaa
15 echo "b = $b"  # b = bbb
http://www.818198.com  Page 466
SHELL十三問
16 echo "c = $c"  # c = ccc
17 # 重新賦值失敗.
18
19 # ------------------------------20
21 # 用下面的另一種方法.
22
23 var=`echo "one two three"`
24 set -- $var
25 a=$1; b=$2; c=$3
26
27 echo "-------"
28 echo "a = $a"  # a = one
29 echo "b = $b"  # b = two
30 echo "c = $c"  # c = three
31 # 重新賦值成功.
32
33 # ------------------------------34
35 #  也請注意echo值到'read'命令裏是在一個子SHELL裏起作用的.
36 #  所以,變量的值只在子SHELL裏被改變了.
37
38 a=aaa          # 從頭開始.
39 b=bbb
40 c=ccc
41
42 echo; echo
43 echo "one two three" | ( read a b c;
44 echo "Inside subshell: "; echo "a = $a"; echo "b = $b"; echo "c = $c" )
45 # a = one
46 # b = two
47 # c = three
48 echo "-----------------"
49 echo "Outside subshell: "
50 echo "a = $a"  # a = aaa
51 echo "b = $b"  # b = bbb
52 echo "c = $c"  # c = ccc
53 echo
54
55 exit 0
################################End Script#########################################
事實上, 也正如 Anthony Richardson 指出的那樣, 管道任何的數據到循環裏都會引起相似的
問題.
   1 # 循環管道問題.
   2 #  Anthony Richardson編寫此例,
   3 #+ Wilbert Berendsen補遺此例.
   4
   5
http://www.818198.com  Page 467
SHELL十三問
   6 foundone=false
   7 find $HOME -type f -atime +30 -size 100k |
   8 while true
   9 do
  10    read f
  11    echo "$f is over 100KB and has not been accessed in over 30 days"
  12    echo "Consider moving the file to archives."
  13    foundone=true
  14    # ------------------------------------  15    echo "Subshell level = $BASH_SUBSHELL"
  16    # Subshell level = 1
  17    # 沒錯, 現在是在子shell裏頭運行.
  18    # ------------------------------------  19 done
  20   
  21 #  foundone 變量在此總是有false值
  22 #+ 因此它是在子SHELL裏被設爲true值的
  23 if [ $foundone = false ]
  24 then
  25    echo "No files need archiving."
  26 fi
  27
  28 # =====================現在, 使用正確的方法:=================
  29
  30 foundone=false
  31 for f in $(find $HOME -type f -atime +30 -size 100k)  # 沒有使用管道.
  32 do
  33    echo "$f is over 100KB and has not been accessed in over 30 days"
  34    echo "Consider moving the file to archives."
  35    foundone=true
  36 done
  37   
  38 if [ $foundone = false ]
  39 then
  40    echo "No files need archiving."
  41 fi
  42
  43 # ==================另一種方法==================
  44
  45 #  腳本中讀變量值的相應部分替換在代碼塊裏頭讀變量,
  46 #+ 這使變量能在相同的子SHELL裏共享了.
  47 #  Thank you, W.B.
  48
  49 find $HOME -type f -atime +30 -size 100k | {
  50      foundone=false
  51      while read f
  52      do
  53        echo "$f is over 100KB and has not been accessed in over 30 days"
http://www.818198.com  Page 468
SHELL十三問
  54        echo "Consider moving the file to archives."
  55        foundone=true
  56      done
  57
  58      if ! $foundone
  59      then
  60        echo "No files need archiving."
  61      fi
  62 }
相關的問題是:當嘗試寫 tail -f 的輸出給管道並傳遞給grep時會發生問題.
   1 tail -f /var/log/messages | grep "$ERROR_MSG" >> error.log
   2 # "error.log"文件裏將不會寫入任何東西.
--
在腳本中使用"suid" 的命令是危險的, 因爲這會危及系統安全. [1]
用shell編寫CGI程序是值得商榷的. Shell腳本的變量不是"類型安全的", 這樣它用於CGI連接
使用時會引發不希望的結果. 其次, 它很難防範駭客的攻擊.
Bash 不能正確處理雙斜線 (//) 字符串.
在Linux 或 BSD上寫的Bash腳本可能需要修正以使它們也能在商業的UNIX (或 Apple OSX)上運
行. 這些腳本常使用比一般的UNIX系統上的同類工具更強大功能的GNU 命令和過濾工具. 這方
面一個明顯的例子是文本處理工具tr.
         Danger is near thee --
         Beware, beware, beware, beware.
         Many brave hearts are asleep in the deep.
         So beware --
         Beware.
             A.J. Lamb and H.W. Petrie
注意事項:
[1]  給腳本設置suid 權限是沒有用的.
第32章 腳本編程風格
====================
寫腳本時養成結構化和系統方法的習慣. 即使你在信封背後隨便做一下草稿也是有益的,要養
成在寫代碼前花幾分鐘來規劃和組織你的想法.
這兒是一些風格的指南. 注意這節文檔不是想成爲一個官方Shell編程風格.
32.1. 非官方的Shell腳本風格
---------------------------* 註釋你的代碼.這會使你的代碼更容易讓別人理解和賞識,同時也便於你維護.
    1 PASS="$PASS${MATRIX:$(($RANDOM%${#MATRIX})):1}"
    2 # 當你去年寫下這句代碼時非常的瞭解它在幹什麼事,但現在它完全是一個謎.
    3 # (摘自 Antek Sawicki的"pw.sh" 腳本.)
 給腳本和函數加上描述性的頭部信息.
    1 #!/bin/bash
    2
    3 #************************************************#
    4 #                   xyz.sh                       #
    5 #           written by Bozo Bozeman              #
    6 #                July 05, 2001                   #
    7 #                                                #
    8 #                   清除項目文件.                #
http://www.818198.com  Page 469
SHELL十三問
    9 #************************************************#
   10
   11 E_BADDIR=65                       # 沒有那樣的目錄.
   12 projectdir=/home/bozo/projects    # 要清除的目錄.
   13
   14 # --------------------------------------------------------- #
   15 # cleanup_pfiles ()                                         #
   16 # 刪除指定目錄裏的所有文件.                                 #
   17 # 參數: $target_directory                                   #
   18 # 返回: 成功返回0 , 失敗返回$E_BADDIR值.                    #
   19 # --------------------------------------------------------- #
   20 cleanup_pfiles ()
   21 {
   22   if [ ! -d "$1" ]  # 測試目標目錄是否存在.
   23   then
   24     echo "$1 is not a directory."
   25     return $E_BADDIR
   26   fi
   27
   28   rm -f "$1"/*
   29   return 0   # 成功.
   30 } 
   31
   32 cleanup_pfiles $projectdir
   33
   34 exit 0
 確認 #!/bin/bash 在腳本的第一行,在任何頭部註釋行之前.
* 避免使用 "魔數,"  [1]  它是硬編碼的字符常量. 用有意義的變量名來代替. 這使腳本
  更容易理解並允許在不破壞應用的情況下做改變和更新.
    1 if [ -f /var/log/messages ]
    2 then
    3   ...
    4 fi
    5 # 一年以後,你決定讓腳本改爲檢查 /var/log/syslog.
    6 # 那麼現在就需要你手動修改腳本里每一處的要改動的代碼,
    7 # 希望不要有你疏漏的地方.
    8
    9 # 更好的辦法是:
   10 LOGFILE=/var/log/messages  # 只需要改動一行.
   11 if [ -f "$LOGFILE" ]
   12 then
   13   ...
   14 fi
* 爲變量和函數選擇描述性的名字.
    1 fl=`ls -al $dirname`                 # 含義含糊.
    2 file_listing=`ls -al $dirname`       # 更好的名字.
    3
    4
http://www.818198.com  Page 470
SHELL十三問
    5 MAXVAL=10   # 同一個腳本所有程序代碼使用腳本常量.
    6 while [ "$index" -le "$MAXVAL" ]
    7 ...
    8
    9
   10 E_NOTFOUND=75                        #  把錯誤代碼的代表的變量名大寫U,
   11                                      # +並以"E_"開頭.
   12 if [ ! -e "$filename" ]
   13 then
   14   echo "File $filename not found."
   15   exit $E_NOTFOUND
   16 fi 
   17
   18
   19 MAIL_DIRECTORY=/var/spool/mail/bozo  # 環境變量名用大寫.
   20 export MAIL_DIRECTORY
   21
   22
   23 GetAnswer ()                         # 函數名用適當的大小寫混合組成.
   24 {
   25   prompt=$1
   26   echo -n $prompt
   27   read answer
   28   return $answer
   29 } 
   30
   31 GetAnswer "What is your favorite number? "
   32 favorite_number=$?
   33 echo $favorite_number
   34
   35
   36 _uservariable=23                     # 語法允許, 但不推薦.
   37 # 用戶定義的變量最好不要用下劃線開頭.
   38 # 把這個留給系統變量使用更好.
* 用有含義和系統的方法來使用退出代碼(exit codes).
    1 E_WRONG_ARGS=65
    2 ...
    3 ...
    4 exit $E_WRONG_ARGS
 也參考附錄 D.
 最後 建議在腳本中使用/usr/include/sysexits.h的退出碼, 雖然它們主要由 C 和 C++
 語言編程時使用.
* 使用標準的參數選項. 最後 建議使用下面一組參數標誌.
    1 -a      All: Return all information (including hidden file info).
    2 -b      Brief: Short version, usually for other scripts.
    3 -c      Copy, concatenate, etc.
    4 -d      Daily: Use information from the whole day, and not merely
    5         information for a specific instance/user.
http://www.818198.com  Page 471
SHELL十三問
    6 -e      Extended/Elaborate: (often does not include hidden file info).
    7 -h      Help: Verbose usage w/descs, aux info, discussion, help.
    8         See also -V.
    9 -l      Log output of script.
   10 -m      Manual: Launch man-page for base command.
   11 -n      Numbers: Numerical data only.
   12 -r      Recursive: All files in a directory (and/or all sub-dirs).
   13 -s      Setup & File Maintenance: Config files for this script.
   14 -u      Usage: List of invocation flags for the script.
   15 -v      Verbose: Human readable output, more or less formatted.
   16 -V      Version / License / Copy(right|left) / Contribs (email too).
 也參考附錄 F.
* 把複雜的腳本分割成簡單一些的模塊. 用合適的函數來實現各個功能. 參考例子 34-4.
* 如果有簡單的結構可以使用,不要使用複雜的結構.
    1 COMMAND
    2 if [ $? -eq 0 ]
    3 ...
    4 # 多餘的並且也不直接明瞭.
    5
    6 if COMMAND
    7 ...
    8 # 更簡練 (或者可能會損失一些可讀性).
     ... reading the UNIX source code to the Bourne shell
     (/bin/sh). I was shocked at how much simple algorithms could
     be made cryptic, and therefore useless, by a poor choice of
     code style. I asked myself, "Could someone be proud of this
     code?"
                 Landon Noll
注意事項:
[1]  在上下文, "魔數" 和用來指明文件類型的 魔數(magic numbers)有完全不同的意思.
第33章 雜項
============
    Nobody really knows what the Bourne shell's grammar is. Even
    examination of the source code is little help.
               Tom Duff
33.1. 交互式和非交互式的shells和腳本
------------------------------------交互式的shell在 tty終端從用戶的輸入中讀取命令. 另一方面, shell能在啓動時讀取啓動文
件,顯示一個提示符並默認激活作業控制. 用戶能交互地使用shell.
運行腳本的shell一般都是非交互的shell. 但腳本仍然可以存取它擁有的終端. 腳本里甚至可
以仿效成可交互的shell.
   1 #!/bin/bash
   2 MY_PROMPT='$ '
   3 while :
   4 do
   5   echo -n "$MY_PROMPT"
   6   read line
   7   eval "$line"
http://www.818198.com  Page 472
SHELL十三問
   8   done
   9
  10 exit 0
  11
  12 # 這個例子腳本, 和上面的解釋由
  13 # Stéphane Chazelas 提供(再次感謝).
讓我們考慮一個要求用戶交互式輸入的腳本,通常用read語句 (參考例子 11-3). 真正的情況
可能有些混亂.以現在假設的情況來說,交互式腳本被限制在一個tty設備上,它本身已經是從
一個控制終端或一箇中被用戶調用的.
初始化和啓動腳本不必是非交互式的,因爲它們必須不需要人爲地干預地運行.許多管理和系
統維護腳本也同樣是非交互式的.不多變的重複性的任務可以自動地由非交互式腳本完成.
非交互式的腳本可以在後臺運行,但交互腳本在後臺運行則會被掛起,等待永遠不會到達的輸
入.解決這個難點的辦法可以寫預料這種情況的腳本或是內嵌here document 的腳本來獲取腳
本期望的輸入,這樣就可作爲後臺任務運行了.在最簡單的情況,重定向一個文件給一個read
語句提供輸入(read variable <file). 這就可能適應交互和非交互的工作環境下都能達成腳
本運行的目的.
如果腳本需要測試當前是否運行在交互shell中,一個簡單的辦法是找一下是否有提示符變量,
即$PS1是否設置了. (如果腳本需要用戶輸入數據,則腳本會顯示一個提示符.)
   1 if [ -z $PS1 ] # 沒有提示符?
   2 then
   3   # 非交互式
   4   ...
   5 else
   6   # 交互式
   7   ...
   8 fi
另一個辦法是腳本可以測試是否在變量$-中出現了選項"i".
   1 case $- in
   2 *i*)    # 交互式 shell
   3 ;;
   4 *)      # 非交互式 shell
   5 ;;
   6 # (Courtesy of "UNIX F.A.Q.," 1993)
注意: 腳本可以使用-i選項強制在交互式模式下運行或腳本頭用#!/bin/bash -i. 注意這樣
  可能會引起腳本古怪的行爲或當沒有錯誤出現時也會顯示錯誤信息.
33.2. Shell 包裝
----------------包裝腳本是指嵌有一個系統命令和程序的腳本,也保存了一組傳給該命令的參數. [1]  包裝
腳本使原本很複雜的命令行簡單化. 這對 sed 和 awk 特別有用.
sed 和 awk 命令一般從命令行上以 sed -e 'commands' 和 awk 'commands' 來調用. 把sed
和awk的命令嵌入到Bash腳本里使調用變得更簡單, 並且也可多次使用. 也可以綜合地利用
sed 和 awk 的功能, 例如管道(piping)連接sed 命令的輸出到awk命令中. 保存爲可執行的
文件, 你可以用腳本編寫的或修改的調用格式多次的調用它, 而不必在命令行上重複鍵入複雜
的命令行.
Example 33-1 shell 包裝
################################Start Script#######################################
 1 #!/bin/bash
 2
http://www.818198.com  Page 473
SHELL十三問
 3 # 這是一個把文件中的空行刪除的簡單腳本.
 4 # 沒有參數檢查.
 5 #
 6 # 你可能想增加類似下面的代碼:
 7 #
 8 # E_NOARGS=65
 9 # if [ -z "$1" ]
10 # then
11 #  echo "Usage: `basename $0` target-file"
12 #  exit $E_NOARGS
13 # fi
14
15
16 # 就像從命令行調用下面的命令:
17 #    sed -e '/^$/d' filename
18 #
19
20 sed -e /^$/d "$1"
21 #  The '-e' 意味着後面跟的是編輯命令 (這是可選的).
22 #  '^' 匹配行的開頭, '$' 則是行的結尾.
23 #  這個表達式匹配行首和行尾之間什麼也沒有的行,
24 #+ 即空白行.
25 #  'd'是刪除命令.
26
27 #  引號引起命令行參數就允許在文件名中使用空白字符和特殊字符
28 #
29
30 #  注意這個腳本不能真正的修改目標文件.
31 #  如果你需要保存修改,就要重定向到某個輸出文件裏.
32
33 exit 0
################################End Script#########################################
Example 33-2 稍微複雜一些的shell包裝
################################Start Script#######################################
 1 #!/bin/bash
 2
 3 #  "subst", 把一個文件中的一個模式替換成一個模式的腳本
 4 #
 5 #  例如, "subst Smith Jones letter.txt".
 6
 7 ARGS=3         # 腳本要求三個參數.
 8 E_BADARGS=65   # 傳遞了錯誤的參數個數給腳本.
 9
10 if [ $# -ne "$ARGS" ]
11 # 測試腳本參數的個數 (這是好辦法).
12 then
13   echo "Usage: `basename $0` old-pattern new-pattern filename"
14   exit $E_BADARGS
http://www.818198.com  Page 474
SHELL十三問
15 fi
16
17 old_pattern=$1
18 new_pattern=$2
19
20 if [ -f "$3" ]
21 then
22     file_name=$3
23 else
24     echo "File \"$3\" does not exist."
25     exit $E_BADARGS
26 fi
27
28
29 #  這兒是實現功能的代碼.
30
31 # -----------------------------------------------32 sed -e "s/$old_pattern/$new_pattern/g" $file_name
33 # -----------------------------------------------34
35 #  's' 在sed命令裏表示替換,
36 #+ /pattern/表示匹配地址.
37 #  The "g"也叫全局標誌使sed會在每一行有$old_pattern模式出現的所有地方替換,
38 #+ 而不只是匹配第一個出現的地方.
39 #  參考'sed'的有關書籍瞭解更深入的解釋.
40
41 exit 0    # 腳本成功調用會返回 0.
################################End Script#########################################
Example 33-3 寫到日誌文件的shell包裝
################################Start Script#######################################
 1 #!/bin/bash
 2 #  普通的shell包裝,執行一個操作並記錄在日誌裏
 3 #
 4
 5 # 需要設置下面的兩個變量.
 6 OPERATION=
 7 #         可以是一個複雜的命令鏈,
 8 #+        例如awk腳本或是管道 . . .
 9 LOGFILE=
10 #         不管怎麼樣,命令行參數還是要提供給操作的.
11
12
13 OPTIONS="$@"
14
15
16 # 記錄操作.
17 echo "`date` + `whoami` + $OPERATION "$@"" >> $LOGFILE
18 # 現在, 執行操作.
http://www.818198.com  Page 475
SHELL十三問
19 exec $OPERATION "$@"
20
21 # 在操作之前記錄日誌是必須的.
22 # 爲什麼?
################################End Script#########################################
Example 33-4 包裝awk的腳本
################################Start Script#######################################
 1 #!/bin/bash
 2 # pr-ascii.sh: 打印 ASCII 碼的字符表.
 3
 4 START=33   # 可打印的 ASCII 字符的範圍 (十進制).
 5 END=125
 6
 7 echo " Decimal   Hex     Character"   # 表頭.
 8 echo " -------   ---     ---------"
 9
10 for ((i=START; i<=END; i++))
11 do
12   echo $i | awk '{printf("  %3d       %2x         %c\n", $1, $1, $1)}'
13 # 在這個上下文,不會運行Bash的內建printf命令:
14 #     printf "%c" "$i"
15 done
16
17 exit 0
18
19
20 #  Decimal   Hex     Character
21 #  -------   ---     ---------22 #    33       21         !
23 #    34       22         "
24 #    35       23         #
25 #    36       24         $
26 #
27 #    . . .
28 #
29 #   122       7a         z
30 #   123       7b         {
31 #   124       7c         |
32 #   125       7d         }
33
34
35 #  把腳本的輸出重定向到一個文件或是管道給more命令來查看:
36 #+   sh pr-asc.sh | more
################################End Script#########################################
Example 33-5 另一個包裝awk的腳本
################################Start Script#######################################
 1 #!/bin/bash
 2
http://www.818198.com  Page 476
SHELL十三問
 3 # 給目標文件增加一列由數字指定的列.
 4
 5 ARGS=2
 6 E_WRONGARGS=65
 7
 8 if [ $# -ne "$ARGS" ] # 檢查命令行參數個數是否正確.
 9 then
10    echo "Usage: `basename $0` filename column-number"
11    exit $E_WRONGARGS
12 fi
13
14 filename=$1
15 column_number=$2
16
17 #  傳遞shell變量給腳本的awk部分需要一點技巧.
18 #  方法之一是在awk腳本中使用強引用來引起bash腳本的變量
19 #
20 #     $'$BASH_SCRIPT_VAR'
21 #      ^                ^
22 #  這個方法在下面的內嵌的awk腳本中出現.
23 #  參考awk文檔瞭解更多的細節.
24
25 # 多行的awk腳本調用格式爲:  awk ' ..... '
26
27
28 # 開始 awk 腳本.
29 # -----------------------------30 awk '
31
32 { total += $'"${column_number}"'
33 }
34 END {
35      print total
36 }    
37
38 ' "$filename"
39 # -----------------------------40 # awk腳本結束.
41
42
43 #   把shell變量傳遞給awk變量可能是不安全的,
44 #+  因此Stephane Chazelas提出了下面另外一種方法:
45 #   ---------------------------------------46 #   awk -v column_number="$column_number" '
47 #   { total += $column_number
48 #   }
49 #   END {
50 #       print total
http://www.818198.com  Page 477
SHELL十三問
51 #   }' "$filename"
52 #   ---------------------------------------53
54
55 exit 0
################################End Script#########################################
對於要實現這些功能而只用一種多合一的瑞士軍刀應該用Perl. Perl兼有sed和awk的能力, 並
且具有C的一個很大的子集. 它是標準的並支持面向對象編程的方方面面,甚至是很瑣碎的東
西. 短的Perl腳本也可以嵌入到shell腳本中去,以至於有些人宣稱Perl能夠完全地代替shell
編程(本文作者對此持懷疑態度).
Example 33-6 把Perl嵌入Bash腳本
################################Start Script#######################################
 1 #!/bin/bash
 2
 3 # Shell命令可以包含 Perl 腳本.
 4 echo "This precedes the embedded Perl script within \"$0\"."
 5 echo "==============================================================="
 6
 7 perl -e 'print "This is an embedded Perl script.\n";'
 8 # 像sed腳本, Perl 也使用"-e"選項.
 9
10 echo "==============================================================="
11 echo "However, the script may also contain shell and system commands."
12
13 exit 0
################################End Script#########################################
把Bash腳本和Perl腳本放在同一個文件是可能的. 依賴於腳本如何被調用, 要麼是Bash部分被
執行,要麼是Perl部分被執行.
Example 33-7 Bash 和 Perl 腳本聯合使用
################################Start Script#######################################
 1 #!/bin/bash
 2 # bashandperl.sh
 3
 4 echo "Greetings from the Bash part of the script."
 5 # 下面可以有更多的Bash命令.
 6
 7 exit 0
 8 # 腳本的Bash部分結束.
 9
10 # =======================================================
11
12 #!/usr/bin/perl
13 # 腳本的這個部分必須用-x選項來調用.
14
15 print "Greetings from the Perl part of the script.\n";
16 # 下面可以有更多的Perl命令.
17
18 # 腳本的Perl部分結束.
http://www.818198.com  Page 478
SHELL十三問
################################End Script#########################################
 bash$ bash bashandperl.sh
 Greetings from the Bash part of the script.
 
 bash$ perl -x bashandperl.sh
 Greetings from the Perl part of the script.
注意事項:
[1]  事實上,相當數量的Linux軟件工具包是shell包裝腳本. 例如/usr/bin/pdf2ps,
  /usr/bin/batch, 和 /usr/X11R6/bin/xmkmf.
33.3. 測試和比較: 另一種方法
----------------------------對於測試,[[ ]]結構可能比[ ]更合適.同樣地,算術比較可能用(( ))結構更有用.
   1 a=8
   2
   3 # 下面所有的比較是等價的.
   4 test "$a" -lt 16 && echo "yes, $a < 16"         # "與列表"
   5 /bin/test "$a" -lt 16 && echo "yes, $a < 16"
   6 [ "$a" -lt 16 ] && echo "yes, $a < 16"
   7 [[ $a -lt 16 ]] && echo "yes, $a < 16"          # 在[[ ]]和(( ))中不必用引號引起變量
   8 (( a < 16 )) && echo "yes, $a < 16"             #
   9
  10 city="New York"
  11 # 同樣,下面的所有比較都是等價的.
  12 test "$city" \< Paris && echo "Yes, Paris is greater than $city"  # 產生 ASCII 順序.
  13 /bin/test "$city" \< Paris && echo "Yes, Paris is greater than $city"
  14 [ "$city" \< Paris ] && echo "Yes, Paris is greater than $city"
  15 [[ $city < Paris ]] && echo "Yes, Paris is greater than $city"    # 不需要用引號引起$city.
  16
  17 # 多謝, S.C.
33.4. 遞歸
----------腳本是否能 遞歸地  調用自己本身? 當然可以.
Example 33-8 遞歸調用自己本身的(無用)腳本
################################Start Script#######################################
 1 #!/bin/bash
 2 # recurse.sh
 3
 4 #  腳本能否遞歸地調用自己?
 5 #  是的, 但這有什麼實際的用處嗎?
 6 #  (看下面的.)
 7
 8 RANGE=10
 9 MAXVAL=9
10
11 i=$RANDOM
12 let "i %= $RANGE"  # 產生一個從 0 到 $RANGE - 1 之間的隨機數.
13
14 if [ "$i" -lt "$MAXVAL" ]
http://www.818198.com  Page 479
SHELL十三問
15 then
16   echo "i = $i"
17   ./$0             #  腳本遞歸地調用再生成一個和自己一樣的實例.
18 fi                 #  每個子腳本做的事都一樣,
19                    #+ 直到產生的變量 $i 和變量 $MAXVAL 相等.
20
21 #  用"while"循環代替"if/then"測試會引起錯誤.
22 #  解釋爲什麼會這樣.
23
24 exit 0
25
26 # 注:
27 # ----28 # 腳本要正確地工作必須有執行權限.
29 # 這是指用"sh"命令來調用這個腳本而沒有設置正確權限導致的問題.
30 # 請解釋原因.
################################End Script#########################################
Example 33-9 遞歸調用自己本身的(有用)腳本
################################Start Script#######################################
 1 #!/bin/bash
 2 # pb.sh: 電話本(phone book)
 3
 4 # 由Rick Boivie編寫,已得到使用許可.
 5 # 由ABS文檔作者修改.
 6
 7 MINARGS=1     #  腳本需要至少一個參數.
 8 DATAFILE=./phonebook
 9               #  在當前目錄下名爲"phonebook"的數據文件必須存在
10               #
11 PROGNAME=$0
12 E_NOARGS=70   #  沒有參數的錯誤值.
13
14 if [ $# -lt $MINARGS ]; then
15       echo "Usage: "$PROGNAME" data"
16       exit $E_NOARGS
17 fi     
18
19
20 if [ $# -eq $MINARGS ]; then
21       grep $1 "$DATAFILE"
22       #  如果$DATAFILE文件不存在,'grep' 會打印一個錯誤信息.
23 else
24       ( shift; "$PROGNAME" $* ) | grep $1
25       # 腳本遞歸調用本身.
26 fi
27
28 exit 0        #  腳本在這兒退出.
29               #  因此Therefore, 從這行開始可以寫沒有#開頭的的註釋行
http://www.818198.com  Page 480
SHELL十三問
30        #
31
32 # ------------------------------------------------------------------------33 "phonebook"文件的例子:
34
35 John Doe        1555 Main St., Baltimore, MD 21228          (410) 222-3333
36 Mary Moe        9899 Jones Blvd., Warren, NH 03787          (603) 898-3232
37 Richard Roe     856 E. 7th St., New York, NY 10009          (212) 333-4567
38 Sam Roe         956 E. 8th St., New York, NY 10009          (212) 444-5678
39 Zoe Zenobia     4481 N. Baker St., San Francisco, SF 94338  (415) 501-1631
40 # ------------------------------------------------------------------------41
42 $bash pb.sh Roe
43 Richard Roe     856 E. 7th St., New York, NY 10009          (212) 333-4567
44 Sam Roe         956 E. 8th St., New York, NY 10009          (212) 444-5678
45
46 $bash pb.sh Roe Sam
47 Sam Roe         956 E. 8th St., New York, NY 10009          (212) 444-5678
48
49 #  當超過一個參數傳給這個腳本時,
50 #+ 它只打印包含所有參數的行.
################################End Script#########################################
Example 33-10 另一個遞歸調用自己本身的(有用)腳本
################################Start Script#######################################
 1 #!/bin/bash
 2 # usrmnt.sh, 由Anthony Richardson編寫
 3 # 得到允許在此使用.
 4
 5 # usage:       usrmnt.sh
 6 # 描述: 掛載設備, 調用者必須列在/etc/sudoers文件的MNTUSERS組裏
 7 #
 8
 9 # ----------------------------------------------------------10 #  這是一個用戶掛載設備的腳本,它用sudo來調用自己.
11 #  只有擁有合適權限的用戶才能用
12
13 #   usermount /dev/fd0 /mnt/floppy
14
15 # 來代替
16
17 #   sudo usermount /dev/fd0 /mnt/floppy
18
19 #  我使用相同的技術來處理我所有的sudo腳本,
20 #+ 因爲我覺得它很方便.
21 # ----------------------------------------------------------22
23 #  如果 SUDO_COMMAND 變量沒有設置,我們不能通過sudo來運行腳本本身.
24 #+ 傳遞用戶的真實ID和組ID . . .
http://www.818198.com  Page 481
SHELL十三問
25
26 if [ -z "$SUDO_COMMAND" ]
27 then
28    mntusr=$(id -u) grpusr=$(id -g) sudo $0 $*
29    exit 0
30 fi
31
32 # 如果我們以sudo來調用運行,就會運行這兒.
33 /bin/mount $* -o uid=$mntusr,gid=$grpusr
34
35 exit 0
36
37 # 附註 (由腳本作者加註):
38 # -------------------------------------------------39
40 # 1) Linux允許在/etc/fstab文件中使用"users"選項
41 #    以使任何用戶能掛載可移動的介質.
42 #    但是, 在一個服務器上,
43 #    我只想有限的幾個用戶可以存取可移動介質.
44 #    我發現使用sudo可以有更多的控制.
45
46 # 2) 我也發現sudo能通過組更方便地達成目的.
47 #
48
49 # 3) 這個方法使給予任何想給合適權限的人使用mount命令
50 #    所以要小心使用.
51 #    你也可以開發類似的腳本mntfloppy, mntcdrom,和 mntsamba來使mount命令得到更好的控制
52 #
53 #
54 #
################################End Script#########################################
注意: 過多層次的遞歸調用會耗盡腳本的堆棧空間,會引起段錯誤.
33.5. 彩色腳本
--------------ANSI [1]  定義了屏幕屬性的轉義序列集合,例如粗體文本,背景和前景顏色. DOS批處理文
件(batch files) 一般使用ANSI的轉義代碼來控制色彩輸出,Bash腳本也是這麼做的.
Example 33-11 一個 "彩色的" 地址資料庫
################################Start Script#######################################
 1 #!/bin/bash
 2 # ex30a.sh: ex30.sh的"彩色" 版本.
 3 #            沒有加工處理的地址資料庫
 4
 5
 6 clear                                   # 清除屏幕.
 7
 8 echo -n "          "
 9 echo -e '\E[37;44m'"\033[1mContact List\033[0m"
10                                         # 白色爲前景色,藍色爲背景色
http://www.818198.com  Page 482
SHELL十三問
11 echo; echo
12 echo -e "\033[1mChoose one of the following persons:\033[0m"
13                                         # 粗體
14 tput sgr0
15 echo "(Enter only the first letter of name.)"
16 echo
17 echo -en '\E[47;34m'"\033[1mE\033[0m"   # 藍色
18 tput sgr0                               # 把色彩設置爲"常規"
19 echo "vans, Roland"                     # "[E]vans, Roland"
20 echo -en '\E[47;35m'"\033[1mJ\033[0m"   # 紅紫色
21 tput sgr0
22 echo "ones, Mildred"
23 echo -en '\E[47;32m'"\033[1mS\033[0m"   # 綠色
24 tput sgr0
25 echo "mith, Julie"
26 echo -en '\E[47;31m'"\033[1mZ\033[0m"   # 紅色
27 tput sgr0
28 echo "ane, Morris"
29 echo
30
31 read person
32
33 case "$person" in
34 # 注意變量被引起來了.
35
36   "E" | "e" )
37   # 接受大小寫的輸入.
38   echo
39   echo "Roland Evans"
40   echo "4321 Floppy Dr."
41   echo "Hardscrabble, CO 80753"
42   echo "(303) 734-9874"
43   echo "(303) 734-9892 fax"
44   echo "[email protected]"
45   echo "Business partner & old friend"
46   ;;
47
48   "J" | "j" )
49   echo
50   echo "Mildred Jones"
51   echo "249 E. 7th St., Apt. 19"
52   echo "New York, NY 10009"
53   echo "(212) 533-2814"
54   echo "(212) 533-9972 fax"
55   echo "[email protected]"
56   echo "Girlfriend"
57   echo "Birthday: Feb. 11"
58   ;;
http://www.818198.com  Page 483
SHELL十三問
59
60 # 稍後爲 Smith 和 Zane 增加信息.
61
62           * )
63    # 默認選項Default option.  
64    # 空的輸入(直接按了回車) 也會匹配這兒.
65    echo
66    echo "Not yet in database."
67   ;;
68
69 esac
70
71 tput sgr0                               # 把色彩重設爲"常規".
72
73 echo
74
75 exit 0
################################End Script#########################################
Example 33-12 畫盒子
################################Start Script#######################################
  1 #!/bin/bash
  2 # Draw-box.sh: 用ASCII字符畫一個盒子.
  3
  4 # Stefano Palmeri編寫,文檔作者作了少量編輯.
  5 # 徵得作者同意在本書使用.
  6
  7
  8 ######################################################################
  9 ###  draw_box 函數的註釋  ###
 10
 11 #  "draw_box" 函數使用戶可以在終端上畫一個盒子.
 12 #
 13 #
 14 #  用法: draw_box ROW COLUMN HEIGHT WIDTH [COLOR]
 15 #  ROW 和 COLUMN 定位要畫的盒子的左上角.      
 16 #
 17 #  ROW 和 COLUMN 必須要大於0且小於目前終端的尺寸.
 18 #
 19 #  HEIGHT 是盒子的行數,必須 > 0.
 20 #  HEIGHT + ROW 必須 <= 終端的高度.
 21 #  WIDTH 是盒子的列數,必須 > 0.
 22 #  WIDTH + COLUMN 必須 <= 終端的寬度.
 23 #
 24 #  例如: 如果你當前終端的尺寸是 20x80,
 25 #  draw_box 2 3 10 45 是合法的
 26 #  draw_box 2 3 19 45 的 HEIGHT 值是錯的 (19+2 > 20)
 27 #  draw_box 2 3 18 78 的 WIDTH 值是錯的 (78+3 > 80)
 28 #
http://www.818198.com  Page 484
SHELL十三問
 29 #  COLOR 是盒子邊框的顏色.
 30 #  它是第5個參數,並且它是可選的.
 31 #  0=黑色 1=紅色 2=綠色 3=棕褐色 4=藍色 5=紫色 6=青色 7=白色.
 32 #  如果你傳給這個函數錯的參數,
 33 #+ 它就會以代碼65退出,
 34 #+ 沒有其他的信息打印到標準出錯上.
 35 #
 36 #  在畫盒子之前要清屏.
 37 #  函數內不包含有清屏命令.
 38 #  這使用戶可以畫多個盒子,甚至疊接多個盒子.
 39
 40 ###  draw_box 函數註釋結束  ###
 41 ######################################################################
 42
 43 draw_box(){
 44
 45 #=============#
 46 HORZ="-"
 47 VERT="|"
 48 CORNER_CHAR="+"
 49
 50 MINARGS=4
 51 E_BADARGS=65
 52 #=============#
 53
 54
 55 if [ $# -lt "$MINARGS" ]; then                 # 如果參數小於4,退出.
 56     exit $E_BADARGS
 57 fi
 58
 59 # 搜尋參數中的非數字的字符.
 60 # 能用其他更好的辦法嗎 (留給讀者的練習?).
 61 if echo $@ | tr -d [:blank:] | tr -d [:digit:] | grep . &> /dev/null; then
 62    exit $E_BADARGS
 63 fi
 64
 65 BOX_HEIGHT=`expr $3 - 1`   #  -1 是需要的,因爲因爲邊角的"+"是高和寬共有的部分.
 66 BOX_WIDTH=`expr $4 - 1`    #
 67 T_ROWS=`tput lines`        #  定義當前終端長和寬的尺寸,
 68 T_COLS=`tput cols`         #
 69         
 70 if [ $1 -lt 1 ] || [ $1 -gt $T_ROWS ]; then    #  如果參數是數字就開始檢查有效性.
 71    exit $E_BADARGS                             #
 72 fi
 73 if [ $2 -lt 1 ] || [ $2 -gt $T_COLS ]; then
 74    exit $E_BADARGS
 75 fi
 76 if [ `expr $1 + $BOX_HEIGHT + 1` -gt $T_ROWS ]; then
http://www.818198.com  Page 485
SHELL十三問
 77    exit $E_BADARGS
 78 fi
 79 if [ `expr $2 + $BOX_WIDTH + 1` -gt $T_COLS ]; then
 80    exit $E_BADARGS
 81 fi
 82 if [ $3 -lt 1 ] || [ $4 -lt 1 ]; then
 83    exit $E_BADARGS
 84 fi                                 # 參數檢查完畢.
 85
 86 plot_char(){                       # 函數內的函數.
 87    echo -e "\E[${1};${2}H"$3
 88 }
 89
 90 echo -ne "\E[3${5}m"               # 如果傳遞了盒子邊框顏色參數,則設置它.
 91
 92 # start drawing the box
 93
 94 count=1                                         #  用plot_char函數畫垂直線
 95 for (( r=$1; count<=$BOX_HEIGHT; r++)); do      #
 96   plot_char $r $2 $VERT
 97   let count=count+1
 98 done
 99
100 count=1
101 c=`expr $2 + $BOX_WIDTH`
102 for (( r=$1; count<=$BOX_HEIGHT; r++)); do
103   plot_char $r $c $VERT
104   let count=count+1
105 done
106
107 count=1                                        #  用plot_char函數畫水平線
108 for (( c=$2; count<=$BOX_WIDTH; c++)); do      #
109   plot_char $1 $c $HORZ
110   let count=count+1
111 done
112
113 count=1
114 r=`expr $1 + $BOX_HEIGHT`
115 for (( c=$2; count<=$BOX_WIDTH; c++)); do
116   plot_char $r $c $HORZ
117   let count=count+1
118 done
119
120 plot_char $1 $2 $CORNER_CHAR                   # 畫盒子的角.
121 plot_char $1 `expr $2 + $BOX_WIDTH` +
122 plot_char `expr $1 + $BOX_HEIGHT` $2 +
123 plot_char `expr $1 + $BOX_HEIGHT` `expr $2 + $BOX_WIDTH` +
124
http://www.818198.com  Page 486
SHELL十三問
125 echo -ne "\E[0m"             #  恢復最初的顏色.
126
127 P_ROWS=`expr $T_ROWS - 1`    #  在終端的底部打印提示符.
128
129 echo -e "\E[${P_ROWS};1H"
130 }     
131
132
133 # 現在, 讓我們來畫一個盒子.
134 clear                       # 清屏.
135 R=2      # 行
136 C=3      # 列
137 H=10     # 高
138 W=45     # 寬
139 col=1    # 顏色(紅)
140 draw_box $R $C $H $W $col   # 畫盒子.
141
142 exit 0
143
144 # 練習:
145 # --------146 # 增加可以在盒子裏打印文本的選項
################################End Script#########################################
最簡單也可能是最有用的ANSI轉義序列是加粗文本, \033[1m ... \033[0m. \033 觸發轉義序
列, 而 "[1" 啓用加粗屬性, 而"[0" 表示切換回禁用加粗狀態. "m"則表示終止一個轉義序列.
 bash$ echo -e "\033[1mThis is bold text.\033[0m"
一種相似的轉義序列可切換下劃線效果 (在 rxvt 和 aterm 上).
 bash$ echo -e "\033[4mThis is underlined text.\033[0m"
       
注意: echo使用-e選項可以啓用轉義序列.
其他的轉義序列可用於更改文本或/和背景色彩.
 bash$ echo -e '\E[34;47mThis prints in blue.'; tput sgr0
 
 bash$ echo -e '\E[33;44m'"yellow text on blue background"; tput sgr0
 
 bash$ echo -e '\E[1;33;44m'"BOLD yellow text on blue background"; tput sgr0
       
注意: 通常爲淡色的前景色文本設置粗體效果是較好的.
tput sgr0 把終端設置恢復爲原樣. 如果省略這一句會使後續在該終端的輸出仍爲藍色.
注意: 因爲tput sgr0 在某些環境下不能恢復終端設置, echo -ne \E[0m 會是更好的選擇.
可以在有色的背景上用下面的模板寫有色彩的文本.
echo -e '\E[COLOR1;COLOR2mSome text goes here.'
"\E[" 開始轉義序列. 分號分隔的數值"COLOR1" 和 "COLOR2" 指定前景色和背景色, 數值和
色彩的對應參見下面的表格. (數值的順序不是有關係的,因爲前景色和背景色數值都落在不
重疊的範圍裏.) "m"終止該轉義序列, 然後文本以結束的轉義指定的屬性顯示.
也要注意到用單引號引用了echo -e後面的餘下命令序列.
下表的數值是在 rxvt 終端運行的結果. 具體效果可能在其他的各種終端上不一樣.
table 33-1. 轉義序列中數值和彩色的對應
http://www.818198.com  Page 487
SHELL十三問
==========================
| 色彩  | 前景色 | 背景色|
|  黑  |  30  |  40   |
|  紅  |  31  |  41   |
|  綠  |  32  |  42   |
|  黃  |  33  |  43   |
|  藍  |  34  |  44   |
|  洋紅 |  35  |  45   |
|  青  |  36  |  46   |
|  白  |  37  |  47   |
==========================
Example 33-13 顯示彩色文本
################################Start Script#######################################
 1 #!/bin/bash
 2 # color-echo.sh: 用彩色來顯示文本.
 3
 4 # 依照需要修改這個腳本.
 5 # 這比手寫彩色的代碼更容易一些.
 6
 7 black='\E[30;47m'
 8 red='\E[31;47m'
 9 green='\E[32;47m'
10 yellow='\E[33;47m'
11 blue='\E[34;47m'
12 magenta='\E[35;47m'
13 cyan='\E[36;47m'
14 white='\E[37;47m'
15
16
17 alias Reset="tput sgr0"      #  把文本屬性重設回原來沒有清屏前的
18                              #
19
20
21 cecho ()                     # Color-echo.
22                              # 參數 $1 = 要顯示的信息
23                              # 參數 $2 = 顏色
24 {
25 local default_msg="No message passed."
26                              # 不是非要一個本地變量.
27
28 message=${1:-$default_msg}   # 默認的信息.
29 color=${2:-$black}           # 如果沒有指定,默認使用黑色.
30
31   echo -e "$color"
32   echo "$message"
33   Reset                      # 重設文本屬性.
34
35   return
http://www.818198.com  Page 488
SHELL十三問
36 } 
37
38
39 # 現在,讓我們試試.
40 # ----------------------------------------------------41 cecho "Feeling blue..." $blue
42 cecho "Magenta looks more like purple." $magenta
43 cecho "Green with envy." $green
44 cecho "Seeing red?" $red
45 cecho "Cyan, more familiarly known as aqua." $cyan
46 cecho "No color passed (defaults to black)."
47        # 缺失 $color (色彩)參數.
48 cecho "\"Empty\" color passed (defaults to black)." ""
49        # 空的 $color (色彩)參數.
50 cecho
51        # $message(信息) 和 $color (色彩)參數都缺失.
52 cecho "" ""
53        # 空的 $message (信息)和 $color (色彩)參數.
54 # ----------------------------------------------------55
56 echo
57
58 exit 0
59
60 # 練習:
61 # ---------62 # 1) 爲'cecho ()'函數增加粗體的效果.
63 # 2) 增加可選的彩色背景.
################################End Script#########################################
Example 33-14 "賽馬" 遊戲
################################Start Script#######################################
  1 #!/bin/bash
  2 # horserace.sh: 非常簡單的賽馬模擬.
  3 # 作者: Stefano Palmeri
  4 # 已取得使用許可.
  5
  6 ################################################################
  7 #  腳本目的:
  8 #  使用轉義字符和終端顏色.
  9 #
 10 #  練習:
 11 #  編輯腳本使其更具有隨機性,
 12 #+ 設置一個假的賭場 . . .    
 13 #  嗯 . . . 嗯 . . . 這個開始使我想起了一部電影 . . .
 14 #
 15 #  腳本給每匹馬一個隨機的障礙.
 16 #  不均等會以障礙來計算
 17 #+ 並且用一種歐洲風格表達出來.
http://www.818198.com  Page 489
SHELL十三問
 18 #  例如: 機率(odds)=3.75 意味着如果你押1美元贏,
 19 #+ 你可以贏得3.75美元.
 20 #
 21 #  腳本已經在GNU/Linux操作系統上測試過 OS,
 22 #+ 測試終端有xterm 和 rxvt, 及 konsole.
 23 #  測試機器有AMD 900 MHz 的處理器,
 24 #+ 平均比賽時間是75秒.   
 25 #  在更快的計算機上比賽時間應該會更低.
 26 #  所以, 如果你想有更多的懸念,重設USLEEP_ARG 變量的值.
 27 #
 28 #  由Stefano Palmeri編寫.
 29 ################################################################
 30
 31 E_RUNERR=65
 32
 33 # 檢查 md5sum 和 bc 是不是安裝了.
 34 if ! which bc &> /dev/null; then
 35    echo bc is not installed. 
 36    echo "Can\'t run . . . "
 37    exit $E_RUNERR
 38 fi
 39 if ! which md5sum &> /dev/null; then
 40    echo md5sum is not installed. 
 41    echo "Can\'t run . . . "
 42    exit $E_RUNERR
 43 fi
 44
 45 #  更改下面的變量值可以使腳本執行的更慢.
 46 #  它會作爲usleep的參數 (man usleep) 
 47 #+ 並且它的單位是微秒 (500000微秒 = 半秒).
 48 USLEEP_ARG=0 
 49
 50 #  如果腳本接收到ctrl-c中斷,清除臨時目錄, 恢復終端光標和顏色
 51 #
 52 trap 'echo -en "\E[?25h"; echo -en "\E[0m"; stty echo;\
 53 tput cup 20 0; rm -fr  $HORSE_RACE_TMP_DIR'  TERM EXIT
 54 #  參考調試的章節瞭解'trap'的更多解釋
 55
 56 # 給腳本設置一個唯一(實際不是絕對唯一的)的臨時目錄名.
 57 HORSE_RACE_TMP_DIR=$HOME/.horserace-`date +%s`-`head -c10 /dev/urandom | md5sum | head -c30`
 58
 59 # 創建臨時目錄,並切換到該目錄下.
 60 mkdir $HORSE_RACE_TMP_DIR
 61 cd $HORSE_RACE_TMP_DIR
 62
 63
 64 #  這個函數把光標移動到行爲 $1 列爲 $2 然後打印 $3.
 65 #  例如: "move_and_echo 5 10 linux" 等同於
http://www.818198.com  Page 490
SHELL十三問
 66 #+ "tput cup 4 9; echo linux", 但是用一個命令代替了兩個.
 67 #  注: "tput cup" 表示在終端左上角的 0 0 位置,
 68 #+ echo 是在終端的左上角的 1 1 位置.
 69 move_and_echo() {
 70           echo -ne "\E[${1};${2}H""$3"
 71 }
 72
 73 # 產生1-9之間僞隨機數的函數.
 74 random_1_9 () {
 75                 head -c10 /dev/urandom | md5sum | tr -d [a-z] | tr -d 0 | cut -c1
 76 }
 77
 78 #  畫馬時模擬運動的兩個函數.
 79 draw_horse_one() {
 80                echo -n " "//$MOVE_HORSE//
 81 }
 82 draw_horse_two(){
 83               echo -n " "\\\\$MOVE_HORSE\\\\
 84 }  
 85
 86
 87 # 取得當前的終端尺寸.
 88 N_COLS=`tput cols`
 89 N_LINES=`tput lines`
 90
 91 # 至少需要 20-行 X 80-列 的終端尺寸. 檢查一下.
 92 if [ $N_COLS -lt 80 ] || [ $N_LINES -lt 20 ]; then
 93    echo "`basename $0` needs a 80-cols X 20-lines terminal."
 94    echo "Your terminal is ${N_COLS}-cols X ${N_LINES}-lines."
 95    exit $E_RUNERR
 96 fi
 97
 98
 99 # 開始畫賽場.
100
101 # 需要一個80個字符的字符串,看下面的.
102 BLANK80=`seq -s "" 100 | head -c80`
103
104 clear
105
106 # 把前景和背景顏色設置成白色的.
107 echo -ne '\E[37;47m'
108
109 # 把光標移到終端的左上角.
110 tput cup 0 0
111
112 # 畫六條白線.
113 for n in `seq 5`; do
http://www.818198.com  Page 491
SHELL十三問
114       echo $BLANK80        # 線是用80個字符組成的字符串. 
115 done
116
117 # 把前景色設置成黑色.
118 echo -ne '\E[30m'
119
120 move_and_echo 3 1 "START  1"           
121 move_and_echo 3 75 FINISH
122 move_and_echo 1 5 "|"
123 move_and_echo 1 80 "|"
124 move_and_echo 2 5 "|"
125 move_and_echo 2 80 "|"
126 move_and_echo 4 5 "|  2"
127 move_and_echo 4 80 "|"
128 move_and_echo 5 5 "V  3"
129 move_and_echo 5 80 "V"
130
131 # 把前景色設置成紅色.
132 echo -ne '\E[31m'
133
134 # 一些ASCII藝術.
135 move_and_echo 1 8 "..@@@..@@@@@...@@@@@.@...@..@@@@..."
136 move_and_echo 2 8 ".@...@...@.......@...@...@.@......."
137 move_and_echo 3 8 ".@@@@@...@.......@...@@@@@.@@@@...."
138 move_and_echo 4 8 ".@...@...@.......@...@...@.@......."
139 move_and_echo 5 8 ".@...@...@.......@...@...@..@@@@..."
140 move_and_echo 1 43 "@@@@...@@@...@@@@..@@@@..@@@@."
141 move_and_echo 2 43 "@...@.@...@.@.....@.....@....."
142 move_and_echo 3 43 "@@@@..@@@@@.@.....@@@@...@@@.."
143 move_and_echo 4 43 "@..@..@...@.@.....@.........@."
144 move_and_echo 5 43 "@...@.@...@..@@@@..@@@@.@@@@.."
145
146
147 # 把前景和背景顏色設爲綠色.
148 echo -ne '\E[32;42m'
149
150 # 畫11行綠線.
151 tput cup 5 0
152 for n in `seq 11`; do
153       echo $BLANK80
154 done
155
156 # 把前景色設爲黑色.
157 echo -ne '\E[30m'
158 tput cup 5 0
159
160 # 畫柵欄.
161 echo "++++++++++++++++++++++++++++++++++++++\
http://www.818198.com  Page 492
SHELL十三問
162 ++++++++++++++++++++++++++++++++++++++++++"
163
164 tput cup 15 0
165 echo "++++++++++++++++++++++++++++++++++++++\
166 ++++++++++++++++++++++++++++++++++++++++++"
167
168 # 把前景和背景色設回白色.
169 echo -ne '\E[37;47m'
170
171 # 畫3條白線.
172 for n in `seq 3`; do
173       echo $BLANK80
174 done
175
176 # 把前景色設爲黑色.
177 echo -ne '\E[30m'
178
179 # 創建9個文件來保存障礙物.
180 for n in `seq 10 7 68`; do
181       touch $n
182 done 
183
184 # 設置腳本要畫的馬的類型爲第一種類型.
185 HORSE_TYPE=2
186
187 #  爲每匹馬創建位置文件和機率文件.
188 #+ 在這些文件裏保存了該匹馬當前的位置,
189 #+ 類型和機率.
190 for HN in `seq 9`; do
191       touch horse_${HN}_position
192       touch odds_${HN}
193       echo \-1 > horse_${HN}_position
194       echo $HORSE_TYPE >>  horse_${HN}_position
195       # 給馬定義隨機的障礙物.
196        HANDICAP=`random_1_9`
197       # 檢查random_1_9函數是否返回了有效值.
198       while ! echo $HANDICAP | grep [1-9] &> /dev/null; do
199                 HANDICAP=`random_1_9`
200       done
201       # 給馬定義最後的障礙的位置.
202       LHP=`expr $HANDICAP \* 7 + 3`
203       for FILE in `seq 10 7 $LHP`; do
204             echo $HN >> $FILE
205       done  
206     
207       # 計算機率.
208       case $HANDICAP in
209               1) ODDS=`echo $HANDICAP \* 0.25 + 1.25 | bc`
http://www.818198.com  Page 493
SHELL十三問
210                                  echo $ODDS > odds_${HN}
211               ;;
212               2 | 3) ODDS=`echo $HANDICAP \* 0.40 + 1.25 | bc`
213                                        echo $ODDS > odds_${HN}
214               ;;
215               4 | 5 | 6) ODDS=`echo $HANDICAP \* 0.55 + 1.25 | bc`
216                                              echo $ODDS > odds_${HN}
217               ;;
218               7 | 8) ODDS=`echo $HANDICAP \* 0.75 + 1.25 | bc`
219                                        echo $ODDS > odds_${HN}
220               ;;
221               9) ODDS=`echo $HANDICAP \* 0.90 + 1.25 | bc`
222                                   echo $ODDS > odds_${HN}
223       esac
224
225
226 done
227
228
229 # 打印機率.
230 print_odds() {
231 tput cup 6 0
232 echo -ne '\E[30;42m'
233 for HN in `seq 9`; do
234       echo "#$HN odds->" `cat odds_${HN}`
235 done
236 }
237
238 # 在起跑線上畫馬.
239 draw_horses() {
240 tput cup 6 0
241 echo -ne '\E[30;42m'
242 for HN in `seq 9`; do
243       echo /\\$HN/\\"                               "
244 done
245 }
246
247 print_odds
248
249 echo -ne '\E[47m'
250 # 等待回車按鍵開始賽馬.
251 # 轉義序列'\E[?25l'禁顯了光標.
252 tput cup 17 0
253 echo -e '\E[?25l'Press [enter] key to start the race...
254 read -s
255
256 #  禁用了終端的常規顯示功能.
257 #  這避免了賽跑時不小心按了按鍵鍵入顯示字符而弄亂了屏幕.
http://www.818198.com  Page 494
SHELL十三問
258 # 
259 stty -echo
260
261 # --------------------------------------------------------262 # 開始賽跑.
263
264 draw_horses
265 echo -ne '\E[37;47m'
266 move_and_echo 18 1 $BLANK80
267 echo -ne '\E[30m'
268 move_and_echo 18 1 Starting...
269 sleep 1
270
271 # 設置終點線的列數.
272 WINNING_POS=74
273
274 # 記錄賽跑開始的時間.
275 START_TIME=`date +%s`
276
277 # COL 是由下面的"while"結構使用的.
278 COL=0   
279
280 while [ $COL -lt $WINNING_POS ]; do
281                   
282           MOVE_HORSE=0    
283          
284           # 檢查random_1_9函數是否返回了有效值.
285           while ! echo $MOVE_HORSE | grep [1-9] &> /dev/null; do
286                 MOVE_HORSE=`random_1_9`
287           done
288          
289           # 取得隨機取得的馬的類型和當前位置.
290           HORSE_TYPE=`cat  horse_${MOVE_HORSE}_position | tail -1`
291           COL=$(expr `cat  horse_${MOVE_HORSE}_position | head -1`)
292          
293           ADD_POS=1
294           # 檢查當前的位置是否是障礙物的位置.
295           if seq 10 7 68 | grep -w $COL &> /dev/null; then
296                 if grep -w $MOVE_HORSE $COL &> /dev/null; then
297                       ADD_POS=0
298                       grep -v -w  $MOVE_HORSE $COL > ${COL}_new
299                       rm -f $COL
300                       mv -f ${COL}_new $COL
301                       else ADD_POS=1
302                 fi
303           else ADD_POS=1
304           fi
305           COL=`expr $COL + $ADD_POS`
http://www.818198.com  Page 495
SHELL十三問
306           echo $COL >  horse_${MOVE_HORSE}_position  # 保存新位置.
307                            
308          # 選擇要畫的馬的類型.        
309           case $HORSE_TYPE in
310                 1) HORSE_TYPE=2; DRAW_HORSE=draw_horse_two
311                 ;;
312                 2) HORSE_TYPE=1; DRAW_HORSE=draw_horse_one
313           esac      
314           echo $HORSE_TYPE >>  horse_${MOVE_HORSE}_position # 保存當前類型.
315         
316           # 把前景色設爲黑,背景色設爲綠.
317           echo -ne '\E[30;42m'
318          
319           # 把光標位置移到新的馬的位置.
320           tput cup `expr $MOVE_HORSE + 5`  `cat  horse_${MOVE_HORSE}_position | head -1`
321          
322           # 畫馬.
323           $DRAW_HORSE
324            usleep $USLEEP_ARG
325          
326            # 當所有的馬都越過15行的之後,再次打印機率.         
327            touch fieldline15
328            if [ $COL = 15 ]; then
329              echo $MOVE_HORSE >> fieldline15 
330            fi
331            if [ `wc -l fieldline15 | cut -f1 -d " "` = 9 ]; then
332                print_odds
333                : > fieldline15
334            fi          
335          
336           # 取得領頭的馬.
337           HIGHEST_POS=`cat *position | sort -n | tail -1`         
338          
339           # 把背景色重設爲白色.
340           echo -ne '\E[47m'
341           tput cup 17 0
342           echo -n Current leader: `grep -w $HIGHEST_POS *position | cut -c7`"                              "          
343
344 done 
345
346 # 取得賽馬結束的時間.
347 FINISH_TIME=`date +%s`
348
349 # 背景色設爲綠色並且啓用閃動的功能.
350 echo -ne '\E[30;42m'
351 echo -en '\E[5m'
352
353 # 使獲勝的馬閃動.
http://www.818198.com  Page 496
SHELL十三問
354 tput cup `expr $MOVE_HORSE + 5` `cat  horse_${MOVE_HORSE}_position | head -1`
355 $DRAW_HORSE
356
357 # 禁用閃動文本.
358 echo -en '\E[25m'
359
360 # 把前景和背景色設爲白色.
361 echo -ne '\E[37;47m'
362 move_and_echo 18 1 $BLANK80
363
364 # 前景色設爲黑色.
365 echo -ne '\E[30m'
366
367 # 閃動獲勝的馬.
368 tput cup 17 0
369 echo -e "\E[5mWINNER: $MOVE_HORSE\E[25m""  Odds: `cat odds_${MOVE_HORSE}`"\
370 "  Race time: `expr $FINISH_TIME - $START_TIME` secs"
371
372 # 恢復光標和最初的顏色.
373 echo -en "\E[?25h"
374 echo -en "\E[0m"
375
376 # 恢復回顯功能.
377 stty echo
378
379 # 刪除賽跑的臨時文件.
380 rm -rf $HORSE_RACE_TMP_DIR
381
382 tput cup 19 0
383
384 exit 0
################################End Script#########################################
參考 例子 A-22.
注意: 然而,有一個主要的問題,那就是ANSI 轉義序列是不可移植的. 在一些終端運行的
  很好的代碼可能在另外一些終端上可能運行地很糟糕. 在彩色腳本作者終端上運行的
  很好的腳本可能在另外一些終端上就產生不可閱讀的輸出了. 這給彩色腳本的用處大
  大打了個折扣,而很可能使這些技術變成一個暗機關或只是一個玩具而已.
Moshe Jacobson的顏色工具(http://runslinux.net/projects.html#color)能相當容易地使用
ANSI轉義序列. 它用清晰和較有邏輯的語法來代替剛纔討論的難用的結構.
Henry/teikedvl 也同樣開發了一個軟件包來簡化彩色腳本的一些操作
(http://scriptechocolor.sourceforge.net/).
注意事項:
[1]  當然,ANSI是American National Standards Institute(美國國家標準組織)的縮
  寫. 這個令人敬畏的組織建立和維護着許多技術和工業的標準.
33.6. 優化
----------大多數shell腳本處理不復雜的問題時會有很快的解決辦法. 正因爲這樣,優化腳本速度不是
一個問題. 考慮這樣的情況, 一個腳本處理很重要的任務, 雖然它確實運行的很好很正確,但
http://www.818198.com  Page 497
SHELL十三問
是處理速度太慢. 用一種可編譯的語言重寫它可能不是非常好的選擇. 最簡單的辦法是重寫使
這個腳本效率低下的部分. 這個代碼優化的原理是否同樣適用於效率低下的shell腳本?
檢查腳本中的循環. 反覆執行操作的時間消耗增長非常的快. 如果可能, 可以從循環中刪除時
間消耗的操作.
優先使用內建(builtin)命令而不是系統命令. 內建命令執行起來更快並且一般調用時不會
產生新的子shell.
避免不需要的命令, 特別是管道(pipe).
   1 cat "$file" | grep "$word"
   2
   3 grep "$word" "$file"
   4
   5 #  上面的命令行有同樣的效果,
   6 #+ 但第二個運行的更有效率,因爲它不產生新的子進程.
cat 命令似乎特別常在腳本中被濫用.
用time和times工具去了解計算花費的時間. 考慮用C甚至是彙編重寫關鍵的消耗時間的部分.
嘗試最小化文件I/O. Bash在文件處理上不是特別地有效率, 所以要考慮在腳本中使用更合適
地工具來處理, 比如說awk 或 Perl.
採用結構化的思想來寫腳本, 使各個模塊能夠依據需要組織和合並起來.一些適用於高級語言
的優化技術也可以用在腳本上 , 但有些技術, 比如說循環優化, 幾乎是不相關的. 上面的討
論, 依據經驗來判斷.
怎樣優化減少執行時間的優秀腳本示例, 請參考例子 12-42.
33.7. 各種小技巧
----------------* 爲了記錄在一個實際的會話期或多個會話期內運行的用戶腳本,可以加下面的代碼到每
 個你想追蹤記錄的腳本里. 這會記錄下連續的腳本名記錄和調用的次數.
    1 # 添加(>>)下面幾行到你想追蹤記錄的腳本末尾處.
    2
    3 whoami>> $SAVE_FILE    # 記錄調用腳本的用戶.
    4 echo $0>> $SAVE_FILE   # 記錄腳本名.
    5 date>> $SAVE_FILE      # 記錄日期和時間.
    6 echo>> $SAVE_FILE      # 空行作爲分隔行.
    7
    8 #  當然, SAVE_FILE 變量應在~/.bashrc中定義並導出(export)
    9 #+ (變量值類似如 ~/.scripts-run)
* >> 操作符可以在文件尾添加內容. 如果你想在文件頭添加內容,那應該怎麼辦?
    1 file=data.txt
    2 title="***This is the title line of data text file***"
    3
    4 echo $title | cat - $file >$file.new
    5 # "cat -" 連接標準輸出的內容和$file的內容.
    6 #  最後的結果就是生成了一個新文件,
    7 #+ 文件的頭添加了 $title 的值,後跟$file的內容.
 這是早先例子 17-13中的簡化變體. 當然, , sed 也可以辦到.
* 腳本也可以像內嵌到另一個shell腳本的普通命令一樣調用, 如 Tcl 或 wish 腳本, 甚至
 可以是Makefile. 它們可以作爲外部shell命令用C語言的 system() 函數調用, 例如.,
 system("script_name");.
* 把內嵌的 sed 或 awk 腳本的內容賦值給一個變量可以增加包裝腳本(shell wrapper)
 的可讀性. 參考 例子 A-1 和 例子 11-18.
http://www.818198.com  Page 498
SHELL十三問
* 把你最喜歡和最有用的定義和函數放在一些文件中. 當需要的使用的時候, 在腳本中使用
 dot (.) 或 source 命令來"包含(include)"這些"庫文件"的一個或多個.
    1 # 腳本庫
    2 # ------ -------    3
    4 # 注:
    5 # 本文件沒有"#!"開頭.
    6 # 也沒有真正做執行動作的代碼.
    7
    8
    9 # 有用的變量定義
   10
   11 ROOT_UID=0             # Root用戶的 $UID 值是0.
   12 E_NOTROOT=101          # 非root用戶出錯代碼.
   13 MAXRETVAL=255          # 函數最大的的返回值(正值).
   14 SUCCESS=0
   15 FAILURE=-1
   16
   17
   18
   19 # 函數
   20
   21 Usage ()               # "Usage:" 信息(即幫助信息).
   22 {
   23   if [ -z "$1" ]       # 沒有傳遞參數.
   24   then
   25     msg=filename
   26   else
   27     msg=$@
   28   fi
   29
   30   echo "Usage: `basename $0` "$msg""
   31 } 
   32
   33
   34 Check_if_root ()       # 檢查是不是root在運行腳本.
   35 {                      # 取自例子"ex39.sh".
   36   if [ "$UID" -ne "$ROOT_UID" ]
   37   then
   38     echo "Must be root to run this script."
   39     exit $E_NOTROOT
   40   fi
   41 } 
   42
   43
   44 CreateTempfileName ()  # 創建一個"唯一"的臨時文件.
   45 {                      # 取自例子"ex51.sh".
   46   prefix=temp
http://www.818198.com  Page 499
SHELL十三問
   47   suffix=`eval date +%s`
   48   Tempfilename=$prefix.$suffix
   49 }
   50
   51
   52 isalpha2 ()            # 測試字符串是不是都是字母組成的.
   53 {                      # 取自例子"isalpha.sh".
   54   [ $# -eq 1 ] || return $FAILURE
   55
   56   case $1 in
   57   *[!a-zA-Z]*|"") return $FAILURE;;
   58   *) return $SUCCESS;;
   59   esac                 # Thanks, S.C.
   60 }
   61
   62
   63 abs ()                           # 絕對值.
   64 {                                # 注意: 最大的返回值 = 255.
   65   E_ARGERR=-999999
   66
   67   if [ -z "$1" ]                 # 要傳遞參數.
   68   then
   69     return $E_ARGERR             # 返回錯誤.
   70   fi
   71
   72   if [ "$1" -ge 0 ]              # 如果非負的值,
   73   then                           #
   74     absval=$1                    # 絕對值是本身.
   75   else                           # 否則,
   76     let "absval = (( 0 - $1 ))"  # 改變它的符號.
   77   fi 
   78
   79   return $absval
   80 }
   81
   82
   83 tolower ()             #  把傳遞的字符串轉爲小寫
   84 {                      #
   85
   86   if [ -z "$1" ]       #  如果沒有傳遞參數,
   87   then                 #+ 打印錯誤信息
   88     echo "(null)"      #+ (C風格的void指針的錯誤信息)
   89     return             #+ 然後從函數中返回.
   90   fi 
   91
   92   echo "$@" | tr A-Z a-z
   93   # 轉換傳遞過來的所有參數($@).
   94
http://www.818198.com  Page 500
SHELL十三問
   95   return
   96
   97 # 用命令替換功能把函數的輸出賦給變量.
   98 # 例如:
   99 #    oldvar="A seT of miXed-caSe LEtTerS"
  100 #    newvar=`tolower "$oldvar"`
  101 #    echo "$newvar"    # 一串混合大小寫的字符轉換成了全部小寫字符
  102 #
  103 # 練習: 重寫這個函數把傳遞的參數變爲大寫
  104 #       ... toupper()  [容易].
  105 }
* 在腳本中添加特殊種類的註釋開頭標識有助於條理清晰和可讀性.
    1 ## 表示注意.
    2 rm -rf *.zzy   ##  "rm"命令的"-rf"組合選項非常的危險,
    3                ##+ 尤其是對通配符而言.
    4
    5 #+ 表示繼續上一行.
    6 #  這是第一行
    7 #+ 這是多行的註釋,
    8 #+ 這裏是最後一行.
    9
   10 #* 表示標註.
   11
   12 #o 表示列表項.
   13
   14 #> 表示另一個觀點.
   15 while [ "$var1" != "end" ]    #> while test "$var1" != "end"
* if-test 結構的一種聰明用法是用來註釋一塊代碼塊.
    1 #!/bin/bash
    2
    3 COMMENT_BLOCK=
    4 #  給上面的變量設置某個值就會產生討厭的結果
    5 #
    6
    7 if [ $COMMENT_BLOCK ]; then
    8
    9 Comment block --   10 =================================
   11 This is a comment line.
   12 This is another comment line.
   13 This is yet another comment line.
   14 =================================
   15
   16 echo "This will not echo."
   17
   18 Comment blocks are error-free! Whee!
   19
   20 fi
http://www.818198.com  Page 501
SHELL十三問
   21
   22 echo "No more comments, please."
   23
   24 exit 0
 把這種方法和使用here documents來註釋代碼塊作一個比較.
* 測試$? 退出狀態變量, 因爲一個腳本可能想要測試一個參數是否只包含數字,以便後面
 可以把它當作一個整數.
    1 #!/bin/bash
    2
    3 SUCCESS=0
    4 E_BADINPUT=65
    5
    6 test "$1" -ne 0 -o "$1" -eq 0 2>/dev/null
    7 # 整數要麼等於零要麼不等於零.
    8 # 2>/dev/null 可以抑制錯誤信息.
    9
   10 if [ $? -ne "$SUCCESS" ]
   11 then
   12   echo "Usage: `basename $0` integer-input"
   13   exit $E_BADINPUT
   14 fi
   15
   16 let "sum = $1 + 25"             # 如果$1不是整數就會產生錯誤.
   17 echo "Sum = $sum"
   18
   19 # 任何變量,而不僅僅命令行參數可用這種方法來測試.
   20
   21 exit 0
* 0 - 255 範圍的函數返回值是個嚴格的限制. 用全局變量和其他方法常常出問題. 函數內
 返回值給腳本主體的另一個辦法是讓函數寫值到標準輸出(通常是用echo) 作爲"返回值",
 並且將其賦給一個變量. 這實際是命令替換(command substitution)的變體.
Example 33-15 返回值技巧
################################Start Script#######################################
 1 #!/bin/bash
 2 # multiplication.sh
 3
 4 multiply ()                     # 傳遞乘數.
 5 {                               # 能接受多個參數.
 6
 7   local product=1
 8
 9   until [ -z "$1" ]             # 直到所有參數都處理完畢...
10   do
11     let "product *= $1"
12     shift
13   done
14
15   echo $product                 #  不會打印到標準輸出,
http://www.818198.com  Page 502
SHELL十三問
16 }                               #+ 因爲要把它賦給一個變量.
17
18 mult1=15383; mult2=25211
19 val1=`multiply $mult1 $mult2`
20 echo "$mult1 X $mult2 = $val1"
21                                 # 387820813
22
23 mult1=25; mult2=5; mult3=20
24 val2=`multiply $mult1 $mult2 $mult3`
25 echo "$mult1 X $mult2 X $mult3 = $val2"
26                                 # 2500
27
28 mult1=188; mult2=37; mult3=25; mult4=47
29 val3=`multiply $mult1 $mult2 $mult3 $mult4`
30 echo "$mult1 X $mult2 X $mult3 X $mult4 = $val3"
31                                 # 8173300
32
33 exit 0
################################End Script#########################################
 相同的技術也可用在字符串中. 這意味着函數可以"返回"一個非數字的值.
    1 capitalize_ichar ()          #  把傳遞來的參數字符串的第一個字母大寫
    2 {                            #
    3
    4   string0="$@"               # 能接受多個參數.
    5
    6   firstchar=${string0:0:1}   # 第一個字符.
    7   string1=${string0:1}       # 餘下的字符.
    8
    9   FirstChar=`echo "$firstchar" | tr a-z A-Z`
   10                              # 第一個字符轉換成大寫字符.
   11
   12   echo "$FirstChar$string1"  # 打印到標準輸出.
   13
   14 } 
   15
   16 newstring=`capitalize_ichar "every sentence should start with a capital letter."`
   17 echo "$newstring"          # Every sentence should start with a capital letter.
 用這種辦法甚至可能"返回" 多個值.
Example 33-16 整型還是string?
################################Start Script#######################################
 1 #!/bin/bash
 2 # sum-product.sh
 3 # 函數可以"返回"多個值.
 4
 5 sum_and_product ()   # 計算所傳參數的總和與乘積.
 6 {
 7   echo $(( $1 + $2 )) $(( $1 * $2 ))
 8 # 打印每個計算的值到標準輸出,各值用空格分隔開.
http://www.818198.com  Page 503
SHELL十三問
 9 }
10
11 echo
12 echo "Enter first number "
13 read first
14
15 echo
16 echo "Enter second number "
17 read second
18 echo
19
20 retval=`sum_and_product $first $second`      # 把函數的輸出賦值給變量.
21 sum=`echo "$retval" | awk '{print $1}'`      # 把第一個域的值賦給sum變量.
22 product=`echo "$retval" | awk '{print $2}'`  # 把第二個域的值賦給product變量.
23
24 echo "$first + $second = $sum"
25 echo "$first * $second = $product"
26 echo
27
28 exit 0
################################End Script#########################################
* 下一個技巧是傳遞數組給函數的技術, 然後 "返回" 一個數組給腳本.
 用 變量替換(command substitution)把數組的所有元素用空格分隔開來並賦給一個變量
 就可以實現給函數傳遞數組. 用先前介紹的方法函數內echo一個數組並"返回此值",然後
 調用命令替換用 ( ... ) 操作符賦值給一個數組.
Example 33-17 傳遞和返回數組
################################Start Script#######################################
 1 #!/bin/bash
 2 # array-function.sh: 傳遞一個數組給函數並且...
 3 #                   從函數"返回"一個數組
 4
 5
 6 Pass_Array ()
 7 {
 8   local passed_array   # 局部變量.
 9   passed_array=( `echo "$1"` )
10   echo "${passed_array[@]}"
11   #  列出新數組中的所有元素
12   #+ 新數組是在函數內聲明和賦值的.
13 }
14
15
16 original_array=( element1 element2 element3 element4 element5 )
17
18 echo
19 echo "original_array = ${original_array[@]}"
20 #                      列出最初的數組元素.
21
http://www.818198.com  Page 504
SHELL十三問
22
23 # 下面是傳遞數組給函數的技巧.
24 # **********************************
25 argument=`echo ${original_array[@]}`
26 # **********************************
27 #  把原數組的所有元素用空格分隔開合成一個字符串並賦給一個變量
28 #
29 #
30 # 注意:只是把數組本身傳給函數是不會工作的.
31
32
33 # 下面是允許數組作爲"返回值"的技巧.
34 # *****************************************
35 returned_array=( `Pass_Array "$argument"` )
36 # *****************************************
37 # 把函數的輸出賦給數組變量.
38
39 echo "returned_array = ${returned_array[@]}"
40
41 echo "============================================================="
42
43 #  現在,再試一次Now, try it again,
44 #+ 嘗試在函數外存取(列出)數組.
45 Pass_Array "$argument"
46
47 # 函數本身可以列出數組,但...
48 #+ 函數外存取數組被禁止.
49 echo "Passed array (within function) = ${passed_array[@]}"
50 # 因爲變量是函數內的局部變量,所以只有NULL值.
51
52 echo
53
54 exit 0
################################End Script#########################################
 在例子 A-10中有一個更精心製作的給函數傳遞數組的例子.
* 利用雙括號結構,使在for 和 while 循環中可以使用C風格的語法來設置和增加變量. 參
 考例子 10-12 和 例子 10-17.
* 在腳本開頭設置 path 和 umask 增加腳本的"可移植性" -- 在某些把 $PATH 和 umask
 弄亂的系統裏也可以運行.
    1 #!/bin/bash
    2 PATH=/bin:/usr/bin:/usr/local/bin ; export PATH
    3 umask 022   # 腳本的創建的文件有 755 的權限設置.
    4
    5 # 多謝Ian D. Allen提出這個技巧.
* 一個有用的腳本技術是:重複地把一個過濾器的輸出回饋(用管道)給另一個相同過濾器,
 但過濾器有不同的參數和/或選項. 尤其對 tr 和 grep 更合適.
    1 # 取自例子"wstrings.sh".
    2
http://www.818198.com  Page 505
SHELL十三問
    3 wlist=`strings "$1" | tr A-Z a-z | tr '[:space:]' Z | \
    4 tr -cs '[:alpha:]' Z | tr -s '\173-\377' Z | tr Z ' '`
Example 33-18 anagrams遊戲
################################Start Script#######################################
 1 #!/bin/bash
 2 # agram.sh: 用anagrams玩遊戲.
 3
 4 # 尋找 anagrams ...
 5 LETTERSET=etaoinshrdlu
 6 FILTER='.......'       # 最小有多少個字母?
 7 #       1234567
 8
 9 anagram "$LETTERSET" | # 找出這串字符中所有的 anagrams ...
10 grep "$FILTER" |       # 至少7個字符,
11 grep '^is' |           # 以'is'開頭
12 grep -v 's$' |         # 不是複數的(指英文單詞複數)
13 grep -v 'ed$'          # 不是過去式的(當然也是英文單詞)
14 # 可以加許多組合條件和過濾器.
15
16 #  使用 "anagram" 軟件
17 #+ 它是作者 "yawl" 單詞列表軟件包的一部分.
18 #  http://ibiblio.org/pub/Linux/libs/yawl-0.3.2.tar.gz
19 #  http://personal.riverusers.com/~thegrendel/yawl-0.3.2.tar.gz
20
21 exit 0                 # 代碼結束.
22
23
24 bash$ sh agram.sh
25 islander
26 isolate
27 isolead
28 isotheral
29
30
31
32 #  練習:
33 #  ---------34 #  修改這個腳本使 LETTERSET 能作爲命令行參數來接受.
35 #  能夠傳遞參數給第 11 - 13 行的過濾器(就像 $FILTER),
36 #+ 以便能靠傳遞參數來指定一種功能.
37
38 #  參考agram2.sh瞭解些微不同的anagram的一種方法
39 #
################################End Script#########################################
 See also Example 27-3, Example 12-22, and Example A-9.
* 使用"匿名的 here documents" 來註釋代碼塊,這樣避免了對代碼塊的每一塊單獨用#來
 註釋了. 參考例子 17-11.
* 當依賴某個命令腳本在一臺沒有安裝該命令的機器上運行時會出錯. 使用 whatis 命令可
http://www.818198.com  Page 506
SHELL十三問
 以避免此問題.
    1 CMD=command1                 # 第一選擇First choice.
    2 PlanB=command2               # 第二選擇Fallback option.
    3
    4 command_test=$(whatis "$CMD" | grep 'nothing appropriate')
    5 #  如果'command1'沒有在系統裏發現 , 'whatis'會返回:
    6 #+ "command1: nothing appropriate."
    7 #
    8 #  另一種更安全的辦法是:
    9 #     command_test=$(whereis "$CMD" | grep \/)
   10 #  但後面的測試判斷應該翻轉過來,
   11 #+ 因爲$command_test只有當系統存在$CMD命令時纔有內容.
   12 #
   13 #     (Thanks, bojster.)
   14
   15
   16 if [[ -z "$command_test" ]]  # 檢查命令是否存在.
   17 then
   18   $CMD option1 option2       #  調用command1.
   19 else                         #  否則,
   20   $PlanB                     #+ 調用command2.
   21 fi
* 在發生錯誤的情況下 if-grep test 可能不會返回期望的結果,因爲文本是打印在標準出
 錯而不是標準輸出上.
    1 if ls -l nonexistent_filename | grep -q 'No such file or directory'
    2   then echo "File \"nonexistent_filename\" does not exist."
    3 fi
 把標準出錯重定向到標準輸出上可以修改這個.
    1 if ls -l nonexistent_filename 2>&1 | grep -q 'No such file or directory'
    2 #                             ^^^^
    3   then echo "File \"nonexistent_filename\" does not exist."
    4 fi
    5
    6 # 多謝Chris Martin指出.
* The run-parts 命令很容易依次運行一組命令腳本,特別是和 cron 或 at 組合起來.
* 在shell腳本里能調用 X-Windows 的窗口小部件將多麼美好. 已經存在有幾種工具包實現
 這個了, 它們稱爲Xscript, Xmenu, 和 widtools. 頭兩個已經不再維護. 幸運地是仍然
 可以從這兒下載widtools.
 注意: widtools (widget tools) 工具包要求安裝了 XForms 庫. 另外, 它的 Makefile
   在典型的Linux系統上安裝前需要做一些合適的編輯. 最後, 提供的6個部件有3個
   不能工作 (事實上會發生段錯誤).
 dialog 工具集提供了shell腳本使用一種稱爲"對話框"的窗口部件. 原始的 dialog 軟件
 包工作在文本模式的控制檯下, 但它的後續軟件 gdialog, Xdialog, 和 kdialog 使用基
 於X-Windows的窗口小部件集.
Example 33-19 在shell腳本中調用的窗口部件
################################Start Script#######################################
 1 #!/bin/bash
 2 # dialog.sh: 使用 'gdialog' 窗口部件.
http://www.818198.com  Page 507
SHELL十三問
 3 # 必須在你的系統裏安裝'gdialog'才能運行此腳本.
 4 # 版本 1.1 (04/05/05 修正)
 5
 6 # 這個腳本的靈感源自下面的文章.
 7 #     "Scripting for X Productivity," by Marco Fioretti,
 8 #      LINUX JOURNAL, Issue 113, September 2003, pp. 86-9.
 9 # Thank you, all you good people at LJ.
10
11
12 # 在窗口中的輸入錯誤.
13 E_INPUT=65
14 # 輸入窗口顯示的尺寸.
15 HEIGHT=50
16 WIDTH=60
17
18 # 輸出文件名 (由腳本名構建而來).
19 OUTFILE=$0.output
20
21 # 把這個腳本的內容顯示在窗口中.
22 gdialog --title "Displaying: $0" --textbox $0 $HEIGHT $WIDTH
23
24
25
26 # 現在,保存輸入到輸出文件中.
27 echo -n "VARIABLE=" > $OUTFILE
28 gdialog --title "User Input" --inputbox "Enter variable, please:" \
29 $HEIGHT $WIDTH 2>> $OUTFILE
30
31
32 if [ "$?" -eq 0 ]
33 # 檢查退出狀態是一個好習慣.
34 then
35   echo "Executed \"dialog box\" without errors."
36 else
37   echo "Error(s) in \"dialog box\" execution."
38         # 或者, 點擊"Cancel", 而不是"OK" 按鈕.
39   rm $OUTFILE
40   exit $E_INPUT
41 fi
42
43
44
45 # 現在,我們重新取得並顯示保存的變量.
46 . $OUTFILE   # 'Source' 保存的文件(即執行).
47 echo "The variable input in the \"input box\" was: "$VARIABLE""
48
49
50 rm $OUTFILE  # 清除臨時文件.
http://www.818198.com  Page 508
SHELL十三問
51              # 有些應用可能需要保留這些文件.
52
53 exit $?
################################End Script#########################################
 其他的在腳本中使用窗口的工具還有 Tk 或 wish (Tcl 派生物), PerlTk (Perl 的 Tk
 擴展), tksh (ksh 的 Tk 擴展), XForms4Perl (Perl 的 XForms 擴展), Gtk-Perl
 (Perl 的 Gtk 擴展), 或 PyQt (Python 的 Qt 擴展).
* 爲了對複雜的腳本做多次的版本修訂管理, 可以使用 rcs 軟件包.
 使用這個軟件包的好處之一是會自動地升級ID頭標識.在 rcs 的co命令處理一些預定義的
 關鍵字參數替換,例如,代替腳本里頭#$Id$的,如類似下面的行:
    1 #$Id: hello-world.sh,v 1.1 2004/10/16 02:43:05 bozo Exp $
33.8. 安全話題
--------------
33.8.1. 被感染的腳本
--------------------有一個簡短的關於腳本安全的介紹是適當的. 腳本程序可能會包含蠕蟲病毒,特洛伊木馬, 或
是其他的病毒. 由於這些原因, 決不要以root身份運行腳本 (或允許它被插入到系統的
/etc/rc.d裏的啓動腳本中) 除非你確定這是值得信賴的源碼或你已經很小心地分析過了腳本
並確信它不會有什麼危害.
Bell實驗室及其他地方的病毒研究人員, 包括 M. Douglas McIlroy, Tom Duff, 和
Fred Cohen 已經調查過了shell腳本病毒的實現. 他們認爲寫腳本病毒甚至對於新手來說也是
很容易的,就像“腳本小子(script kiddie)”也能容易地寫出. [1]
這也是學習腳本編程的原因之一:學習讀懂腳本和理解腳本可以保護你的系統免受駭客攻擊或
破壞.
33.8.2. 隱藏Shell腳本源碼
-------------------------爲了安全, 使腳本不可讀是有必要的. 如果有軟件可以把腳本轉化成相應的二進制執行文件就
好了. Francisco Rosales的 shc - 通用的Shell腳本編譯器(generic shell script
compiler) 可以出色地完成目標.
不幸地, 依照發表在2005年十月的Linux Journal雜誌上的一篇文章, 二進制文件可以,至少
在某些情況下能被恢復回腳本的原始代碼. 但不管怎麼說,這對大多數技術不高超的駭客來說
仍然是一個保持腳本安全的有效的方法.
注意事項:
[1]  參考 Marius van Oers 的文章(Unix Shell Scripting Malware),和列在參考書目
  (bibliography)的參考資料.
33.9. 移植話題
--------------這本書是關於在GNU/Linux系統下的Bash編程. 但同樣,sh  和 ksh 用戶也能在這兒得到許多
有用的價值.
以現在的情況來看,許多種shell和腳本語言都盡力使自己符合 POSIX 1003.2 標準. 用
--posix 選項調用Bash或在腳本開頭插入 set -o posix 就能使Bash能以很接近這個標準的方
式運行. 在腳本開頭用
   1 #!/bin/sh
比用
   1 #!/bin/bash
會更好.注意在Linux和一些UNIX風格的系統裏/bin/sh是/bin/bash的一個鏈接(link), 並且
如果腳本以/bin/sh調用時會禁用Bash的擴展功能.
大多數的Bash腳本能不作修改就能運行在 ksh下, 反之亦然, 因爲 Chet Ramey 辛勤地把 ksh
http://www.818198.com  Page 509
SHELL十三問
的屬性移植到了最新的Bash版本.
在商業的 UNIX 機器上, 使用了GNU擴展屬性的標準命令的腳本可能不會工作. 這個問題在最
近幾年已經有所改觀了, 因爲GNU軟件包完美地代替了在這些"大塊頭的"UNIX運行的相應工具.
源碼分發 給傳統UNIX上加快了這種趨勢.
Bash 有傳統的 Bourne shell 缺乏的一些屬性. 下面是其中一些:
* 一些擴展的 調用選項(invocation options)
* 使用 $( ) 結構來完成命令替換(Command substitution)
* 一些 字符串處理(string manipulation)  操作符
* 進程替換(Process substitution)
* Bash的 內建(builtins) 命令
參考 Bash F.A.Q. 查看完整的列表.
33.10. 在Windows下進行Shell編程
-------------------------------使用其他操作系統用戶希望能運行UNIX類型的腳本能在他們的系統上運行, 因此也希望能在這
本書裏能學到這方面的知識. 來自Cygnus的 Cygwin 軟件結合來自Mortice Kern的MKS軟件包
(MKS utilities)可以給Windows添加shell腳本的兼容.
已經有正式宣佈Windows的將來版本會包含Bash風格的命令行和腳本能力,但目前爲止還沒有
結果.
第34章 Bash, 版本 2 和 3
=========================
34.1. Bash, 版本2
-----------------當前運行在你的機器裏的Bash版本號是版本 2.xx.y 或 3.xx.y.
 bash$ echo $BASH_VERSION
 2.05.b.0(1)-release
經典的Bash版本2編程語言升級版增加了數組變量, [1] 字符串和參數擴展, 和間接變量引用
的更好的方法,及其他的屬性.
Example 34-1 字符串擴展
################################Start Script#######################################
 1 #!/bin/bash
 2
 3 # 字符串擴展.
 4 # Bash版本2引入的特性.
 5
 6 #  具有$'xxx'格式的字符串
 7 #+ 將會解釋裏面的標準的轉義字符.
 8
 9 echo $'Ringing bell 3 times \a \a \a'
10      # 可能在一些終端只能響鈴一次.
11 echo $'Three form feeds \f \f \f'
12 echo $'10 newlines \n\n\n\n\n\n\n\n\n\n'
13 echo $'\102\141\163\150'   # Bash
14                            # 八進制相等的字符.
15
16 exit 0
################################End Script#########################################
Example 34-2 間接變量引用 - 新方法
################################Start Script#######################################
http://www.818198.com  Page 510
SHELL十三問
 1 #!/bin/bash
 2
 3 # 間接變量引用.
 4 # 這有點像C++的引用屬性.
 5
 6
 7 a=letter_of_alphabet
 8 letter_of_alphabet=z
 9
10 echo "a = $a"           # 直接引用.
11
12 echo "Now a = ${!a}"    # 間接引用.
13 # ${!variable} 形式比老的"eval var1=\$$var2"更高級
14
15 echo
16
17 t=table_cell_3
18 table_cell_3=24
19 echo "t = ${!t}"                      # t = 24
20 table_cell_3=387
21 echo "Value of t changed to ${!t}"    # 387
22
23 #  這在用來引用數組或表格的成員時非常有用,
24 #+ 或用來模擬多維數組.
25 #  如果有可索引的選項 (類似於指針運算)
26 #+ 會更好. 唉.
27
28 exit 0
################################End Script#########################################
Example 34-3 使用間接變量引用的簡單數據庫應用
################################Start Script#######################################
 1 #!/bin/bash
 2 # resistor-inventory.sh
 3 # 使用間接變量引用的簡單數據庫應用.
 4
 5 # ============================================================== #
 6 # 數據
 7
 8 B1723_value=470                                   # 值
 9 B1723_powerdissip=.25                             # 是什麼
10 B1723_colorcode="yellow-violet-brown"             # 色彩帶寬
11 B1723_loc=173                                     # 它們存在哪兒
12 B1723_inventory=78                                # 有多少
13
14 B1724_value=1000
15 B1724_powerdissip=.25
16 B1724_colorcode="brown-black-red"
17 B1724_loc=24N
http://www.818198.com  Page 511
SHELL十三問
18 B1724_inventory=243
19
20 B1725_value=10000
21 B1725_powerdissip=.25
22 B1725_colorcode="brown-black-orange"
23 B1725_loc=24N
24 B1725_inventory=89
25
26 # ============================================================== #
27
28
29 echo
30
31 PS3='Enter catalog number: '
32
33 echo
34
35 select catalog_number in "B1723" "B1724" "B1725"
36 do
37   Inv=${catalog_number}_inventory
38   Val=${catalog_number}_value
39   Pdissip=${catalog_number}_powerdissip
40   Loc=${catalog_number}_loc
41   Ccode=${catalog_number}_colorcode
42
43   echo
44   echo "Catalog number $catalog_number:"
45   echo "There are ${!Inv} of [${!Val} ohm / ${!Pdissip} watt] resistors in stock."
46   echo "These are located in bin # ${!Loc}."
47   echo "Their color code is \"${!Ccode}\"."
48
49   break
50 done
51
52 echo; echo
53
54 # 練習:
55 # ---------56 # 1) 重寫腳本,使其從外部文件裏讀數據.
57 # 2) 重寫腳本,用數組代替間接變量引用
58 #
59 #    用數組會更簡單明瞭
60
61
62 # 注:
63 # -----64 #  Shell腳本除了最簡單的數據應用,其實並不合適數據庫應用,
65 #+ 它過多地依賴實際工作的環境和命令.
http://www.818198.com  Page 512
SHELL十三問
66 #  寫數據庫應用更好的還是用一門自然支持數據結構的語言,
67 #+ 如 C++ 或 Java (或甚至是 Perl).
68
69 exit 0
################################End Script#########################################
Example 34-4 用數組和其他的小技巧來處理四人隨機打牌
################################Start Script#######################################
  1 #!/bin/bash
  2
  3 # Cards:
  4 # 處理四人打牌.
  5
  6 UNPICKED=0
  7 PICKED=1
  8
  9 DUPE_CARD=99
 10
 11 LOWER_LIMIT=0
 12 UPPER_LIMIT=51
 13 CARDS_IN_SUIT=13
 14 CARDS=52
 15
 16 declare -a Deck
 17 declare -a Suits
 18 declare -a Cards
 19 #  用一個三維數據來描述數據會更容易實現也更明瞭一些.
 20 #
 21 #  可能Bash將來的版本會支持多維數組.
 22
 23
 24 initialize_Deck ()
 25 {
 26 i=$LOWER_LIMIT
 27 until [ "$i" -gt $UPPER_LIMIT ]
 28 do
 29   Deck[i]=$UNPICKED   # 把整副牌的每張牌都設爲沒人持牌.
 30   let "i += 1"
 31 done
 32 echo
 33 }
 34
 35 initialize_Suits ()
 36 {
 37 Suits[0]=C #梅花
 38 Suits[1]=D #方塊
 39 Suits[2]=H #紅心
 40 Suits[3]=S #黑桃
 41 }
http://www.818198.com  Page 513
SHELL十三問
 42
 43 initialize_Cards ()
 44 {
 45 Cards=(2 3 4 5 6 7 8 9 10 J Q K A)
 46 # 另一種初始化數組的方法.
 47 }
 48
 49 pick_a_card ()
 50 {
 51 card_number=$RANDOM
 52 let "card_number %= $CARDS"
 53 if [ "${Deck[card_number]}" -eq $UNPICKED ]
 54 then
 55   Deck[card_number]=$PICKED
 56   return $card_number
 57 else 
 58   return $DUPE_CARD
 59 fi
 60 }
 61
 62 parse_card ()
 63 {
 64 number=$1
 65 let "suit_number = number / CARDS_IN_SUIT"
 66 suit=${Suits[suit_number]}
 67 echo -n "$suit-"
 68 let "card_no = number % CARDS_IN_SUIT"
 69 Card=${Cards[card_no]}
 70 printf %-4s $Card
 71 # 優雅地打印各張牌.
 72 }
 73
 74 seed_random ()  # 隨機產生牌上數值的種子.
 75 {               # 如果你沒有這麼做會有什麼發生?
 76 seed=`eval date +%s`
 77 let "seed %= 32766"
 78 RANDOM=$seed
 79 #  其他的產生隨機用的種子的方法還有什麼W?
 80 #
 81 }
 82
 83 deal_cards ()
 84 {
 85 echo
 86
 87 cards_picked=0
 88 while [ "$cards_picked" -le $UPPER_LIMIT ]
 89 do
http://www.818198.com  Page 514
SHELL十三問
 90   pick_a_card
 91   t=$?
 92
 93   if [ "$t" -ne $DUPE_CARD ]
 94   then
 95     parse_card $t
 96
 97     u=$cards_picked+1
 98     # 改回1步進的索引(臨時的). 爲什麼?
 99     let "u %= $CARDS_IN_SUIT"
100     if [ "$u" -eq 0 ]   # 內嵌的 if/then 條件測試.
101     then
102      echo
103      echo
104     fi
105     # Separate hands.
106
107     let "cards_picked += 1"
108   fi 
109 done 
110
111 echo
112
113 return 0
114 }
115
116
117 # 結構化編程:
118 # 整個程序邏輯模塊化.
119
120 #================
121 seed_random
122 initialize_Deck
123 initialize_Suits
124 initialize_Cards
125 deal_cards
126 #================
127
128 exit 0
129
130
131
132 # 練習 1:
133 # 把這個腳本完整地做註釋.
134
135 # 練習 2:
136 # 增加一個處理例程 (函數) 來以花色排序打印出每個人手中的牌.
137 # 如果你高興,可增加你喜歡的各種酷的代碼.
http://www.818198.com  Page 515
SHELL十三問
138
139 # 練習 3:
140 # 簡化和理順腳本的邏輯.
################################End Script#########################################
注意事項:
[1]  Chet Ramey 承諾會在Bash的未來版本中實現關聯數組(associative arrays)
  (一個Perl特性). 到了版本3,這個特性還沒有實現.
34.2. Bash版本3
---------------在2004年7月27日, Chet Ramey 發佈了Bash的第三版本. 它修復了許多bug並加入了一些新的
屬性.
增加的一些屬性有:
* 新的,更特別的不可移植的 {a..z} 花括號擴展(brace expansion) 操作符.
    1 #!/bin/bash
    2
    3 for i in {1..10}
    4 #  比下面的更簡單並且更易於理解
    5 #+ for i in $(seq 10)
    6 do
    7   echo -n "$i "
    8 done
    9
   10 echo
   11
   12 # 1 2 3 4 5 6 7 8 9 10
* ${!array[@]} 操作符, 它擴展給定的數組(array)的所有元素下標.
    1 #!/bin/bash
    2
    3 Array=(element-zero element-one element-two element-three)
    4
    5 echo ${Array[0]}   # 元素0
    6                    # 數組的第一個元素.
    7
    8 echo ${!Array[@]}  # 0 1 2 3
    9                    # 數組所有的下標.
   10
   11 for i in ${!Array[@]}
   12 do
   13   echo ${Array[i]} # element-zero
   14                    # element-one
   15                    # element-two
   16                    # element-three
   17                    #
   18                    # 在數組裏的所有元素.
   19 done
* =~ 正則表達式(Regular Expression) 匹配操作符在雙方括號(double brackets)
 測試表達式中使用. (Perl也有一個相似的操作符.)
    1 #!/bin/bash
http://www.818198.com  Page 516
SHELL十三問
    2
    3 variable="This is a fine mess."
    4
    5 echo "$variable"
    6
    7 if [[ "$variable" =~ "T*fin*es*" ]]
    8 # 在雙方括號([[]])裏用=~操作符進行正則匹配.
    9 then
   10   echo "match found"
   11       # match found
   12 fi
 或, 更有用的用法:
    1 #!/bin/bash
    2
    3 input=$1
    4
    5
    6 if [[ "$input" =~ "[1-9][0-9][0-9]-[0-9][0-9]-[0-9][0-9][0-9][0-9]" ]]
    7 # NNN-NN-NNNN
    8 # 每個N是一個數字.
    9 # 但, 開頭的第一個數字不能是 0.
   10 then
   11   echo "Social Security number."
   12   # 處理 SSN.
   13 else
   14   echo "Not a Social Security number!"
   15   # 或者, 要求正確的輸入.
   16 fi
 還有一個使用 =~ 操作符的例子, 參考例子 A-28.
注意: 升級到Bash版本3使原來在早先版本可以工作的少部分腳本不能工作了. 要重新測試
  原來的腳本看是否它們仍然可以工作!
  確實發生不能工作的情況, 在 Advanced Bash Scripting Guide 裏的部分腳本代碼
  不得不修復 (例如,例子 A-20 和 例子 9-4).
第35章 後記
============
35.1. 作者後記
--------------
          doce ut discas
          (Teach, that you yourself may learn.)
我怎麼會寫這本腳本編程的書?這有一個很奇怪的故事.時間回到前幾年,那時我準備學習
shell腳本編程--這最好的辦法莫過於讀一本這方面的好書了.我一直在找一本能覆蓋這個
主題方方面面的指南參考書.我也在找一本能把難點說得清楚容易並能用實際的代碼和代碼注
釋解釋這些難以理解的細節. [1]  事實上,我在找一本非常滿意的書, 或者是類似的東西.
不幸的是,這是不存在的.如果我想要一本,那我不得不自己寫一本.於是,它就寫出來了.
這使我想起一個關於瘋子教授的故事.下面的事像笨蛋那樣荒謬.當這本書上架之前,對於所
有的在圖書館的書使他有了想寫一本書的主意.他就這樣做了,開始了這項好處多多的工作.
他爭分奪秒地開始完成和現在這本書標題很像的書,他每天都很快地奔跑回家來做這件事.當
幾年之後他死掉了,他有了寫幾千本書保存下來,可能會被和其他的破書一塊放在書架上.可
http://www.818198.com  Page 517
SHELL十三問
能他寫的書不是那麼的好,但這有什麼關係呢?這是一個活在幻想裏的一個朋友.即使他沒有
被幻想給迷惑驅使...但我忍不住地欽佩這個老笨蛋.
注意事項:
[1]  這是聲名狼藉使人鬱悶到死的技術.
35.2. 關於作者
--------------這傢伙到底是誰?
作者沒有外交特權,不是被強迫寫作的. [1] 這本書有點違揹他的其他的主要工作,
HOW-2 Meet Women: The Shy Man's Guide to Relationships. 他另外也寫了
Software-Building HOWTO. 近來, 他嘗試寫一些虛構的短篇小說.
自1995年成爲一個Linux用戶以來(Slackware 2.2, kernel 1.2.1), 作者已經發表了一些軟件
包,包括 cruft 一次性加密軟件包(one-time pad encryption utility), 軟件 mcalc 可
用做計算器, 軟件judge是Scrabble拼字遊戲的自動求解包, 和軟件包yawl 一起組成猜詞表.
他從一臺CDC 3800上使用FORTRAN IV開始他的編程之旅, 但那種日子一點也不值得懷念.
作者和他的妻子還有他們的狗隱居在一個偏遠的地方,他幻想着人性是善良的.
注意事項:
[1]   這些誰可以做,誰不可以做...拿到一個MCSE證書.
35.3. 哪裏可以取得幫助?
-----------------------作者不是太忙(並且心情不錯)的話,會回答一般性的腳本編程問題.但是,如果你的特定應
用的腳本不工作,建議你最好把問題發到 comp.os.unix.shell 新聞組去.
35.4. 製作這本書的工具
----------------------
35.4.1 硬件
-----------一個運行着Red Hat 7.1/7.3的IBM Thinkpad, model 760XL(P166, 104 meg RAM)的筆記本.
是的,它非常的緩慢並且還有一個更人膽戰心驚的鍵盤,但它總比一根鉛筆加一個巨大的寫字
板好多了.
升級: 升級到了運行着FC3的 770Z Thinkpad (P2-366, 192 meg RAM)筆記本.誰想捐贈一個
更新的一點筆記本給這個快餓死的作者 <g>?
35.4.2 軟件和排版軟件
---------------------   1.  Bram Moolenaar的功能強大的SGML軟件 vim文本編輯器.
   2.  OpenJade, 一個把SGML文檔轉換爲其他格式的DSSSL翻譯引擎.
   3.  Norman Walsh的DSSSL排版框架.
   4.  由Norman Walsh和Leonard Muellner (O'Reilly, ISBN 1-56592-580-7)寫的最權威的
  指南:DocBook. 它仍然是任何一個想寫Docbook SGML格式的書的標準參考書.
 
todo
====
12.22 第2部分翻譯結束.階段性總結.
12.02 本打算自己看,做做筆記就算了,如果要大家看,就不行了.補全前3章所有未譯的地方.
01.06 所有的沒翻譯或翻譯不對的地方標記<rojy bug>.
01.08 一些朋友提出想要,可惜沒譯完,最近效率比較低,手裏還有些其它的活:(.
  醜媳婦總要見公婆,還是放了吧.
01.13 11章結束
01.14 12章開始, 翻譯html版本.
01.19 開始12.4節.
http://www.818198.com  Page 518
SHELL十三問
02.16 開始12.6節.
03.23 12章終於結束了...
04.15 前3部分終於結束了, 歷時4個多月:(, 後邊的部分就交給
05.15 終於完成了, 沒有黃毅兄的幫助, 真是不敢想象, 恐怕就要流產了
  接下來就要搞sgml版本的了, 唉... 到底走彎路了...
[]    
   
awk基礎入門(1)
文章整理: 文章來源: 網絡
Awk是一種非常好的語言,同時有一個非常奇怪的名稱。在本系列文章中,DanielRobbins 將使您迅速掌握 awk編程技巧。
隨着本系列的進展,將討論更高級的主題,最後將演示一個真正的高級awk 演示程序。
捍衛 awk
   在本系列文章中,我將使您成爲精通 awk 的編碼人員。我承認,awk 並沒有一個非常好聽且又非常“時髦”的名字。
awk 的 GNU 版本(叫作 gawk)聽起來非常怪異。那些不熟悉這種語言的人可能聽說過 "awk",並可能認爲它是一組落伍
且過時的混亂代碼。它甚至會使最博學的 UNIX 權威陷於錯亂的邊緣(使他不斷地發出 "kill -9!" 命令,就象使用咖啡機一
樣)。
    的確,awk 沒有一個動聽的名字。但它是一種很棒的語言。awk 適合於文本處理和報表生成,它還有許多精心設計的特
性,允許進行需要特殊技巧程序設計。與某些語言不同,awk 的語法較爲常見。它借鑑了某些語言的一些精華部分,如 C
語言、python 和 bash(雖然在技術上,awk 比 python 和 bash 早創建)。awk 是那種一旦學會了就會成爲您戰略編碼庫
的主要部分的語言。
第一個 awk
讓我們繼續,開始使用 awk,以瞭解其工作原理。在命令行中輸入以下命令:
$ awk '{ print }' /etc/passwd
[]    
   
awk基礎入門(2)
文章整理: 文章來源: 網絡
如您所見,awk 打印出 /etc/passwd 文件的第一和第三個字段,它們正好分別是用戶名和用戶標識字段。現在,當腳本運行
時,它並不理想 -- 在兩個輸出字段之間沒有空格!如果習慣於使用 bash 或 python 進行編程,那麼您會指望 print $1 $3 命
令在兩個字段之間插入空格。然而,當兩個字符串在 awk 程序中彼此相鄰時,awk 會連接它們但不在它們之間添加空格。
以下命令會在這兩個字段中插入空格:
$ awk -F":" '{ print $1 " " $3 }' /etc/passwd
[]    
   
awk基礎入門(3)
文章整理: 文章來源: 網絡
條件語句
awk 還提供了非常好的類似於 C 語言的 if 語句。如果您願意,可以使用 if 語句重寫前一個腳本:
{     if ( $5 ~ /root/ ) {         print $3     }}
[]
--------------------製作工具:小說下載閱讀器 http://www.mybook66.com<PIXTEL_MMI_EBOOK_2005>2�������������������
</PIXTEL_MMI_EBOOK_2005>
http://www.818198.com  Page 519

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 


 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章