Bash 中的遞歸函數(shù)【轉(zhuǎn)載】

作為 Linux/Unix 系統(tǒng)上內(nèi)核與用戶之間的接口脑豹,shell 由于使用方便、可交互能力強(qiáng)、具有強(qiáng)大的編程能力等特性而受到廣泛的應(yīng)用茬高。bash(Bourne Again shell)是對(duì) Bourne shell 的擴(kuò)展,并且綜合了很多 csh 和 Korn Shell 中的優(yōu)點(diǎn)假抄,使得 bash 具有非常靈活且強(qiáng)大的編程接口怎栽,同時(shí)又有很友好的用戶界面。bash 所提供的諸如命令補(bǔ)齊宿饱、通配符熏瞄、命令歷史記錄、別名之類的新特性谬以,使其迅速成為很多用戶的首選强饮。


注意: 【本文】轉(zhuǎn)載至IBM developerworks社區(qū),如有興趣查看類似的其他文章为黎,可以訪問:IBM developerworks查閱邮丰。


然而,作為一種解釋性語(yǔ)言铭乾,bash 在編程能力方面提供的支持并不像其他編譯性的語(yǔ)言(例如 C 語(yǔ)言)那樣完善剪廉,執(zhí)行效率也會(huì)低很多,這些缺點(diǎn)在編寫函數(shù)(尤其是遞歸函數(shù))時(shí)都展現(xiàn)的一覽無余片橡。本文將從經(jīng)典的 fork 炸彈入手妈经,逐一介紹在 bash 中編寫遞歸函數(shù)時(shí)需要注意問題,并探討各種問題的解決方案捧书。

盡管本文是以 bash 為例介紹相關(guān)概念吹泡,但是類似的思想基本上也適用于其他 shell。

遞歸經(jīng)典:fork 炸彈

函數(shù)在程序設(shè)計(jì)中是一個(gè)非常重要的概念经瓷,它可以將程序劃分成一個(gè)個(gè)功能相對(duì)獨(dú)立的代碼塊爆哑,使代碼的模塊化更好,結(jié)構(gòu)更加清晰舆吮,并可以有效地減少程序的代碼量揭朝。遞歸函數(shù)更是充分提現(xiàn)了這些優(yōu)點(diǎn)队贱,通過在函數(shù)定義中調(diào)用自身,可以將復(fù)雜的計(jì)算問題變成一個(gè)簡(jiǎn)單的迭代算法潭袱,當(dāng)回溯到邊界條件時(shí)柱嫌,再逐層返回上一層函數(shù)。有很多數(shù)學(xué)問題都非常適合于采用遞歸的思想來設(shè)計(jì)程序求解屯换,例如階乘编丘、漢諾(hanoi)塔等。

可能很多人都曾經(jīng)聽說過 fork 炸彈彤悔,它實(shí)際上只是一個(gè)非常簡(jiǎn)單的遞歸程序嘉抓,程序所做的事情只有一樣:不斷 fork 一個(gè)新進(jìn)程。由于程序是遞歸的晕窑,如果沒有任何限制抑片,這會(huì)導(dǎo)致這個(gè)簡(jiǎn)單的程序迅速耗盡系統(tǒng)里面的所有資源。

在 bash 中設(shè)計(jì)這樣一個(gè) fork 炸彈非常簡(jiǎn)單杨赤,Jaromil 在 2002 年設(shè)計(jì)了最為精簡(jiǎn)的一個(gè) fork炸彈的實(shí)現(xiàn)敞斋,整個(gè)程序從函數(shù)定義到調(diào)用僅僅包含 13 個(gè)字符,如清單 1 所示望拖。

清單1. bash 中的 fork 炸彈

.(){ .|.& };.

這串字符乍看上去根本就看不出個(gè)所以然來渺尘,下面讓我們逐一解釋一下它究竟在干些什么。為了解釋方便说敏,我們對(duì)清單1中的內(nèi)容重新設(shè)置一下格式,并在前面加上了行號(hào)丢郊,如清單 2 所示盔沫。

清單2. bash 中的 fork 炸彈的解釋
1 .()
2 {
3  .|.&
4 }
5 ;
6 .
  • 第 1 行說明下面要定義一個(gè)函數(shù),函數(shù)名為小數(shù)點(diǎn)枫匾,沒有可選參數(shù)架诞。
  • 第 2 行表示函數(shù)體開始。
  • 第 3 行是函數(shù)體真正要做的事情干茉,首先它遞歸調(diào)用本函數(shù)谴忧,然后利用管道調(diào)用一個(gè)新進(jìn)程(它要做的事情也是遞歸調(diào)用本函數(shù)),并將其放到后臺(tái)執(zhí)行角虫。
  • 第 4 行表示函數(shù)體結(jié)束沾谓。
  • 第 5 行并不會(huì)執(zhí)行什么操作,在命令行中用來分隔兩個(gè)命令用戳鹅。從總體來看均驶,它表明這段程序包含兩個(gè)部分,首先定義了一個(gè)函數(shù)枫虏,然后調(diào)用這個(gè)函數(shù)妇穴。
  • 第 6 行表示調(diào)用本函數(shù)爬虱。

