編寫(xiě)Linux 實(shí)用程序的藝術(shù)
Linux 和其他類(lèi) UNIX 系統總是附帶了大量的工具,它們執行從顯而易見(jiàn)的到不可思議的廣泛功能。類(lèi) UNIX 編程環(huán)境的成功很大程度上歸功于工具的高品質(zhì)和選擇,以及這些工具之間相互銜接的簡(jiǎn)易性。
本文引用地址:http://dyxdggzs.com/article/257941.htm作為開(kāi)發(fā)人員,您可能會(huì )發(fā)現現有實(shí)用程序并不總是能夠解決問(wèn)題。雖然能夠通過(guò)結合使用現有實(shí)用程序來(lái)容易地解決許多問(wèn)題,然而解決其他問(wèn)題卻至少需要一些實(shí)際的編程工作。這些后面的任務(wù)通常是創(chuàng )建新實(shí)用程序的候選任務(wù),結合現有實(shí)用程序來(lái)創(chuàng )建新實(shí)用程序可以通過(guò)做最少的工作來(lái)解決問(wèn)題。本文考察優(yōu)秀實(shí)用程序所具有的品質(zhì),以及設計這種實(shí)用程序所經(jīng)歷的過(guò)程。
優(yōu)秀的實(shí)用程序具有哪些品質(zhì)?
Kernighan Pike 所著(zhù)的 The UNIX Programming Environment 一書(shū)中包含了對此問(wèn)題的精彩討論。優(yōu)秀的實(shí)用程序是把自己的工作做得盡可能好的實(shí)用程序。它必須與其他實(shí)用程序配合融洽;必須能夠容易地與其他實(shí)用程序結合使用。無(wú)法與其他實(shí)用程序結合使用的程序不是實(shí)用程序,而是應用程序。
實(shí)用程序應該允許您根據手邊的材料廉價(jià)而容易地構建一次性的應用程序。許多人認為實(shí)用程序就像是工具箱中的工具。設計實(shí)用程序的目標不是為了讓單個(gè)工具來(lái)做所有事情,而是為了擁有一組工具,其中每個(gè)工具都盡可能好地做一件事情。
有些實(shí)用程序自身就是相當有用的,而其他實(shí)用程序則必須與一系列實(shí)用程序配合使用。前者的例子包括 sort 和 grep 。另一方面, xargs 除了與其他實(shí)用程序(最常見(jiàn)的是 find )配合使用外,很少單獨使用。
使用什么語(yǔ)言來(lái)編寫(xiě)實(shí)用程序?
大多數 UNIX 系統實(shí)用程序都是用 C 語(yǔ)言來(lái)編寫(xiě)的。本文中的例子使用 Perl 和 sh。應該使用恰當的工具來(lái)做恰當的事情。如果您對某個(gè)實(shí)用程序使用得足夠頻繁,那么用編譯型語(yǔ)言來(lái)編寫(xiě)它的成本也許能通過(guò)性能提升來(lái)獲得回報。另一方面,對于程序的工作負荷很輕這種相當普遍的情況,使用腳本語(yǔ)言也許會(huì )提供更快的開(kāi)發(fā)速度。
如果無(wú)法肯定,您應該使用自己最了解的語(yǔ)言。至少當您在對某個(gè)實(shí)用程序進(jìn)行原型化,或在弄清它是如何有用時(shí),程序員效率將優(yōu)先于性能調整。大多數 UNIX 系統實(shí)用程序都是用 C 編寫(xiě)的,這只是因為這些實(shí)用程序使用得足夠頻繁,以致考慮效率比考慮開(kāi)發(fā)成本更加重要。Perl 和 sh(或 ksh)可能是用于快速原型化的很好語(yǔ)言。對于與其他程序配合實(shí)用的實(shí)用程序,使用 shell 來(lái)編寫(xiě)它們或許要比使用更傳統的編程語(yǔ)言來(lái)編寫(xiě)它們要容易一些。另一方面,當您希望與原始的字節交互時(shí),C 或許就是最好的選擇。
設計實(shí)用程序
一個(gè)不錯的經(jīng)驗法則就是當您第二次必須解決某個(gè)問(wèn)題時(shí),首先考慮實(shí)用程序的設計。不要對第一次編寫(xiě)的一次性作品感到遺憾;您可以將它看作是一個(gè)原型。第二次,請把您所需的功能與第一次所需的功能作比較。在第三次前后,您應該開(kāi)始考慮花時(shí)間來(lái)編寫(xiě)一個(gè)通用實(shí)用程序。即使純粹的重復性任務(wù)也可能會(huì )給實(shí)用程序的開(kāi)發(fā)帶來(lái)好處;例如,由于人們對嘗試以通用的方式重命名文件感到失望,于是開(kāi)發(fā)了許多通用文件重命名程序。
下面是一些實(shí)用程序設計目標;每個(gè)目標將在下面單獨的小節中介紹。
做好一件事情。
成為一個(gè)過(guò)濾器。
通用化。
健壯。
新穎。
做好一件事情
做好一件事情;不要糟糕地做多件事情。關(guān)于做好一件事情的最佳例子或許是 sort 。除了 sort 外,沒(méi)有 其他哪個(gè)實(shí)用程序具有排序功能?;镜乃枷牒芎?jiǎn)單:如果一次僅解決一個(gè)問(wèn)題,您就能花時(shí)間把它解決好。
設想一下,如果大多數程序都具有排序功能,但是有些僅支持按詞法排序,而其他一些僅支持按數字排序,另外一些甚至支持關(guān)鍵字選擇而不是對整行排序,那將是一件多么令人沮喪的事情。起碼,這也是惱人的。
當您發(fā)現某個(gè)問(wèn)題需要解決時(shí),應嘗試將問(wèn)題分解為多個(gè)部分,不要重復那些其他實(shí)用程序中已經(jīng)存在的部分。您對允許配合現有工具使用的工具關(guān)注得越多,您的實(shí)用程序就越有可能保持有用。
也許您需要編寫(xiě)多個(gè)程序。完成專(zhuān)門(mén)任務(wù)的最佳途徑通常是編寫(xiě)一兩個(gè)實(shí)用程序,再用一些線(xiàn)索將它們聯(lián)系起來(lái),而不是編寫(xiě)單個(gè)程序來(lái)解決整件事情。使用 20 行的 shell 腳本來(lái)將新的實(shí)用程序與現有工具結合起來(lái)是很理想的。如果嘗試一次解決整個(gè)問(wèn)題,隨之而來(lái)的第一個(gè)變更就可能要求您全盤(pán)重新考慮。
我偶爾需要從數據庫生成兩列或三列的輸出。編寫(xiě)一個(gè)程序在單個(gè)列中生成輸出,然后結合使用一個(gè)對輸出進(jìn)行分列的程序,這樣通常會(huì )更有效率。組合這兩個(gè)實(shí)用程序的 shell 腳本本身是臨時(shí)性的,單獨的實(shí)用程序比這個(gè)腳本的使用壽命更長(cháng)。
有些實(shí)用程序服務(wù)于非常專(zhuān)一的需要。針對一個(gè)包含大量?jì)热莸哪夸?,如?ls 的輸出非??斓貪L出屏幕,這可能是因為其中有一個(gè)文件具有非常長(cháng)的文件名,從而迫使 ls 僅對輸出使用單個(gè)列。使用 more 來(lái)對輸出分頁(yè)會(huì )花一些時(shí)間。為什么不像下面這樣就按長(cháng)度對行排序,然后通過(guò) tail 來(lái)管道輸出結果呢?
清單 1. 世間能找到的最小實(shí)用程序 sl
#/usr/bin/perl -w
print sort { length $a => length $b } >;
清單 1 中的腳本確切地就做一件事情。它不接受任何選項,因為它不需要選項;它僅關(guān)心行的長(cháng)度。歸功于 Perl 便利的 > 表達方式,這個(gè)小實(shí)用程序既適用于標準輸入,也適用于命令行指定的文件。
成為一個(gè)過(guò)濾器
幾乎所有實(shí)用程序都最適合想像為過(guò)濾器,盡管有一些非常有用的實(shí)用程序不符合這個(gè)模型。(例如,某個(gè)程序在執行計數時(shí)可能非常有用,盡管它作為過(guò)濾器工作得并不好。僅接受命令行參數作為輸入并潛在地產(chǎn)生復雜輸出的程序可能非常有用。)然而,大多數實(shí)用程序都應該作為過(guò)濾器來(lái)工作。根據慣例,過(guò)濾器對文本的行起作用。大多數過(guò)濾器都應該支持多個(gè)輸入文件。
記住實(shí)用程序需要在命令行和腳本中運行。有時(shí),理想的行為會(huì )稍有不同。例如,大多數版本的 ls 都會(huì )在向終端寫(xiě)出時(shí)自動(dòng)將輸入排序到多個(gè)列中。 grep 的默認行為是在指定多個(gè)文件的情況下打印從其中找到匹配項的那個(gè)文件名稱(chēng)。這樣的差別應該與用戶(hù)希望的實(shí)用程序工作方式有關(guān),而不是與其他事項有關(guān)。例如,舊版本的 GNU bc 在啟動(dòng)時(shí)顯示強迫性的版權標記。請不要那樣做。讓您的實(shí)用程序僅做它應該做的事情。
實(shí)用程序喜歡生活在管道中。管道允許實(shí)用程序專(zhuān)注于自己的工作,而不是去關(guān)注旁枝末節。為了生活在管道中,實(shí)用程序需要從標準輸入讀取數據,然后向標準輸出寫(xiě)出數據。如果您希望處理記錄,那么您最好能夠使每一行成為一個(gè)“記錄”。諸如 sort 和 join 之類(lèi)的現有程序已經(jīng)在那樣考慮了。它們將會(huì )因為您這樣做而感謝您。
我偶爾使用這樣一個(gè)實(shí)用程序,它針對一個(gè)文件樹(shù)反復調用其他程序。這充分利用了標準的 UNIX 實(shí)用程序過(guò)濾器模型,但是該模型僅適用于讀取輸入然后寫(xiě)出輸出的實(shí)用程序;不能將它用于就地操作或接受輸入輸出文件名的實(shí)用程序。
可以使用標準輸入來(lái)運行的大多數程序也完全可以針對單個(gè)文件或一組文件運行。注意,可以證明這樣違背了反對重復工作的規則;顯而易見(jiàn),這可以通過(guò)將 cat 的輸出饋送給該系列中的下一個(gè)程序來(lái)解決。然而這在實(shí)踐中似乎是合理的。
有些程序可能合法地讀取一種格式的記錄,但是卻產(chǎn)生完全不同的輸出。這樣的一個(gè)例子就是將輸入材料劃分為列的實(shí)用程序。這樣一個(gè)實(shí)用程序可能將輸入中的行視為記錄,但是卻在輸出中的每行上產(chǎn)生多個(gè)記錄。
并非每個(gè)實(shí)用程序都完全符合這個(gè)模型。例如, xargs 不是接受記錄而是接受文件名作為輸入,并且所有的實(shí)際處理都是由其他程序完成的。
通用化
嘗試將任務(wù)看作與您實(shí)際執行的任務(wù)類(lèi)似;如果您能找出這些任務(wù)的通用描述,那么最好嘗試編寫(xiě)一個(gè)符合該描述的實(shí)用程序。例如,如果您發(fā)現自己一天在根據詞法對文本排序,而另一天在根據數字對文本排序,那么考慮編寫(xiě)一個(gè)通用排序實(shí)用程序也許是有意義的。
對功能進(jìn)行通用化有時(shí)會(huì )導致您發(fā)現:某個(gè)看起來(lái)似乎像單個(gè)實(shí)用程序的程序,實(shí)際上卻是配合起來(lái)使用的兩個(gè)實(shí)用程序。這很好。編寫(xiě)兩個(gè)設計良好的實(shí)用程序可能要比編寫(xiě)一個(gè)丑陋的或復雜的實(shí)用程序更容易。
做好一件事情并不意味著(zhù) 僅僅做一件事情。它意味著(zhù)處理一致但有用的問(wèn)題空間。許多人都使用 grep 。然而,它的大量效用在于執行相關(guān)任務(wù)的能力。 grep 的各種選項完成許多小實(shí)用程序的工作,如果這些工作都由單獨的小實(shí)用程序來(lái)完成,最終會(huì )造成大量共享的、重復的代碼。
這條規則,以及做好一件事情的規則,都是一個(gè)根本原理的必然結果:無(wú)論何時(shí)都要盡可能避免代碼重復。如果您編寫(xiě)半打程序,其中每個(gè)都對行排序,您最終可能必須六次修復六個(gè)類(lèi)似的 bug,而不是去使用一個(gè)得到更好維護的 sort 程序。
這是編寫(xiě)實(shí)用程序的一部分,即把大多數工作添加到完成該實(shí)用程序的過(guò)程中。您也許沒(méi)有時(shí)間在最初就完全通用化一個(gè)實(shí)用程序,但是當您一直使用該實(shí)用程序就會(huì )獲得相應的回報。
有時(shí),向某個(gè)程序添加相關(guān)功能是很有用的,即使這個(gè)功能并不是用來(lái)完成完全相同的任務(wù)。例如,當運行在終端設備上時(shí),對原始二進(jìn)制數據進(jìn)行完美打印的程序可能更為有用,因為它使終端進(jìn)入原始模式。這樣使得測試涉及鍵盤(pán)映射、新鍵盤(pán)等的問(wèn)題變得容易多了。不確定為什么當您按 delete 鍵時(shí)卻得到代字號(~)嗎? 這是弄清實(shí)際發(fā)送了什么內容的容易途徑。這并不是完全相同的任務(wù),但它足夠類(lèi)似,因而可能成為一個(gè)附加特性。
清單 2 中的 errno 實(shí)用程序就是通用化的很好例子,因為它同時(shí)支持數字和符號名稱(chēng)。
健壯
實(shí)用程序的穩定性是很重要的。容易崩潰或無(wú)法處理真實(shí)數據的實(shí)用程序不是有用的實(shí)用程序。實(shí)用程序應該能夠處理任意長(cháng)度的行、巨型文件,等等。實(shí)用程序無(wú)法處理超過(guò)其內存容量的數據集或許是可以容忍的,但是有些實(shí)用程序不是這樣;例如, sort 通過(guò)使用臨時(shí)文件,一般能夠對比其內存容量大得多的數據集排序。
應該盡量確保弄清楚您的實(shí)用程序可能要操作哪些數據。不要簡(jiǎn)單地忽略無(wú)法處理的數據的可能性。應該檢查這種情況并診斷您的實(shí)用程序。錯誤消息越明確,您對用戶(hù)就越有幫助。盡量給用戶(hù)提供足夠的信息,以便讓他們知道發(fā)生了什么情況以及如何解決。當處理數據文件時(shí),盡量準確識別出不良的數據。當嘗試解析數字時(shí),不要簡(jiǎn)單地放棄;應該告訴用戶(hù)您得到了什么數據,而且如果可能的話(huà),還應該告訴用戶(hù)該數據位于輸入流中的哪一行上。
作為一個(gè)很好的例子,請考慮 dc 的兩種實(shí)現之間的區別。如果您運行 dc /home ,其中一種實(shí)現會(huì )顯示“Cannot use directory as input!”而另一種實(shí)現只是無(wú)聲地返回,沒(méi)有錯誤消息,也沒(méi)有不尋常的退出代碼。當您錯誤地鍵入一個(gè) cd 命令時(shí),您更希望當前路徑中有哪一種實(shí)現呢?類(lèi)似地,如果您提供某個(gè)目錄中的數據流(或許是執行 dc /home ),前者會(huì )給出詳細的錯誤消息。另一方面,當它在獲得無(wú)效數據的早期就選擇放棄可能是理想的。
安全漏洞經(jīng)常植根于在意料之外的數據面前表現得不夠健壯的程序中。務(wù)必記住,優(yōu)秀的實(shí)用程序能夠設法在 shell 腳本中作為根(root)用戶(hù)身份運行。諸如 find 這樣的程序中的緩沖區溢出可能會(huì )給大量的系統帶來(lái)風(fēng)險。
程序對意料之外的數據處理得越好,它就更可能適應變化的環(huán)境。通常,設法使程序更健壯會(huì )導致您更好地理解該程序的作用,從而更好地使之通用化。
新穎
要編寫(xiě)的最糟糕的實(shí)用程序種類(lèi)之一就是您已經(jīng)有了的實(shí)用程序。我編寫(xiě)過(guò)一個(gè)名為 count 的美妙的實(shí)用程序。它允許我執行幾乎任何計數任務(wù)。它是一個(gè)出色的實(shí)用程序,但是已經(jīng)有一個(gè)名為 jot 的標準 BSD 實(shí)用程序做同樣的事情。同樣地,我的一個(gè)用于將數據轉換為列的靈活的程序重復了一個(gè)現有實(shí)用程序 rs 的功能,這個(gè)實(shí)用程序同樣可以在 BSD 系統上找到,只不過(guò) rs 更靈活,設計得更好。請參閱下面的 參考資料 以了解關(guān)于 jot 和 rs 的更多信息。
如果您即將開(kāi)始編寫(xiě)一個(gè)實(shí)用程序,請花一點(diǎn)時(shí)間瀏覽一下各種系統,以確定那樣的實(shí)用程序是否已經(jīng)存在。不要害怕在 BSD 上借用 Linux 實(shí)用程序,或在 Linux 上借用 BSD 實(shí)用程序;實(shí)用程序代碼的樂(lè )趣之一在于,幾乎所有實(shí)用程序都具有很好的可移植性。
不要忘了考察一下組合現有應用程序來(lái)形成一個(gè)實(shí)用程序的可能性。從理論上講,組合現有程序來(lái)形成的實(shí)用程序運行得不足夠快是可能的,但是編寫(xiě)一個(gè)新的實(shí)用程序很少會(huì )比等待一個(gè)稍慢的管道更快。
一個(gè)例子實(shí)用程序
從某種意義上,這個(gè)程序是一個(gè)可執行文件,因為對于作為過(guò)濾器來(lái)說(shuō),它決不會(huì )有任何用處。然而,它作為一個(gè)命令行實(shí)用程序卻工作得非常好。
這個(gè)程序僅做一件事情。它以近乎完美的輸出格式輸出 /usr/include/sys/errno.h 中的 errno 行。例如:
$ errno 22
EINVAL [22]: Invalid argument
清單 2. errno 查找器
#!/bin/sh
usage() {
echo >2 usage: errno [numbers or error names]n
exit 1
}
for i
do
case $i in
[0-9]*)
awk '/^#define/ $3 == '$i' {
for (i = 5; i NF; ++i) {
foo = foo $i;
}
printf(%-22s%sn, $2 [ $3 ]:, foo);
foo =
}' /usr/include/sys/errno.h
;;
E*)
awk '/^#define/ $2 == '$i' {
for (i = 5; i NF; ++i) {
foo = foo $i;
}
printf(%-22s%sn, $2 [ $3 ]:, foo);
foo =
}' /usr/include/sys/errno.h
;;
*)
echo >2 errno: can't figure out whether '$i' is a name or a number.
usage
;;
esac
done
這個(gè)程序通用化了嗎?是的,非常理想。它同時(shí)支持數字和符號名稱(chēng)。另一方面,它不知道關(guān)于可能具有相同格式的其他文件的信息,比如 /usr/include/sys/signal.h??梢匀菀椎財U展它來(lái)做到這點(diǎn),但是對于這樣一個(gè)便利的實(shí)用能夠程序,簡(jiǎn)單地創(chuàng )建一個(gè)名為“signal”的拷貝來(lái)讀取 signal.h,同時(shí)使用“SIG*”作為模式來(lái)匹配名稱(chēng),這樣會(huì )更容易。
雖然這僅比對系統頭文件使用 grep 方便一小點(diǎn),但是它更不容易出錯。它不會(huì )因為考慮不周的參數而產(chǎn)生無(wú)用的結果。另一方面,如果沒(méi)有從頭文件中找到給定的名稱(chēng)或數字,它不會(huì )產(chǎn)生診斷信息。它也不會(huì )費心去糾正某些輸入錯誤。而且,由于命令行實(shí)用程序從來(lái)沒(méi)有打算在自動(dòng)化的環(huán)境中使用,因此它的上述特性無(wú)可非議。
另一個(gè)例子可能是取消對輸入排序的程序(請參閱 參考資料 以獲得指向此實(shí)用程序的鏈接)。這相當簡(jiǎn)單;也就是讀入輸入文件,以某種方式存儲它們,然后生成一個(gè)隨機順序來(lái)輸出那些行。這是一個(gè)幾乎具有無(wú)限應用前景的實(shí)用程序。編寫(xiě)這個(gè)實(shí)用程序也比編寫(xiě)排序程序容易得多;例如,您不需要指定您沒(méi)有對哪些鍵排序,或者是您希望按字母順序、詞法順序還是按數字順序隨機排序。棘手的部分在于讀入可能非常長(cháng)的行。事實(shí)上,上面提供的版本在搞欺騙;它假設所讀入的行中沒(méi)有空字節。糾正這個(gè)問(wèn)題要困難多了,我在編寫(xiě)它時(shí)懶得去理會(huì )它。
結束語(yǔ)
如果您發(fā)現自己在重復執行某個(gè)任務(wù),可以考慮編寫(xiě)一個(gè)程序來(lái)完成這個(gè)任務(wù)。如果事實(shí)證明該程序更通用化一點(diǎn)是合理的,那就通用化它,這樣您就編寫(xiě)了一個(gè)實(shí)用程序。
不要在您第一次需要某個(gè)實(shí)用程序的時(shí)候設計它。要等到您具有一些經(jīng)驗之后才著(zhù)手設計。請隨意地編寫(xiě)一兩個(gè)原型;優(yōu)秀的實(shí)用程序比糟糕的實(shí)用程序更能證明所花的時(shí)間和研究工作的價(jià)值。如果原先設想的出色實(shí)用程序最終卻在您編寫(xiě)它之后成為無(wú)用之物,不要感到遺憾。如果您發(fā)現自己對新程序的缺點(diǎn)感到沮喪,您只需再執行另外一個(gè)原型化階段。如果結果證明它是無(wú)用的,不奇怪,有時(shí)會(huì )發(fā)生這樣的事情。
您要尋求的是這樣一個(gè)程序,它查找您的最初使用模式之外的通用應用。我編寫(xiě) unsort 是因為,我希望找到一種從舊的 X11“rgb.txt”文件中獲得隨機顏色序列的容易途徑。從那以后,我將它用于令人難以置信的大量任務(wù)中,這些任務(wù)都不是為了生成用于調試和基準排序例程的測試數據。
優(yōu)秀的實(shí)用程序能夠為您在所有不很理想的作品上所花的時(shí)間帶來(lái)回報。要做的下一件事情是使它對其他人可用,以便他們能夠試驗它。也要使您失敗的嘗試對其他人可用,也許其他人對某個(gè)實(shí)用程序具有您所不需要的用途。更重要的是,您的失敗的實(shí)用程序也許是其他某個(gè)人的原型,從而給每個(gè)人帶來(lái)一個(gè)美妙的實(shí)用程序。
評論