用C語言編寫Linux實用程式的藝術(轉)

BSDLite發表於2007-08-11
用C語言編寫Linux實用程式的藝術(轉)[@more@]  Linux 和其他類 UNIX 系統總是附帶了大量的工具,它們執行從顯而易見的到不可思議的廣泛功能。類 UNIX 程式設計環境的成功很大程度上歸功於工具的高品質和選擇,以及這些工具之間相互銜接的簡易性。
  
  作為開發人員,您可能會發現現有實用程式並不總是能夠解決問題。雖然能夠透過結合使用現有實用程式來容易地解決許多問題,然而解決其他問題卻至少需要一些實 際的程式設計工作。這些後面的任務通常是建立新實用程式的候選任務,結合現有實用程式來建立新實用程式可以透過做最少的工作來解決問題。本文考察優秀實用程式所具有的品質,以及設計這種實用程式所經歷的過程。
  
  優秀的實用程式具有哪些品質?
  Kernighan & Pike 所著的 The UNIX Programming Environment 一書中包含了對此問題的精彩討論。優秀的實用程式是把自己的工作做得儘可能好的實用程式。它必須與其他實用程式配合融洽;必須能夠容易地與其他實用程式結合使用。無法與其他實用程式結合使用的程式不是實用程式,而是應用程式。
  
  實用程式應該允許您根據手邊的材料廉價而容易地構建一次性的應用程式。許多人認為實用程式就像是工具箱中的工具。設計實用程式的目標不是為了讓單個工具來做所有事情,而是為了擁有一組工具,其中每個工具都儘可能好地做一件事情。
  
  有些實用程式自身就是相當有用的,而其他實用程式則必須與一系列實用程式配合使用。前者的例子包括 sort 和 grep。另一方面,xargs 除了與其他實用程式(最常見的是 find)配合使用外,很少單獨使用。
  
  使用什麼語言來編寫實用程式?
  大多數 UNIX 系統實用程式都是用 C 語言來編寫的。本文中的例子使用 Perl 和 sh。應該使用恰當的工具來做恰當的事情。如果您對某個實用程式使用得足夠頻繁,那麼用編譯型語言來編寫它的成本也許能透過效能提升來獲得回報。另一方面,對於程式的工作負荷很輕這種相當普遍的情況,使用指令碼語言也許會提供更快的開發速度。
  
  如果無法肯定,您應該使用自己最瞭解的語言。至少當您在對某個實用程式進行原型化,或在弄清它是如何有用時,程式設計師效率將優先於效能調整。大多數 UNIX 系統實用程式都是用 C 編寫的,這只是因為這些實用程式使用得足夠頻繁,以致考慮效率比考慮開發成本更加重要。Perl 和 sh(或 ksh)可能是用於快速原型化的很好語言。對於與其他程式配合實用的實用程式,使用 shell 來編寫它們或許要比使用更傳統的程式語言來編寫它們要容易一些。另一方面,當您希望與原始的位元組互動時,C 或許就是最好的選擇。
  
  設計實用程式
  一個不錯的經驗法則就是當您第二次必須解決某個問題時,首先考慮實用程式的設計。不要對第一次編寫的一次性作品感到遺憾;您可以將它看作是一個原型。第二次,請把您所需的功能與第一次所需的功能作比較。在第三次前後,您應該開始考慮花時間來編寫一個通用實用程式。即使純粹的重複性任務也可能會給實用程式的開發帶來好處;例如,由於人們對嘗試以通用的方式重新命名檔案感到失望,於是開發了許多通用檔案重新命名程式。
  
  做好一件事情;不要糟糕地做多件事情。關於做好一件事情的最佳例子或許是 sort。除了 sort 外,沒有其他 哪個實用程式具有排序功能。基本的思想很簡單:如果一次僅解決一個問題,您就能花時間把它解決好。
  
  設想一下,如果大多數程式都具有排序功能,但是有些僅支援按詞法排序,而其他一些僅支援按數字排序,另外一些甚至支援關鍵字選擇而不是對整行排序,那將是一件多麼令人沮喪的事情。起碼,這也是惱人的。
  
  當您發現某個問題需要解決時,應嘗試將問題分解為多個部分,不要重複那些其他實用程式中已經存在的部分。您對允許配合現有工具使用的工具關注得越多,您的實用程式就越有可能保持有用。
  
  也許您需要編寫多個程式。完成專門任務的最佳途徑通常是編寫一兩個實用程式,再用一些線索將它們聯絡起來,而不是編寫單個程式來解決整件事情。使用 20 行的 shell 指令碼來將新的實用程式與現有工具結合起來是很理想的。如果嘗試一次解決整個問題,隨之而來的第一個變更就可能要求您全盤重新考慮。
  
  我偶爾需要從資料庫生成兩列或三列的輸出。編寫一個程式在單個列中生成輸出,然後結合使用一個對輸出進行分列的程式,這樣通常會更有效率。組合這兩個實用程式的 shell 指令碼本身是臨時性的,單獨的實用程式比這個指令碼的使用壽命更長。
  
  有些實用程式服務於非常專一的需要。針對一個包含大量內容的目錄,如果 ls 的輸出非常快地滾出螢幕,這可能是因為其中有一個檔案具有非常長的檔名,從而迫使 ls 僅對輸出使用單個列。使用 more 來對輸出分頁會花一些時間。為什麼不像下面這樣就按長度對行排序,然後透過 tail 來管道輸出結果呢?
  
  清單 1. 世間能找到的最小實用程式 sl
  
  #/usr/bin/perl -w
  print sort { length $a <=> length $b } <>;
  
  清單 1 中的指令碼確切地就做一件事情。它不接受任何選項,因為它不需要選項;它僅關心行的長度。歸功於 Perl 便利的 <> 表達方式,這個小實用程式既適用於標準輸入,也適用於命令列指定的檔案。
  
  成為一個過濾器
  幾乎所有實用程式都最適合想像為過濾器,儘管有一些非常有用的實用程式不符合這個模型。(例如,某個程式在執行計數時可能非常有用,儘管它作為過濾器工作得並不好。僅接受命令列引數作為輸入並潛在地產生複雜輸出的程式可能非常有用。)然而,大多數實用程式都應該作為過濾器來工作。根據慣例,過濾器對文字的行起作用。大多數過濾器都應該支援多個輸入檔案。
  
  記住實用程式需要在命令列和指令碼中執行。有時,理想的行為會稍有不同。例如,大多數版本的 ls 都會在向終端寫出時自動將輸入排序到多個列中。grep 的預設行為是在指定多個檔案的情況下列印從其中找到匹配項的那個檔名稱。這樣的差別應該與使用者希望的實用程式工作方式有關,而不是與其他事項有關。例如,舊版本的 GNU bc 在啟動時顯示強迫性的版權標記。請不要那樣做。讓您的實用程式僅做它應該做的事情。
  
  實用程式喜歡生活在管道中。管道允許實用程式專注於自己的工作,而不是去關注旁枝末節。為了生活在管道中,實用程式需要從標準輸入讀取資料,然後向標準輸出寫出資料。如果您希望處理記錄,那麼您最好能夠使每一行成為一個“記錄”。諸如 sort 和 join 之類的現有程式已經在那樣考慮了。它們將會因為您這樣做而感謝您。
  
  我偶爾使用這樣一個實用程式,它針對一個檔案樹反覆呼叫其他程式。這充分利用了標準的 UNIX 實用程式過濾器模型,但是該模型僅適用於讀取輸入然後寫出輸出的實用程式;不能將它用於就地操作或接受輸入輸出檔名的實用程式。
  
  可以使用標準輸入來執行的大多數程式也完全可以針對單個檔案或一組檔案執行。注意,可以證明這樣違背了反對重複工作的規則;顯而易見,這可以透過將 cat 的輸出饋送給該系列中的下一個程式來解決。然而這在實踐中似乎是合理的。
  
  有些程式可能合法地讀取一種格式的記錄,但是卻產生完全不同的輸出。這樣的一個例子就是將輸入材料劃分為列的實用程式。這樣一個實用程式可能將輸入中的行視為記錄,但是卻在輸出中的每行上產生多個記錄。
  
  並非每個實用程式都完全符合這個模型。例如,xargs 不是接受記錄而是接受檔名作為輸入,並且所有的實際處理都是由其他程式完成的。
  
  通用化
  嘗試將任務看作與您實際執行的任務類似;如果您能找出這些任務的通用描述,那麼最好嘗試編寫一個符合該描述的實用程式。例如,如果您發現自己一天在根據詞法對文字排序,而另一天在根據數字對文字排序,那麼考慮編寫一個通用排序實用程式也許是有意義的。
  
  對功能進行通用化有時會導致您發現:某個看起來似乎像單個實用程式的程式,實際上卻是配合起來使用的兩個實用程式。這很好。編寫兩個設計良好的實用程式可能要比編寫一個醜陋的或複雜的實用程式更容易。
  
  做好一件事情並不意味著 僅僅 做一件事情。它意味著處理一致但有用的問題空間。許多人都使用 grep。然而,它的大量效用在於執行相關任務的能力。grep 的各種選項完成許多小實用程式的工作,如果這些工作都由單獨的小實用程式來完成,最終會造成大量共享的、重複的程式碼。
  
  這條規則,以及做好一件事情的規則,都是一個根本原理的必然結果:無論何時都要儘可能避免程式碼重複。如果您編寫半打程式,其中每個都對行排序,您最終可能必須六次修復六個類似的 bug,而不是去使用一個得到更好維護的 sort 程式。
  
  這是編寫實用程式的一部分,即把大多數工作新增到完成該實用程式的過程中。您也許沒有時間在最初就完全通用化一個實用程式,但是當您一直使用該實用程式就會獲得相應的回報。
  
  有時,向某個程式新增相關功能是很有用的,即使這個功能並不是用來完成完全相同的任務。例如,當執行在終端裝置上時,對原始二進位制資料進行完美列印的程式可能更為有用,因為它使終端進入原始模式。這樣使得測試涉及鍵盤對映、新鍵盤等的問題變得容易多了。不確定為什麼當您按 delete 鍵時卻得到代字號(~)嗎? 這是弄清實際傳送了什麼內容的容易途徑。這並不是完全相同的任務,但它足夠類似,因而可能成為一個附加特性。
  
  清單 2 中的 errno 實用程式就是通用化的很好例子,因為它同時支援數字和符號名稱。
  
  健壯
  實用程式的穩定性是很重要的。容易崩潰或無法處理真實資料的實用程式不是有用的實用程式。實用程式應該能夠處理任意長度的行、巨型檔案,等等。實用程式無法處理超過其記憶體容量的資料集或許是可以容忍的,但是有些實用程式不是這樣;例如,sort 透過使用臨時檔案,一般能夠對比其記憶體容量大得多的資料集排序。
  
  應該儘量確保弄清楚您的實用程式可能要操作哪些資料。不要簡單地忽略無法處理的資料的可能性。應該檢查這種情況並診斷您的實用程式。錯誤訊息越明確,您對使用者就越有幫助。儘量給使用者提供足夠的資訊,以便讓他們知道發生了什麼情況以及如何解決。當處理資料檔案時,儘量準確識別出不良的資料。當嘗試解析數字時,不要簡單地放棄;應該告訴使用者您得到了什麼資料,而且如果可能的話,還應該告訴使用者該資料位於輸入流中的哪一行上。
  
  作為一個很好的例子,請考慮 dc 的兩種實現之間的區別。如果您執行 dc /home ,其中一種實現會顯示“Cannot use directory as input!”而另一種實現只是無聲地返回,沒有錯誤訊息,也沒有不尋常的退出程式碼。當您錯誤地鍵入一個 cd 命令時,您更希望當前路徑中有哪一種實現呢?類似地,如果您提供某個目錄中的資料流(或許是執行 dc < /home),前者會給出詳細的錯誤訊息。另一方面,當它在獲得無效資料的早期就選擇放棄可能是理想的。
  
  安全漏洞經常植根於在意料之外的資料面前表現得不夠健壯的程式中。務必記住,優秀的實用程式能夠設法在 shell 指令碼中作為根(root)使用者身份執行。諸如 find 這樣的程式中的緩衝區溢位可能會給大量的系統帶來風險。
  
  程式對意料之外的資料處理得越好,它就更可能適應變化的環境。通常,設法使程式更健壯會導致您更好地理解該程式的作用,從而更好地使之通用化。
  
  新穎
  要編寫的最糟糕的實用程式種類之一就是您已經有了的實用程式。我編寫過一個名為 count 的美妙的實用程式。它允許我執行幾乎任何計數任務。它是一個出色的實用程式,但是已經有一個名為 jot 的標準 BSD 實用程式做同樣的事情。同樣地,我的一個用於將資料轉換為列的靈活的程式重複了一個現有實用程式 rs 的功能,這個實用程式同樣可以在 BSD 系統上找到,只不過 rs 更靈活,設計得更好。請參閱下面的 參考資料 以瞭解關於 jot 和 rs 的更多資訊。
  
  如果您即將開始編寫一個實用程式,請花一點時間瀏覽一下各種系統,以確定那樣的實用程式是否已經存在。不要害怕在 BSD 上借用 Linux 實用程式,或在 Linux 上借用 BSD 實用程式;實用程式程式碼的樂趣之一在於,幾乎所有實用程式都具有很好的可移植性。
  
  不要忘了考察一下組合現有應用程式來形成一個實用程式的可能性。從理論上講,組合現有程式來形成的實用程式執行得不足夠快是可能的,但是編寫一個新的實用程式很少會比等待一個稍慢的管道更快。
  
  一個例子實用程式
  從某種意義上,這個程式是一個可執行檔案,因為對於作為過濾器來說,它決不會有任何用處。然而,它作為一個命令列實用程式卻工作得非常好。
  
  這個程式僅做一件事情。它以近乎完美的輸出格式輸出 /usr/include/sys/errno.h 中的 errno 行。例如:
  
  $ errno 22
  EINVAL [22]: Invalid argument
  
  清單 2. errno 查詢器
  
    
QUOTE:

來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/10617542/viewspace-947083/,如需轉載,請註明出處,否則將追究法律責任。

用C語言編寫Linux實用程式的藝術(轉)
請登入後發表評論 登入
全部評論

相關文章