對(duì)于函數(shù)名,大家可能會(huì)有所疑惑腾它,小數(shù)點(diǎn)也能做函數(shù)名使用嗎跑筝?畢竟小數(shù)點(diǎn)是 shell 的一個(gè)內(nèi)嵌命令,用來在當(dāng)前 shell 環(huán)境中讀取指定文件瞒滴,并運(yùn)行其中的命令继蜡。實(shí)際上的確可以,這取決于 bash 對(duì)命令的解釋順序逛腿。默認(rèn)情況下稀并,bash 處于非 POSIX 模式,此時(shí)對(duì)命令的解釋順序如下:

  • 關(guān)鍵字单默,例如 if碘举、for 等。
  • 別名搁廓。別名不能與關(guān)鍵字相同引颈,但是可以為關(guān)鍵字定義別名,例如 end=fi境蜕。
  • 特殊內(nèi)嵌命令蝙场,例如 break、continue 等粱年。POSIX 定義的特殊內(nèi)嵌命令包括:.(小數(shù)點(diǎn))售滤、:(冒號(hào))、break台诗、continue完箩、eval、exec拉队、exit弊知、export、readonly粱快、return秩彤、set、shift事哭、times漫雷、trap 和 unset。bash 又增加了一個(gè)特殊的內(nèi)嵌命令 source慷蠕。
  • 函數(shù)珊拼。如果處于非 POSIX 模式,bash 會(huì)優(yōu)先匹配函數(shù)流炕,然后再匹配內(nèi)嵌命令澎现。
  • 非特殊內(nèi)嵌命令仅胞,例如 cd、test 等剑辫。
  • 腳本和可執(zhí)行程序干旧。在 PATH 環(huán)境變量指定的目錄中進(jìn)行搜索,返回第一個(gè)匹配項(xiàng)妹蔽。

由于默認(rèn)情況下椎眯,bash 處于非 POSIX 模式,因此 fork 炸彈中的小數(shù)點(diǎn)會(huì)優(yōu)先當(dāng)成一個(gè)函數(shù)進(jìn)行匹配胳岂。(實(shí)際上编整,Jaromil 最初的設(shè)計(jì)并沒有使用小數(shù)點(diǎn),而是使用的冒號(hào)乳丰,也能起到完全相同的效果端姚。)要使用 POSIX 模式來運(yùn)行 bash 腳本灼舍,可以使用以下三種方法:

  • 使用 --posix 選項(xiàng)啟動(dòng) bash。
  • 在運(yùn)行 bash 之后缘圈,執(zhí)行 set -o posix 命令阅嘶。
  • 使用 /bin/sh 莫鸭。

最后一種方法比較有趣吏祸,盡管 sh 在大部分系統(tǒng)上是一個(gè)指向 bash 的符號(hào)鏈接揭糕,但是它所啟用的卻是 POSIX 模式,所有的行為都完全遵守 POSIX 規(guī)范屎即。在清單 3 給出的例子中庙睡,我們可以發(fā)現(xiàn),小數(shù)點(diǎn)在默認(rèn) bash 中被解釋成一個(gè)函數(shù)剑勾,能夠正常執(zhí)行埃撵;但是在 sh 中,小數(shù)點(diǎn)卻被當(dāng)作一個(gè)內(nèi)嵌命令虽另,因此調(diào)用函數(shù)時(shí)會(huì)被認(rèn)為存在語(yǔ)法錯(cuò)誤,無法正常執(zhí)行饺谬。

清單3. bash 與 sh 對(duì)命令匹配順序的區(qū)別
[root@localhost ~]# ls -l /bin/bash /bin/sh
-rwxr-xr-x 1 root root 735144 2007-08-31 22:20 /bin/bash
lrwxrwxrwx 1 root root      4 2007-12-18 13:26 /bin/sh -> bash
[root@localhost ~]# echo $SHELL
/bin/bash
[root@localhost ~]# .() { echo hello; } ; .
hello
[root@localhost ~]# sh
sh-3.2# echo $SHELL
/bin/bash
sh-3.2# .() { echo hello; } ; .
sh: .': not a valid identifier
sh: .: filename argument required
.: usage: . filename [arguments]
sh-3.2#

一旦運(yùn)行清單 1 給出的 fork 炸彈捂刺,會(huì)以2的指數(shù)次冪的速度不斷產(chǎn)生新進(jìn)程,這會(huì)導(dǎo)致系統(tǒng)資源會(huì)被迅速耗光募寨,最終除非重新啟動(dòng)機(jī)器族展,否則基本上就毫無辦法了。為了防止這會(huì)造成太大的損害拔鹰,我們可以使用 ulimit 限制每個(gè)用戶能夠創(chuàng)建的進(jìn)程數(shù)仪缸,如清單 4 所示。

清單4. 限制用戶可以創(chuàng)建的進(jìn)程數(shù)
[root@localhost ~]# ulimit -u 128
[root@localhost ~]# ulimit -a
core file size          (blocks, -c) 0
data seg size           (kbytes, -d) unlimited
max nice                        (-e) 20
file size               (blocks, -f) unlimited
pending signals                 (-i) unlimited
max locked memory       (kbytes, -l) unlimited
max memory size         (kbytes, -m) unlimited
open files                      (-n) 1024
pipe size            (512 bytes, -p) 8
POSIX message queues     (bytes, -q) unlimited
max rt priority                 (-r) unlimited
stack size              (kbytes, -s) 8192
cpu time               (seconds, -t) unlimited
max user processes              (-u) 128
virtual memory          (kbytes, -v) unlimited
file locks                      (-x) unlimited

[root@localhost ~]# .() { .|.& } ; .
[1] 6152

[root@localhost ~]# bash: fork: Resource temporarily unavailable
bash: fork: Resource temporarily unavailable
bash: fork: Resource temporarily unavailable
...

在清單 4 中列肢,我們將用戶可以創(chuàng)建的最大進(jìn)程數(shù)限制為 128恰画,執(zhí)行 fork 炸彈會(huì)迅速 fork 出大量進(jìn)程宾茂,此后會(huì)由于資源不足而無法繼續(xù)執(zhí)行。

