在學習一門語言的過程中,有個實際可開發的小專案,有助於語言的熟悉與應用,也許寫個文字編輯器之類的桌面視窗程式,或者是開發個留言版、部落格。現在前端當道,許多人也會以手機App為目標。

在此同時,寫個命令列介面(Command-line interface,CLI)也是個不錯選擇,兼具實用性。

管理命令列選項

忘了是哪位大師曾經說過,每當他學習一門程式語言時,必定寫個文字編輯器作為練習,我也曾經在我的書中介紹過視窗介面,具體開發一個簡單的文字編輯器專案,作為讀者的練習對象。

然而,在Web應用程式與手機App相繼流行起來之後,視窗應用程式受到的關注逐漸式微,我也就順勢在後續改版書籍時,將視窗介面的介紹從書中移除了,從那之後,我就一直在思考著,還有什麼好的主題,可以讓讀者在語言的初學階段,就能開發並藉此熟悉語言的相關元素、思考風格與典範呢?

無論是視窗、Web應用程式,或者是手機App,都是個好的練習對象,只是都需要額外學習一些語言之外的API方案,那麼來開發一個命令列介面如何?

畢竟語言介紹時,多半是從在命令列顯示Hello, World的小程式開始,而後續的練習程式為了要有互動性,往往也是在命令列中進行一些輸入與輸出,開發命令列介面作為練習,實際應用場合也多,就算是視窗應用程式,為了方便與其他應用程式結合,多半也要提供這樣的執行模式。

想要管理命令列選項,我們首先要決定接受的選項格式。

常見的單破折號選項,像是tar -zxvf foo.tar.gz是POSIX風格(POSIX like options),破折號之後,跟隨著的單一字元,各代表一個選項(有時空一格加上對應值)。

另一種常見的雙折號長名稱選項,像是du --human-readable --max-depth=1為GNU風格(GNU like long options),擁有比較好的可讀性,在GNU的命令列介面標準(https://goo.gl/kbPFsS)中提到,基於使用者友善考量,建議對-A這類的短選項,也提供對應的--almost-all長選項名稱。

實際上,每種應用程式基於不同的考量,可能採取的選項風格也不相同,例如Java中常見的-Djava.awt.headless=true特性選項,同時具備短選項與值的gcc -O2 foo.c風格,或是單折號長名稱的ant -projecthelp風格等,不同選項風格牽涉到不同的剖析方式,協助(Help)訊息的輸出方式也不同,想要寫個通用的命令列介面,會比想像中複雜的多,不單只是要熟悉語言元素,還會有設計上的考量。

定義選項

如果現在決定要使用單折號長名稱,像是-help可以輸出協助訊息的話,以下的簡單程式,就算是第一次學習程式語言的初學者,應該都覺得不難:

if(args.length != 0 && "-help".equals(args[0])) {
out.println("顯示協助訊息");
}

接著試著進一步想像,若加入更多選項會如何?像是加入-version來顯示版本訊息?這麼一來,就會發現有更多的if判斷式,而用來檢查選項的條件式會開始出現重複,這時可將重複的程式碼抽取出來成為hasOption(String[] args, String option),之後就可以使用if(hasOption(args, "-version"))來避免重複,而且語意上比較清晰。

現在考慮使用者想查詢某個選項的作用,這就須為每個選項名稱加上描述文字,也許選項還有必要(Required)與選用(Optional)之別,既然如此,就定義一個Option類別來封裝選項的相關欄位吧!

使用者也許會使用-version來取得描述,程式面上,也就需要使用字串來取得對應的Option物件,這可以使用Map<String, Option>資料結構,只不過既然是用來管理一組Option物件,何不定義一個Options物件呢?如此一來,Options還可以提供一個addOption(String name, String desc)方法方便新增選項,讓開發者不用自行建立Option物件,例如:

Options options = new Options();
options.addOption("-help", "show help messages");
options.addOption("-version", "show version messages");

隨著選項可能性的增加,也許考慮使用流暢API風格,讓選項在建立時的程式碼,更能表達自身意圖,例如,使用OptionBuilder.withArgName("file").hasArg().withDescription("use given file for log" ).create( "-logfile" )來表示,將會建立一個具有引數(file)、描述的-logfile選項。

剖析選項

現在我們來考慮一下,像options.addOption("-help", "show help messages"),會有什麼問題?如果另一個開發者想要使用這個命令列介面,來管理程式,那麼,目前的方案等於強迫他使用單折號長名稱的風格。在建立選項時應該中立,不偏向哪一種風格,也就是使用options.addOption("help", "show help messages")的方式。

這麼一來,針對目前單折號長名稱的方案,可定義一個Parser,判斷使用者輸入的選項是否以單折號開頭,若是的話,傳回一個POSIXCommandLine,由它來負責將-help的選項,對應到Option("help", "show help messages")物件這類的工作。

接著,可以在Parser中定義雙折號的判斷,傳回GnuCommandLine之類的物件,你可以定義CommandLine介面,讓POSIXCommandLine、GnuCommandLine都實作此介面,以便在剖析出不同的選項風格時,傳回不同的實作,進一步地,還可以定義CommandLineParser介面,使用這個命令列介面管理程式的開發者,可以定義自己的CommandLineParser、CommandLine實作,建立自己的選項剖析過程。

處理命令列的格式化其實也是個麻煩,為了方便開發者輸出協助訊息,可以定義HelpFormatter,接受Options物件,提供各種printHelp方法,根據不同的選項風格,輸出不同的訊息內容。

不想重新打造輪子?

也許不見得是練習,而是應用程式確實需要一個命令列介面,試著自己寫的好處是,程式量身打造,不用額外相依於其他的程式庫套件,然而,如果想要有個立即可用的方案,主流程式語言都會有相應的開放原始碼方案。

實際上,先前描述的重構流程(可參考https://goo.gl/uc6RcY),是特意模仿Apache Commons CLI的風格,透過這樣的重構過程,也就能瞭解到,為何官方文件中一開始就提到處理命令列選項,會需要:選項的定義(Definition)、剖析(Parsing),以及詢問(Interrogation)這三個階段。

Commons CLI是個相對小型的程式庫,然而,原始碼規模相對來說也比較小,在試著模仿它的風格撰寫出自己的方案之後,可以看看原始碼做個比較;如果需要的命令列介面更為複雜,像是考量到預設值、大小寫處理、國際化等需求,或者程式面上,會想要使用標註(Annotation),讓程式碼更為簡明時,可以考慮使用JCommander程式庫。

當然,不同程式庫會有不同風格,其他語言也會有相對應的方案,在Python中,從3.2版本之後,甚至提供了標準模組argparse(此標準模組也回饋至Python 2.7),讓開發者便於處理命令列引數,官方網站也提供了Argparse Tutorial(https://goo.gl/cnSV6v)進行詳盡的說明,類似地,可以試著自己開發一個簡單的原型,再看看argparse原始碼作為比較,這將會是個有趣的體驗!

專欄作者

熱門新聞

Advertisement