隨著程序變得更加龐大和復(fù)雜,設(shè)計(jì),編碼和維護(hù)它們也變得更加困難。對(duì)于任意一個(gè)大項(xiàng)目而言, 把繁重,復(fù)雜的任務(wù)分割為細(xì)小且簡(jiǎn)單的任務(wù),往往是一個(gè)好主意。想象一下,我們?cè)噲D描述 一個(gè)平凡無(wú)奇的工作,一位火星人要去市場(chǎng)買食物。我們可能通過(guò)下面一系列步驟來(lái)形容整個(gè)過(guò)程:
* 上車
* 開(kāi)車到市場(chǎng)
* 停車
* 買食物
* 回到車中
* 開(kāi)車回家
* 回到家中
然而,火星人可能需要更詳細(xì)的信息。我們可以進(jìn)一步細(xì)化子任務(wù)“停車”為這些步驟:
* 找到停車位
* 開(kāi)車到停車位
* 關(guān)閉引擎
* 拉緊手剎
* 下車
* 鎖車
這個(gè)“關(guān)閉引擎”子任務(wù)可以進(jìn)一步細(xì)化為這些步驟,包括“關(guān)閉點(diǎn)火裝置”,“移開(kāi)點(diǎn)火匙”等等,直到 已經(jīng)完整定義了要去市場(chǎng)買食物整個(gè)過(guò)程的每一個(gè)步驟。
這種先確定上層步驟,然后再逐步細(xì)化這些步驟的過(guò)程被稱為自頂向下設(shè)計(jì)。這種技巧允許我們 把龐大而復(fù)雜的任務(wù)分割為許多小而簡(jiǎn)單的任務(wù)。自頂向下設(shè)計(jì)是一種常見(jiàn)的程序設(shè)計(jì)方法, 尤其適合 shell 編程。
在這一章中,我們將使用自頂向下的設(shè)計(jì)方法來(lái)進(jìn)一步開(kāi)發(fā)我們的報(bào)告產(chǎn)生器腳本。
## Shell 函數(shù)
目前我們的腳本執(zhí)行以下步驟來(lái)產(chǎn)生這個(gè) HTML 文檔:
* 打開(kāi)網(wǎng)頁(yè)
* 打開(kāi)網(wǎng)頁(yè)標(biāo)頭
* 設(shè)置網(wǎng)頁(yè)標(biāo)題
* 關(guān)閉網(wǎng)頁(yè)標(biāo)頭
* 打開(kāi)網(wǎng)頁(yè)主體部分
* 輸出網(wǎng)頁(yè)標(biāo)頭
* 輸出時(shí)間戳
* 關(guān)閉網(wǎng)頁(yè)主體
* 關(guān)閉網(wǎng)頁(yè)
為了下一階段的開(kāi)發(fā),我們將在步驟7和8之間添加一些額外的任務(wù)。這些將包括:
* 系統(tǒng)正常運(yùn)行時(shí)間和負(fù)載。這是自上次關(guān)機(jī)或重啟之后系統(tǒng)的運(yùn)行時(shí)間,以及在幾個(gè)時(shí)間間隔內(nèi)當(dāng)前運(yùn)行在處理 中的平均任務(wù)量。
* 磁盤空間。系統(tǒng)中存儲(chǔ)設(shè)備的總使用量。
* 家目錄空間。每個(gè)用戶所使用的存儲(chǔ)空間數(shù)量。
如果對(duì)于每一個(gè)任務(wù),我們都有相應(yīng)的命令,那么通過(guò)命令替換,我們就能很容易地把它們添加到我們的腳本中:
~~~
#!/bin/bash
# Program to output a system information page
TITLE="System Information Report For $HOSTNAME"
CURRENT_TIME=$(date +"%x %r %Z")
TIME_STAMP="Generated $CURRENT_TIME, by $USER"
cat << _EOF_
<HTML>
<HEAD>
<TITLE>$TITLE</TITLE>
</HEAD>
<BODY>
<H1>$TITLE</H1>
<P>$TIME_STAMP</P>
$(report_uptime)
$(report_disk_space)
$(report_home_space)
</BODY>
</HTML>
_EOF_
~~~
我們能夠用兩種方法來(lái)創(chuàng)建這些額外的命令。我們可以分別編寫三個(gè)腳本,并把它們放置到 環(huán)境變量 PATH 所列出的目錄下,或者我們也可以把這些腳本作為 shell 函數(shù)嵌入到我們的程序中。 我們之前已經(jīng)提到過(guò),shell 函數(shù)是位于其它腳本中的“微腳本”,作為自主程序。Shell 函數(shù)有兩種語(yǔ)法形式:
~~~
function name {
commands
return
}
and
name () {
commands
return
}
~~~
這里的 name 是函數(shù)名,commands 是一系列包含在函數(shù)中的命令。
兩種形式是等價(jià)的,可以交替使用。下面我們將查看一個(gè)說(shuō)明 shell 函數(shù)使用方法的腳本:
~~~
1 #!/bin/bash
2
3 # Shell function demo
4
5 function funct {
6 echo "Step 2"
7 return
8 }
9
10 # Main program starts here
11
12 echo "Step 1"
13 funct
14 echo "Step 3"
~~~
隨著 shell 讀取這個(gè)腳本,它會(huì)跳過(guò)第1行到第11行的代碼,因?yàn)檫@些文本行由注釋和函數(shù)定義組成。 從第12行代碼開(kāi)始執(zhí)行,有一個(gè) echo 命令。第13行會(huì)調(diào)用 shell 函數(shù) funct,然后 shell 會(huì)執(zhí)行這個(gè)函數(shù), 就如執(zhí)行其它命令一樣。這樣程序控制權(quán)會(huì)轉(zhuǎn)移到第六行,執(zhí)行第二個(gè) echo 命令。然后再執(zhí)行第7行。 這個(gè) return 命令終止這個(gè)函數(shù),并把控制權(quán)交給函數(shù)調(diào)用之后的代碼(第14行),從而執(zhí)行最后一個(gè) echo 命令。注意為了使函數(shù)調(diào)用被識(shí)別出是 shell 函數(shù),而不是被解釋為外部程序的名字,所以在腳本中 shell 函數(shù)定義必須出現(xiàn)在函數(shù)調(diào)用之前。
我們將給腳本添加最小的 shell 函數(shù)定義:
~~~
#!/bin/bash
# Program to output a system information page
TITLE="System Information Report For $HOSTNAME"
CURRENT_TIME=$(date +"%x %r %Z")
TIME_STAMP="Generated $CURRENT_TIME, by $USER"
report_uptime () {
return
}
report_disk_space () {
return
}
report_home_space () {
return
}
cat << _EOF_
<HTML>
<HEAD>
<TITLE>$TITLE</TITLE>
</HEAD>
<BODY>
<H1>$TITLE</H1>
<P>$TIME_STAMP</P>
$(report_uptime)
$(report_disk_space)
$(report_home_space)
</BODY>
</HTML>
_EOF_
~~~
Shell 函數(shù)的命名規(guī)則和變量一樣。一個(gè)函數(shù)必須至少包含一條命令。這條 return 命令(是可選的)滿足要求。
## 局部變量
目前我們所寫的腳本中,所有的變量(包括常量)都是全局變量。全局變量在整個(gè)程序中保持存在。 對(duì)于許多事情來(lái)說(shuō),這很好,但是有時(shí)候它會(huì)使 shell 函數(shù)的使用變得復(fù)雜。在 shell 函數(shù)中,經(jīng)常期望 會(huì)有局部變量。局部變量只能在定義它們的 shell 函數(shù)中使用,并且一旦 shell 函數(shù)執(zhí)行完畢,它們就不存在了。
擁有局部變量允許程序員使用的局部變量名,可以與已存在的變量名相同,這些變量可以是全局變量, 或者是其它 shell 函數(shù)中的局部變量,卻不必?fù)?dān)心潛在的名字沖突。
這里有一個(gè)實(shí)例腳本,其說(shuō)明了怎樣來(lái)定義和使用局部變量:
~~~
#!/bin/bash
# local-vars: script to demonstrate local variables
foo=0 # global variable foo
funct_1 () {
local foo # variable foo local to funct_1
foo=1
echo "funct_1: foo = $foo"
}
funct_2 () {
local foo # variable foo local to funct_2
foo=2
echo "funct_2: foo = $foo"
}
echo "global: foo = $foo"
funct_1
echo "global: foo = $foo"
funct_2
echo "global: foo = $foo"
~~~
正如我們所看到的,通過(guò)在變量名之前加上單詞 local,來(lái)定義局部變量。這就創(chuàng)建了一個(gè)只對(duì)其所在的 shell 函數(shù)起作用的變量。在這個(gè) shell 函數(shù)之外,這個(gè)變量不再存在。當(dāng)我們運(yùn)行這個(gè)腳本的時(shí)候, 我們會(huì)看到這樣的結(jié)果:
~~~
[me@linuxbox ~]$ local-vars
global: foo = 0
funct_1: foo = 1
global: foo = 0
funct_2: foo = 2
global: foo = 0
~~~
我們看到對(duì)兩個(gè) shell 函數(shù)中的局部變量 foo 賦值,不會(huì)影響到在函數(shù)之外定義的變量 foo 的值。
這個(gè)功能就允許 shell 函數(shù)能保持各自以及與它們所在腳本之間的獨(dú)立性。這個(gè)非常有價(jià)值,因?yàn)樗鼛兔?阻止了程序各部分之間的相互干涉。這樣 shell 函數(shù)也可以移植。也就是說(shuō),按照需求, shell 函數(shù)可以在腳本之間進(jìn)行剪切和粘貼。
## 保持腳本運(yùn)行
當(dāng)開(kāi)發(fā)程序的時(shí)候,保持程序的可執(zhí)行狀態(tài)非常有用。這樣做,并且經(jīng)常測(cè)試,我們就可以在程序 開(kāi)發(fā)過(guò)程的早期檢測(cè)到錯(cuò)誤。這將使調(diào)試問(wèn)題容易多了。例如,如果我們運(yùn)行這個(gè)程序,做一個(gè)小的修改, 然后再次執(zhí)行這個(gè)程序,最后發(fā)現(xiàn)一個(gè)問(wèn)題,非常有可能這個(gè)最新的修改就是問(wèn)題的來(lái)源。通過(guò)添加空函數(shù), 程序員稱之為占位符,我們可以在早期階段證明程序的邏輯流程。當(dāng)構(gòu)建一個(gè)占位符的時(shí)候, 能夠包含一些為程序員提供反饋信息的代碼是一個(gè)不錯(cuò)的主意,這些信息展示了正在執(zhí)行的邏輯流程。 現(xiàn)在看一下我們腳本的輸出結(jié)果:
~~~
[me@linuxbox ~]$ sys_info_page
<HTML>
<HEAD>
<TITLE>System Information Report For twin2</TITLE>
</HEAD>
<BODY>
<H1>System Information Report For linuxbox</H1>
<P>Generated 03/19/2009 04:02:10 PM EDT, by me</P>
</BODY>
</HTML>
~~~
我們看到時(shí)間戳之后的輸出結(jié)果中有一些空行,但是我們不能確定這些空行產(chǎn)生的原因。如果我們 修改這些函數(shù),讓它們包含一些反饋信息:
~~~
report_uptime () {
echo "Function report_uptime executed."
return
}
report_disk_space () {
echo "Function report_disk_space executed."
return
}
report_home_space () {
echo "Function report_home_space executed."
return
}
~~~
然后再次運(yùn)行這個(gè)腳本:
~~~
[me@linuxbox ~]$ sys_info_page
<HTML>
<HEAD>
<TITLE>System Information Report For linuxbox</TITLE>
</HEAD>
<BODY>
<H1>System Information Report For linuxbox</H1>
<P>Generated 03/20/2009 05:17:26 AM EDT, by me</P>
Function report_uptime executed.
Function report_disk_space executed.
Function report_home_space executed.
</BODY>
</HTML>
~~~
現(xiàn)在我們看到,事實(shí)上,執(zhí)行了三個(gè)函數(shù)。
我們的函數(shù)框架已經(jīng)各就各位并且能工作,是時(shí)候更新一些函數(shù)代碼了。首先,是 report_uptime 函數(shù):
~~~
report_uptime () {
cat <<- _EOF_
<H2>System Uptime</H2>
<PRE>$(uptime)</PRE>
_EOF_
return
}
~~~
這些代碼相當(dāng)直截了當(dāng)。我們使用一個(gè) here 文檔來(lái)輸出標(biāo)題和 uptime 命令的輸出結(jié)果,命令結(jié)果被 標(biāo)簽包圍, 為的是保持命令的輸出格式。這個(gè) report_disk_space 函數(shù)類似:
~~~
report_disk_space () {
cat <<- _EOF_
<H2>Disk Space Utilization</H2>
<PRE>$(df -h)</PRE>
_EOF_
return
}
~~~
這個(gè)函數(shù)使用 df -h 命令來(lái)確定磁盤空間的數(shù)量。最后,我們將建造 report_home_space 函數(shù):
~~~
report_home_space () {
cat <<- _EOF_
<H2>Home Space Utilization</H2>
<PRE>$(du -sh /home/*)</PRE>
_EOF_
return
}
~~~
我們使用帶有 -sh 選項(xiàng)的 du 命令來(lái)完成這個(gè)任務(wù)。然而,這并不是此問(wèn)題的完整解決方案。雖然它會(huì) 在一些系統(tǒng)(例如 Ubuntu)中起作用,但是在其它系統(tǒng)中它不工作。這是因?yàn)樵S多系統(tǒng)會(huì)設(shè)置家目錄的 權(quán)限,以此阻止其它用戶讀取它們,這是一個(gè)合理的安全措施。在這些系統(tǒng)中,這個(gè) report_home_space 函數(shù), 只有用超級(jí)用戶權(quán)限執(zhí)行我們的腳本時(shí),才會(huì)工作。一個(gè)更好的解決方案是讓腳本能根據(jù)用戶的使用權(quán)限來(lái) 調(diào)整自己的行為。我們將在下一章中討論這個(gè)問(wèn)題。
> 你的 .bashrc 文件中的 shell 函數(shù)
>
> Shell 函數(shù)是更為完美的別名替代物,實(shí)際上是創(chuàng)建較小的個(gè)人所用命令的首選方法。別名 非常局限于命令的種類和它們支持的 shell 功能,然而 shell 函數(shù)允許任何可以編寫腳本的東西。 例如,如果我們喜歡 為我們的腳本開(kāi)發(fā)的這個(gè) report_disk_space shell 函數(shù),我們可以為我們的 .bashrc 文件 創(chuàng)建一個(gè)相似的名為 ds 的函數(shù):
>
> ~~~
> ds () {
> echo “Disk Space Utilization For $HOSTNAME”
> df -h
> }
>
> ~~~
## 總結(jié)歸納
這一章中,我們介紹了一種常見(jiàn)的程序設(shè)計(jì)方法,叫做自頂向下設(shè)計(jì),并且我們知道了怎樣 使用 shell 函數(shù)按照要求來(lái)完成逐步細(xì)化的任務(wù)。我們也知道了怎樣使用局部變量使 shell 函數(shù) 獨(dú)立于其它函數(shù),以及其所在程序的其它部分。這就有可能使 shell 函數(shù)以可移植的方式編寫, 并且能夠重復(fù)使用,通過(guò)把它們放置到多個(gè)程序中;節(jié)省了大量的時(shí)間。
## 拓展閱讀
* Wikipedia 上面有許多關(guān)于軟件設(shè)計(jì)原理的文章。這里是一些好文章:
[http://en.wikipedia.org/wiki/Top-down_design](http://en.wikipedia.org/wiki/Top-down_design)
[http://en.wikipedia.org/wiki/Subroutines](http://en.wikipedia.org/wiki/Subroutines)
- 第一章:引言
- 第二章:什么是shell
- 第三章:文件系統(tǒng)中跳轉(zhuǎn)
- 第四章:研究操作系統(tǒng)
- 第五章:操作文件和目錄
- 第六章:使用命令
- 第七章:重定向
- 第八章:從shell眼中看世界
- 第九章:鍵盤高級(jí)操作技巧
- 第十章:權(quán)限
- 第十一章:進(jìn)程
- 第十二章:shell環(huán)境
- 第十三章:VI簡(jiǎn)介
- 第十四章:自定制shell提示符
- 第十五章:軟件包管理
- 第十六章:存儲(chǔ)媒介
- 第十七章:網(wǎng)絡(luò)系統(tǒng)
- 第十八章:查找文件
- 第十九章:歸檔和備份
- 第二十章:正則表達(dá)式
- 第二十一章:文本處理
- 第二十二章:格式化輸出
- 第二十三章:打印
- 第二十四章:編譯程序
- 第二十五章:編寫第一個(gè)shell腳本
- 第二十六章:?jiǎn)?dòng)一個(gè)項(xiàng)目
- 第二十七章:自頂向下設(shè)計(jì)
- 第二十八章:流程控制 if分支結(jié)構(gòu)
- 第二十九章:讀取鍵盤輸入
- 第三十章:流程控制 while/until 循環(huán)
- 第三十一章:疑難排解
- 第三十二章:流程控制 case分支
- 第三十三章:位置參數(shù)
- 第三十四章:流程控制 for循環(huán)
- 第三十五章:字符串和數(shù)字
- 第三十六章:數(shù)組
- 第三十七章:奇珍異寶