fork 炸彈讓我們認(rèn)識(shí)到了遞歸函數(shù)的強(qiáng)大功能拴还,同時(shí)也意識(shí)到一旦使用不當(dāng)跨晴,遞歸函數(shù)所造成的破壞將是巨大的。實(shí)際上片林,fork 炸彈只是一個(gè)非常簡(jiǎn)單的遞歸函數(shù)端盆,它并不涉及參數(shù)傳遞、返回值等問題费封,而這些問題在使用 bash 編程時(shí)是否有完善的支持呢焕妙?下面讓我們通過幾個(gè)例子來逐一介紹在 bash 中編寫遞歸函數(shù)時(shí)應(yīng)該注意的相關(guān)問題。

返回值問題

有一些經(jīng)典的數(shù)學(xué)問題弓摘,使用遞歸函數(shù)來解決都非常方便焚鹊。階乘就是這樣一個(gè)典型的問題,清單 5 給出了一個(gè)實(shí)現(xiàn)階乘計(jì)算的 bash 腳本(當(dāng)然衣盾,除了使用遞歸函數(shù)之外寺旺,簡(jiǎn)單地利用一個(gè)循環(huán)也可以實(shí)現(xiàn)計(jì)算階乘的目的,不過本文以此為例來介紹遞歸函數(shù)的相關(guān)問題)势决。

清單5. 階乘函數(shù)的 bash 實(shí)現(xiàn)
[root@localhost shell]# cat -n factorial1.sh
1  #!/bin/bash
2
3  factorial()
4  {
5    i=$1
6
7    if [ $i -eq 0 ]
8    then
9      return 1;
10    else
11      factorial expr $i - 1
12      return expr $i \* $? 
13    fi
14  }
15
16  if [ -z $1 ]
17  then
18    echo "Need one parameter."
19    exit 1
20  fi
21
22  factorial $1
23
24  echo $?
[root@localhost shell]# ./factorial1.sh 5
0

這個(gè)腳本看上去并沒有什么問題:遞歸函數(shù)的參數(shù)傳遞和普通函數(shù)沒什么不同阻塑,返回值是通過獲取 $? 的值實(shí)現(xiàn)的,這是利用了執(zhí)行命令的退出碼果复。然而陈莽,最終的結(jié)果卻顯然是錯(cuò)誤的。調(diào)試一下就會(huì)發(fā)現(xiàn)虽抄,當(dāng)遞歸回溯到盡頭時(shí)走搁,變量 i 的值被修改為 0;而退出上次函數(shù)調(diào)用之后迈窟,變量 i 的新值也被帶了回來私植,詳細(xì)信息如清單 6 所示(請(qǐng)注意黑體部分)。

清單6. 調(diào)試 factorial1.sh 的問題
[root@localhost shell]# export PS4='+[$FUNCNAME: $LINENO] '
[root@localhost shell]# sh -x factorial1.sh 5
+[: 16] '[' -z 5 ']'
+[: 22] factorial 5
+[factorial: 5] i=5
+[factorial: 7] '[' 5 -eq 0 ']'
++[factorial: 11] expr 5 - 1
+[factorial: 11] factorial 4
+[factorial: 5] i=4
+[factorial: 7] '[' 4 -eq 0 ']'
++[factorial: 11] expr 4 - 1
+[factorial: 11] factorial 3
+[factorial: 5] i=3
+[factorial: 7] '[' 3 -eq 0 ']'
++[factorial: 11] expr 3 - 1
+[factorial: 11] factorial 2
+[factorial: 5] i=2
+[factorial: 7] '[' 2 -eq 0 ']'
++[factorial: 11] expr 2 - 1
+[factorial: 11] factorial 1
+[factorial: 5] i=1
+[factorial: 7] '[' 1 -eq 0 ']'
++[factorial: 11] expr 1 - 1
+[factorial: 11] factorial 0
+[factorial: 5] i=0
+[factorial: 7] '[' 0 -eq 0 ']'
+[factorial: 9] return 1
++[factorial: 12] expr 0 '*' 1
+[factorial: 12] return 0
++[factorial: 12] expr 0 '*' 0
+[factorial: 12] return 0
++[factorial: 12] expr 0 '*' 0
+[factorial: 12] return 0
++[factorial: 12] expr 0 '*' 0
+[factorial: 12] return 0
++[factorial: 12] expr 0 '*' 0
+[factorial: 12] return 0
+[: 24] echo 0
0

這段腳本問題的根源在于變量的作用域:在 shell 腳本中车酣,不管是否在函數(shù)中定義曲稼,變量默認(rèn)就是全局的,一旦定義之后湖员,對(duì)于此后執(zhí)行的命令全部可見贫悄。bash 也支持局部變量,不過需要使用 local 關(guān)鍵字進(jìn)行顯式地聲明娘摔。local 是bash 中的一個(gè)內(nèi)嵌命令窄坦,其作用是將變量的作用域設(shè)定為只有對(duì)本函數(shù)及其子進(jìn)程可見。局部變量只能在變量聲明的代碼塊中可見,這也就意味著在函數(shù)內(nèi)聲明的局部變量只能在函數(shù)代碼塊中才能被訪問鸭津,它們并不會(huì)污染同名全局變量彤侍。因此為了解決上面這個(gè)程序的問題,我們應(yīng)該使用 local 關(guān)鍵字將 i 聲明為局部變量曙博。修改后的腳本如清單 7 所示拥刻。

清單7. 遞歸函數(shù)中使用 local 關(guān)鍵字聲明局部變量
[root@localhost shell]# cat -n factorial2.sh
1  #!/bin/bash
2
3  factorial()
4  {
5    local i=$1
6
7    if [ $i -eq 0 ]
8    then
9      return 1;
10    else
11      factorial expr $i - 1
12      return expr $i \* $? 
13    fi
14  }
15
16  if [ -z $1 ]
17  then
18    echo "Need one parameter."
19    exit 1
20  fi
21
22  factorial $1
23
24  echo $?
[root@localhost shell]# ./factorial2.sh 5
120
[root@localhost shell]# ./factorial2.sh 6208

這下 5 的階乘計(jì)算對(duì)了,但是稍微大一點(diǎn)的數(shù)字都會(huì)出錯(cuò)父泳,比如 6 的階乘計(jì)算出來是錯(cuò)誤的 208般哼。這個(gè)問題的原因在于腳本中傳遞函數(shù)返回值的方式存在缺陷,$? 所能傳遞的最大值是 255惠窄,超過該值就沒有辦法利用這種方式來傳遞返回值了蒸眠。解決這個(gè)問題的方法有兩種,一種是利用全局變量杆融,另外一種則是利用其他方式進(jìn)行周轉(zhuǎn)(例如標(biāo)準(zhǔn)輸入輸出設(shè)備)楞卡。清單 8 和清單 9 分別給出了這兩種方法的參考實(shí)現(xiàn)。

清單8. 使用全局變量傳遞返回值
[root@localhost shell]# cat -n factorial3.sh
1  #!/bin/bash
2
3  factorial()
4  {
5    local i=$1
6
7    if [ $i -eq 0 ]
8    then
9      rtn=1
10    else
11      factorial expr $i - 1
12      rtn=expr $i \* $rtn 
13    fi
14
15    return $rtn
16  }
17
18  if [ -z $1 ]
19  then
20    echo "Need one parameter."
21    exit 1
22  fi
23
24  factorial $1
25
26  echo $rtn
[root@localhost shell]# ./factorial3.sh 6
720
清單9. 利用標(biāo)準(zhǔn)輸入輸出設(shè)備傳遞返回值
[root@localhost shell]# cat -n factorial4.sh
1  #!/bin/bash
2
3  factorial()
4  {
5    local i=$1
6
7    if [ $i -eq 0 ]
8    then
9      echo 1
10    else
11      local j=expr $i - 1
12      local k=factorial $j
13      echo expr $i \* $k 
14    fi
15  }
16
17  if [ -z $1 ]
18  then
19    echo "Need one parameter."
20    exit 1
21  fi
22
23  rtn=factorial $1
24  echo $rtn
[root@localhost shell]# ./factorial4.sh 6
720

盡管利用全局變量或標(biāo)準(zhǔn)輸入輸出設(shè)備都可以解決如何正確傳遞返回值的問題脾歇,但是它們卻各有缺點(diǎn):如果利用全局變量蒋腮,由于全局變量對(duì)此后的程序全部可見,一旦被其他程序修改藕各,就會(huì)出錯(cuò)池摧,所以編寫代碼時(shí)需要格外小心,特別是在編寫復(fù)雜的遞歸程序的時(shí)候激况;如果利用標(biāo)準(zhǔn)輸入輸出設(shè)備作彤,那么遞歸函數(shù)中就存在諸多限制,例如任何地方都不能再向標(biāo)準(zhǔn)輸出設(shè)備中打印內(nèi)容乌逐,否則就可能被上一層調(diào)用當(dāng)作正常輸出結(jié)果讀走了竭讳,另外速度方面也可能存在嚴(yán)重問題。

參數(shù)傳遞問題

在設(shè)計(jì)函數(shù)時(shí)浙踢,除了返回值之外绢慢,我們可能還希望所調(diào)用的函數(shù)還能夠返回其他一些信息。例如洛波,在上面的階乘遞歸函數(shù)中呐芥,我們除了希望計(jì)算最后的結(jié)果之外,還希望了解這個(gè)函數(shù)一共被調(diào)用了多少次奋岁。熟悉 c 語(yǔ)言之類的讀者都會(huì)清楚,這可以通過傳遞一個(gè)指針類型的參數(shù)實(shí)現(xiàn)荸百。然而闻伶,在 bash 中并不支持指針,它提供了另外一種在解釋性語(yǔ)言中常見的設(shè)計(jì):間接變量引用(indirect variable reference)够话。讓我們看一下下面這個(gè)例子:

var2=$var3
var1=$var2

其中變量 var2 的存在實(shí)際上就是為了讓 var1 能夠訪問 var3蓝翰,實(shí)際上也可以通過 var1 直接引用 var3 的值光绕,方法是 var1=\$$var3(請(qǐng)注意轉(zhuǎn)義字符是必須的,否則 $$ 符號(hào)會(huì)被解釋為當(dāng)前進(jìn)程的進(jìn)程 ID 號(hào))畜份,這種方式就稱為間接變量引用诞帐。從 bash2 開始,對(duì)間接變量引入了一種更為清晰的語(yǔ)法爆雹,方法是 var1=${!var3}停蕉。

清單 10 中給出了使用間接變量引用來統(tǒng)計(jì)階乘函數(shù)被調(diào)用次數(shù)的實(shí)現(xiàn)。

清單10. 利用間接變量引用統(tǒng)計(jì)遞歸函數(shù)的調(diào)用次數(shù)
[root@localhost shell]# cat -n depth.sh
1  #!/bin/bash
2
3  factorial()
4  {
5    local i=$1
6    local l=$2
7
8    if [ $i -eq 0 ]
9    then
10      eval ${l}=1
11      rtn=1
12    else
13      factorial expr $i - 1 ${l}
14      rtn=expr $i \* $rtn 
15     
16      local k=${!l}
17      eval ${l}=expr ${k} + 1
18    fi
19
20    return $rtn
21  }
22
23  if [ -z $1 ]
24  then
25    echo "Need one parameter."
26    exit 1
27  fi
28
29  level=0
30  factorial $1 level
31
32  echo "The factorial of $1 is : $rtn"
33  echo " the function of factorial is invoked $level times."
[root@localhost shell]# ./depth.sh 6
The factorial of 6 is : 720
the function of factorial is invoked 7 times.

在上面我們?cè)?jīng)介紹過钙态,為了解決變量作用域和函數(shù)返回值的問題慧起,在遞歸函數(shù)中我們使用 local 聲明局部變量,并采用全局變量來傳遞返回值册倒。但是隨著調(diào)用關(guān)系變得更加復(fù)雜蚓挤,全局變量的值有可能在其他地方被錯(cuò)誤地修改。實(shí)際上驻子,使用局部變量也存在一個(gè)問題灿意,下面讓我們來看一下清單 11 中給出的例子。

清單11. 查找字符串在文件中是否存在崇呵,并計(jì)算所在行數(shù)和出現(xiàn)次數(shù)
[root@localhost shell]# cat -n getline1.sh
1  #!/bin/bash
2
3  GetLine()
4  {
5    string=$1
6    file=$2
7
8    line=grep -n $string $file
9    if [ $? -eq 0 ]
10    then
11      printf "$string is found as the %drd line in $file \n" echo $line \
| cut -f1 -d:
12      num=grep $string $file | wc -l
13      rtn=0
14    else
15      printf "$string is not found in $file \n"
16      num=0
17      rtn=1
18    fi
19
20    return $rtn;
21  }
22
23  if [ ! -f testfile.$$ ]
24  then
25    cat >> testfile.$$ <<EOF
26  first line .
27  second line ..
28  third line ...
29  EOF
30  fi
31
32  num=0
33  rtn=0
34  for i in "second" "six" "line"
35  do
36    echo
37    GetLine $i testfile.$$
38    echo "return value: $rtn"
39
40    if [ $num -gt 0 ]
41    then
42      echo "$num occurences found totally."
43    fi
44  done
[root@localhost shell]# ./getline1.sh
second is found as the 2rd line in testfile.4280
return value: 0
1 occurences found totally.
six is not found in testfile.4280
return value: 1
line is found as the 1rd line in testfile.4280
return value: 0
3 occurences found totally.
[root@localhost shell]#

這段程序的目的是查找某個(gè)字符串在指定文件中是否存在缤剧,如果存在,就計(jì)算第一次出現(xiàn)的行數(shù)和總共出現(xiàn)的次數(shù)演熟。為了說明局部變量和后面提到的子函數(shù)的問題鞭执,我們故意將對(duì)出現(xiàn)次數(shù)的打印也放到了 GetLine 函數(shù)之外進(jìn)行處理。清單 11 中全部使用全局變量芒粹,并沒有出現(xiàn)什么問題兄纺。下面讓我們來看一下將 GetLine 中使用的局部變量改用 local 聲明后會(huì)出現(xiàn)什么問題,修改后的代碼和執(zhí)行結(jié)果如清單 12 所示化漆。

清單12. 使用 local 聲明局部變量需要注意的問題
[root@localhost shell]# cat -n getline2.sh
1  #!/bin/bash
2
3  GetLine()
4  {
5    local string=$1
6    local file=$2
7
8    local line=grep -n $string $file
9    if [ $? -eq 0 ]
10    then
11      printf "$string is found as the %drd line in $file \n" echo $line \
| cut -f1 -d:
12      num=grep $string $file | wc -l
13      rtn=0
14    else
15      printf "$string is not found in $file \n"
16      num=0
17      rtn=1
18    fi
19
20    return $rtn;
21  }
22
23  if [ ! -f testfile.$$ ]
24  then
25    cat >> testfile.$$ <<EOF
26  first line .
27  second line ..
28  third line ...
29  EOF
30  fi
31
32  num=0
33  rtn=0
34  for i in "second" "six" "line"
35  do
36    echo
37    GetLine $i testfile.$$
38    echo "return value: $rtn"
39
40    if [ $num -gt 0 ]
41    then
42      echo "$num occurences found totally."
43    fi
44  done
[root@localhost shell]# ./getline2.sh
second is found as the 2rd line in testfile.4300
return value: 0
1 occurences found totally.
six is found as the 0rd line in testfile.4300 return value: 0
line is found as the 1rd line in testfile.4300
return value: 0
3 occurences found totally.

清單 12 的運(yùn)行結(jié)果顯示估脆,在文件中搜索 six 關(guān)鍵字時(shí)的結(jié)果是錯(cuò)誤的,調(diào)試會(huì)發(fā)現(xiàn)座云,問題的原因在于:第 8 行使用 local 將 line 聲明為局部變量疙赠,并將 grep 命令的執(zhí)行結(jié)果賦值給 line 變量。然而不論 grep 是否成功在文件中找到匹配項(xiàng)(grep 程序找到匹配項(xiàng)返回值為 0朦拖,否則返回值為 1)圃阳,第 9 行中 ? 的值總是 0。實(shí)際上璧帝,第 8 行相當(dāng)于執(zhí)行了兩條語(yǔ)句:第一條語(yǔ)句使用 grep 在文件中查找匹配項(xiàng)捍岳,第二條語(yǔ)句將 grep 命令的結(jié)果賦值給變量 line,并設(shè)定其作用域只對(duì)于本函數(shù)及其子進(jìn)程可見。因此第 9 行命令中? 的值實(shí)際上是執(zhí)行 local 命令的返回值锣夹,不管 grep 命令的結(jié)果如何页徐,它總是 0。

要解決這個(gè)問題银萍,可以將第 8 行的命令拆分開变勇,首先使用單獨(dú)一行將變量 line 聲明為 local的,然后再執(zhí)行這條 grep 命令贴唇,并將結(jié)果賦值給變量 line(此時(shí)前面不能加上 local)搀绣。

解決變量作用域的另外一種方法是使用子 shell。所謂子 shell 是在當(dāng)前 shell 環(huán)境中啟動(dòng)一個(gè)子 shell 來執(zhí)行所調(diào)用的命令或函數(shù)滤蝠,這個(gè)函數(shù)中所聲明的所有變量都是局部變量豌熄,它們不會(huì)污染原有 shell 的名字空間。清單 13 給出了使用子 shell 修改后的例子物咳。

清單13. 利用子 shell 實(shí)現(xiàn)局部變量
[root@localhost shell]# cat -n getline3.sh
1  #!/bin/bash
2
3  GetLine()
4  {
5    string=$1
6    file=$2
7
8    line=grep -n $string $file
9    if [ $? -eq 0 ]
10    then
11      printf "$string is found as the %drd line in $file \n" echo $line  \
| cut -f1 -d:
12      num=grep $string $file | wc -l
13      rtn=0
14    else
15      printf "$string is not found in $file \n"
16      num=0
17      rtn=1
18    fi
19
20    return $rtn;
21  }
22
23  if [ ! -f testfile.$$ ]
24  then
25    cat >> testfile.$$ <<EOF
26  first line .
27  second line ..
28  third line ...
29  EOF
30  fi
31
32  num=0
33  rtn=0
34  for i in "second" "six" "line"
35  do
36    echo
37    (GetLine $i testfile.$$)
38    echo "return value: $? (rtn = $rtn)"
39
40    if [ $num -gt 0 ]
41    then
42      echo "$num occurences found totally."
43    fi
44  done
[root@localhost shell]# ./getline3.sh
second is found as the 2rd line in testfile.4534
return value: 0 (rtn = 0)
six is not found in testfile.4534
return value: 1 (rtn = 0)
line is found as the 1rd line in testfile.4534
return value: 0 (rtn = 0)

在清單 13 中锣险,GetLine 函數(shù)并不需要任何變化,變量定義和程序調(diào)用都沿用正常方式览闰。唯一的區(qū)別在于調(diào)用該函數(shù)時(shí)芯肤,要將其作為一個(gè)子 shell 來調(diào)用(請(qǐng)注意第 37 行兩邊的圓括號(hào))。另外一個(gè)問題是在子 shell 中修改的所有變量對(duì)于原有 shell 來說都是不可見的压鉴,這也就是為什么在第 38 行要通過 $? 來檢查返回值崖咨,而 rtn 變量的值卻是錯(cuò)誤的。另外由于 num 在 GetLine 函數(shù)中也被當(dāng)作是局部變量油吭,同樣無法將修改后的值傳出來击蹲,因此也并沒有打印所匹配到的 line 的數(shù)目是 3 行的信息。

解決上面這個(gè)問題就只能使用前面提到的利用標(biāo)準(zhǔn)輸入輸出設(shè)備的方法了婉宰,否則即使使用間接變量引用也無法正常工作歌豺。清單 14 給出了一個(gè)使用間接變量引用的例子,盡管我們使用不同的名字來命名全局變量和局部變量心包,從而確保不會(huì)引起同名混淆类咧,但是依然無法正常工作。原因同樣在于 GetLine 函數(shù)是在另外一個(gè)子進(jìn)程中運(yùn)行的蟹腾,它對(duì)變量所做的更新隨著子 shell 的退出就消失了痕惋。

清單14. 利用間接變量索引也無法解決子 shell 通過變量回傳值的問題
[root@localhost shell]# cat -n getline4.sh
1  #!/bin/bash
2
3  GetLine()
4  {
5    string=$1
6    file=$2
7    num=$3
8    rtn=$4
9
10    line=grep -n $string $file
11    if [ $? -eq 0 ]
12    then
13      printf "$string is found as the %drd line in $file \n"  \
        echo $line | cut -f1 -d:
14      eval ${num}=grep $string $file | wc -l
15      eval ${rtn}=0
16    else
17      printf "$string is not found in $file \n"
18      eval ${num}=0
19      eval ${rtn}=1
20    fi
21
22    return ${!rtn};
23  }
24
25  if [ ! -f testfile.$$ ]
26  then
27    cat >> testfile.$$ <<EOF
28  first line .
29  second line ..
30  third line ...
31  EOF
32  fi
33
34  g_num=0
35  g_rtn=0
36  for i in "second" "six" "line"
37  do
38    echo
39    (GetLine $i testfile.$$ g_num g_rtn)
40    echo "return value: $? (g_rtn = $g_rtn)"
41
42    if [ $g_num -gt 0 ]
43    then
44      echo "$g_num occurence(s) found totally."
45    fi
46  done
[root@localhost shell]# ./getline4.sh
second is found as the 2rd line in testfile.4576
return value: 0 (g_rtn = 0)
six is not found in testfile.4576
return value: 1 (g_rtn = 0)
line is found as the 1rd line in testfile.4576
return value: 0 (g_rtn = 0)

性能問題

盡管編寫 bash 腳本可以實(shí)現(xiàn)遞歸函數(shù),但是由于先天性的不足娃殖,使用 bash 腳本編寫的遞歸函數(shù)的性能都比較差值戳,問題的根本在于它的主要流程都是要不斷地調(diào)用其他程序,這會(huì) fork 出很多進(jìn)程炉爆,從而極大地增加運(yùn)行時(shí)的開銷述寡。下面讓我們來看一個(gè)計(jì)算累加和的例子柿隙,清單 15 和清單 16 給出了兩個(gè)實(shí)現(xiàn),它們分別利用全局變量和標(biāo)準(zhǔn)輸入輸出設(shè)備來傳遞返回值鲫凶。為了簡(jiǎn)單起見,我們也不對(duì)輸入?yún)?shù)進(jìn)行任何判斷衩辟。

清單15. 累加和螟炫,利用全局變量傳遞返回值
[root@localhost shell]# cat -n sum1.sh
1  #!/bin/bash
2
3  sum()
4  {
5    local i=$1
6
7    if [ $i -eq 1 ]
8    then
9      rtn=1
10    else
11      sum expr $i - 1
12      rtn=expr $i + $rtn 
13    fi
14
15    return $rtn
16  }
17
18  if [ -z $1 ]
19  then
20    echo "Need one parameter."
21    exit 1
22  fi
23
24  sum $1
25
26  echo $rtn

清單16. 累加和,利用標(biāo)準(zhǔn)輸入輸出設(shè)備傳遞返回值
[root@localhost shell]# cat -n sum2.sh
1  #!/bin/bash
2
3  sum()
4  {
5    local i=$1
6
7    if [ $i -eq 1 ]
8    then
9      echo 1
10    else
11      local j=expr $i - 1
12      local k=sum $j
13      echo expr $i + $k 
14    fi
15  }
16
17  if [ -z $1 ]
18  then
19    echo "Need one parameter."
20    exit 1
21  fi
22
23  rtn=sum $1
24  echo $rtn

下面讓我們來測(cè)試一下這兩個(gè)實(shí)現(xiàn)的性能會(huì)有多大的差距:

清單17. 利用全局變量和標(biāo)準(zhǔn)輸入輸出設(shè)備傳遞返回值的性能比較
[root@localhost shell]# cat -n run.sh
1  #!/bin/bash
2
3  if [ $# -lt 2 ]
4  then
5    echo "Usage: $0 [number] [executable list]"
6    exit 1
7  fi
8
9  NUM=$1
10  shift
11
12  for i in $*
13  do
14    echo "Running command: $i $NUM"
15    time ./$i $NUM
16
17    sleep 5
18    echo
19  done
[root@localhost shell]# ./run.sh 500 sum1.sh sum2.sh
Running command: sum1.sh 500
125250
real    0m8.336s
user    0m0.532s
sys     0m7.772s
Running command: sum2.sh 500
125250
real    0m20.775s
user    0m1.316s
sys     0m17.741s

在計(jì)算 1 到 500 的累加和時(shí)艺晴,利用標(biāo)準(zhǔn)輸入輸出設(shè)備傳遞返回值的方法速度要比利用全局變量慢 1 倍以上昼钻。隨著迭代次數(shù)的增加,二者的差距也會(huì)越來越大封寞,主要原因標(biāo)準(zhǔn)輸入輸出設(shè)備都是字符設(shè)備然评,從中讀寫數(shù)據(jù)耗時(shí)會(huì)很長(zhǎng);而全局變量則是在內(nèi)存中進(jìn)行操作的狈究,速度會(huì)明顯快很多碗淌。

為了提高 shell 腳本的性能,在編寫 shell 腳本時(shí)抖锥,應(yīng)該盡量多使用 shell 的內(nèi)嵌命令亿眠,而不能過多地調(diào)用外部腳本或命令,因?yàn)檎{(diào)用內(nèi)嵌命令時(shí)不會(huì) fork 新的進(jìn)程磅废,而是在當(dāng)前 shell 環(huán)境中直接執(zhí)行這些命令纳像,這樣可以減少很多系統(tǒng)開銷。以計(jì)算表達(dá)式的值為例拯勉,前面的例子我們都是通過調(diào)用 expr 來對(duì)表達(dá)式進(jìn)行求值的竟趾,但是 bash 中提供了一些內(nèi)嵌的計(jì)算表達(dá)式手段,例如 ((i = j +k)) 與 i=expr $j + $k 的效果就是完全相同的宫峦,都是計(jì)算變量 j 與 k 的值岔帽,并將結(jié)果賦值給變量 i,但是前者卻節(jié)省了一次 fork 新進(jìn)程以及執(zhí)行 expr 命令的開銷斗遏。下面讓我們對(duì)清單 15 中的腳本進(jìn)行一下優(yōu)化山卦,如清單 18 所示。

清單18. 優(yōu)化后的計(jì)算累加和的腳本
[root@localhost shell]# cat -n sum3.sh
1  #!/bin/bash
2
3  sum()
4  {
5    local i=$1
6
7    if [ $i -eq 1 ]
8    then
9      rtn=1
10    else
11      sum $(($i - 1))
12      ((rtn = rtn + i))
13    fi
14
15    return $rtn
16  }
17
18  sum $1
19
20  echo $rtn

現(xiàn)在讓我們來比較一下優(yōu)化前后的性能差距诵次,如清單 19 所示账蓉。

清單19. 優(yōu)化前后的性能對(duì)比
[root@localhost shell]# ./run.sh 2000 sum1.sh sum3.sh
Running command: sum1.sh 2000
2001000
real    1m19.378s
user    0m15.877s
sys     1m3.472s
Running command: sum3.sh 2000
2001000
real    0m12.202s
user    0m10.949s
sys     0m1.220s

可以看出,在迭代 2000 次時(shí)逾一,優(yōu)化后的腳本速度要比優(yōu)化前快 5 倍以上铸本。但是無論如何,使用 shell 腳本編寫的遞歸函數(shù)的執(zhí)行效率都不高遵堵,c 語(yǔ)言的實(shí)現(xiàn)與其相比箱玷,快了可能都不止一個(gè)數(shù)量級(jí)怨规,詳細(xì)數(shù)據(jù)請(qǐng)參看清單 20。

清單20. c 語(yǔ)言與 bash 腳本實(shí)現(xiàn)遞歸函數(shù)的對(duì)比
[root@localhost shell]# cat -n sum.c
1  #include <stdlib.h>
2  #include <stdio.h>
3
4  int sum(int i)
5  {
6    if (i == 1)
7      return 1;
8    else
9      return i + sum(i-1);
10  }
11
12  int main(int argc, char **argv[])
13  {
14    printf("%d\n", sum(atoi((char *)argv[1])));
15  }
[root@localhost shell]# gcc -O2 -o sum sum.c
[root@localhost shell]# ./run.sh 3000 sum sum3.sh
Running command: sum 3000
4501500
real    0m0.004s
user    0m0.000s
sys     0m0.004s
Running command: sum3.sh 3000
4501500
real    0m31.182s
user    0m28.998s
sys     0m2.004s

因此锡足,如果編寫對(duì)性能要求很高的遞歸程序波丰,還是選擇其他語(yǔ)言實(shí)現(xiàn)好了,這并不是 shell 的強(qiáng)項(xiàng)舶得。

小結(jié)

本文從經(jīng)典的 fork 炸彈遞歸函數(shù)入手掰烟,逐一介紹了在 bash 中編寫遞歸函數(shù)時(shí)需要注意的問題,包括返回值沐批、參數(shù)傳遞和性能等方面的問題以及解決方法纫骑,并對(duì)如何提高 shell 腳本性能提供了一個(gè)建議。

下載

相關(guān)主題

  • 請(qǐng)?jiān)L問 bash 的主頁(yè)先馆,從這里可以獲得最新版本的 bash 源代碼以及詳細(xì)的手冊(cè)。
  • Shell 腳本調(diào)試技術(shù)一文中介紹了常用的一些 shell 腳本調(diào)試方法躺彬。
  • 希望擁有一個(gè)像 gdb 一樣功能強(qiáng)大的 shell 腳本調(diào)試器嗎煤墙?bashdb 就是最好的選擇,詳細(xì)信息請(qǐng)參看bashdb 的主頁(yè)顾患。
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末番捂,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子江解,更是在濱河造成了極大的恐慌设预,老刑警劉巖,帶你破解...
    沈念sama閱讀 217,406評(píng)論 6 503
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件犁河,死亡現(xiàn)場(chǎng)離奇詭異鳖枕,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)桨螺,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,732評(píng)論 3 393
  • 文/潘曉璐 我一進(jìn)店門宾符,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人灭翔,你說我怎么就攤上這事魏烫。” “怎么了肝箱?”我有些...
    開封第一講書人閱讀 163,711評(píng)論 0 353
  • 文/不壞的土叔 我叫張陵哄褒,是天一觀的道長(zhǎng)。 經(jīng)常有香客問我煌张,道長(zhǎng)呐赡,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,380評(píng)論 1 293
  • 正文 為了忘掉前任骏融,我火速辦了婚禮链嘀,結(jié)果婚禮上萌狂,老公的妹妹穿的比我還像新娘。我一直安慰自己怀泊,他們只是感情好茫藏,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,432評(píng)論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著包个,像睡著了一般刷允。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上碧囊,一...
    開封第一講書人閱讀 51,301評(píng)論 1 301
  • 那天,我揣著相機(jī)與錄音纤怒,去河邊找鬼糯而。 笑死,一個(gè)胖子當(dāng)著我的面吹牛泊窘,可吹牛的內(nèi)容都是我干的熄驼。 我是一名探鬼主播,決...
    沈念sama閱讀 40,145評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼烘豹,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼瓜贾!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起携悯,我...
    開封第一講書人閱讀 39,008評(píng)論 0 276
  • 序言:老撾萬榮一對(duì)情侶失蹤祭芦,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后憔鬼,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體龟劲,經(jīng)...
    沈念sama閱讀 45,443評(píng)論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,649評(píng)論 3 334
  • 正文 我和宋清朗相戀三年轴或,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了昌跌。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 39,795評(píng)論 1 347
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡照雁,死狀恐怖蚕愤,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情饺蚊,我是刑警寧澤萍诱,帶...
    沈念sama閱讀 35,501評(píng)論 5 345
  • 正文 年R本政府宣布,位于F島的核電站卸勺,受9級(jí)特大地震影響砂沛,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜曙求,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,119評(píng)論 3 328
  • 文/蒙蒙 一碍庵、第九天 我趴在偏房一處隱蔽的房頂上張望映企。 院中可真熱鬧,春花似錦静浴、人聲如沸堰氓。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,731評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)双絮。三九已至,卻和暖如春得问,著一層夾襖步出監(jiān)牢的瞬間囤攀,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,865評(píng)論 1 269
  • 我被黑心中介騙來泰國(guó)打工宫纬, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留焚挠,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 47,899評(píng)論 2 370
  • 正文 我出身青樓漓骚,卻偏偏與公主長(zhǎng)得像蝌衔,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子蝌蹂,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,724評(píng)論 2 354

推薦閱讀更多精彩內(nèi)容

  • 官網(wǎng) 中文版本 好的網(wǎng)站 Content-type: text/htmlBASH Section: User ...
    不排版閱讀 4,381評(píng)論 0 5
  • 一噩斟、Python簡(jiǎn)介和環(huán)境搭建以及pip的安裝 4課時(shí)實(shí)驗(yàn)課主要內(nèi)容 【Python簡(jiǎn)介】: Python 是一個(gè)...
    _小老虎_閱讀 5,744評(píng)論 0 10
  • 第 2 章 SHELL 基礎(chǔ)知識(shí)2.1 shell腳本我們?cè)谏厦婧?jiǎn)單介紹了一下什么是shell腳本,現(xiàn)在我們來進(jìn)一...
    LiWei_9e4b閱讀 1,569評(píng)論 0 0
  • 來源: Linux命令行與shell腳本編程大全 博客地址孤个,推薦電腦點(diǎn) 內(nèi)容 基本的腳本函數(shù)返回值在函數(shù)中使用變量...
    王詩(shī)翔閱讀 2,466評(píng)論 0 3
  • 心中有了夢(mèng)想剃允,看到的天空都是暖暖的藍(lán)色
    Heaxui閱讀 75評(píng)論 0 0