閑話
在很久很久以前,電腦是命令行/終端/控制台的天下,那屏幕上的光標在行雲流水般的鍵盤敲擊下歡快地飛躍着,那一行行的字符輸出唰唰唰地滾動着……直到 Windows 95 的出現(那時候我還不知道蘋果電腦和它的操作系統),我的鼠標終於不再召灰,開始有了用武之地,然后就是 GUI 的天下……
然而世事就是這樣,錦綉繁華之后就開始返璞歸真,大魚大肉太多就向往點粗茶淡飯,開車開久了就懷念起自行車,GUI 充斥的 Windows 的世界里似乎也開始掛起一陣控制台的清風。畢竟,一旦你熟悉了各種命令和參數,敲鍵盤的速度還是勝過鼠標的,只是現在的人都太懶或者太忙,總是寧願犧牲效率而不願意去多記一點東西。
我個人還是很喜歡命令行的,尤其是遠程訪問一個系統的時候,一個簡單的 ssh 命令直接登錄到遠程 Linux,一個簡單的 scp 命令就可以互相傳輸文件,這種便利、快捷是 Windows 遠程桌面所無法比擬的。GUI 也許是 Windows 的設計哲學, 做什么事情都要靠 GUI。 沒錯,這大大降低了各種操作門檻,但是作為一個程序員,GUI 工具並非總是最佳選擇,但除了 GUI 工具,替代選擇並不多——直到 .NET Core 的出現。
專業的控制台程序
首先我們要有個標准,怎樣才算“專業的控制台程序”?
平常無論是寫着玩還是工作需要,我都做過一些控制台程序,在啟動參數的傳入、解析和執行上都比較隨意,類似 MyProgram abc 123 這樣,MyProgram 是程序名,abc、123 是參數值,內部直接用 args[0]、args[1]取得參數值並使用。僅此而已。時間長了,自己都搞不懂每個參數什么意思,參數有哪些有效值,都得查源代碼才知道,每個參數的順序也很重要,顛倒不得。而內部實現上,至少是 Main 方法里是典型的面條式代碼。由此可見,我的這些控制台程序,無論是外在,還是內在,都業余得很。
那一個專業的控制台程序應該是什么樣的呢?
完善的幫助信息
當你面對一個陌生的命令行程序,或者重新面對你自己2個月之前寫的命令行程序,你心里第一反應會是什么呢?讓我猜猜,你的第一反應一定是“這貨到底怎么用?”(不要告訴我我猜錯了,我不相信💢),下一個想法就是這個程序能告訴我用法就好了。一個專業的控制台程序必然會滿足你的這個需要——它可以提供完善的幫助信息。比如 git:

有了這些幫助信息,我們自然信心倍增,心里有譜多了。
我希望我寫的控制台程序也能做到這點!
符合“國際慣例”的調用方式
如果你留意一下 Linux 平台下的一眾控制台程序,你會發現他們的參數組織和調用方式十分類似,這種約定俗成的“國際慣例”十分有助於降低熟悉各個控制台程序的學習成本。
我們還是以 git 為例,從上面的截圖可以看出,它有非常多的參數可用。雖然都是參數,但根據作用不同,可以分為 command, argument, option 三類。我不是控制台程序達人,對這3類參數的區別與聯系還在深入理解、學習中。目前的理解是(以 git 為例):
-
command
一個復雜的控制台程序可以提供多個子命令,而 command 就代表這些子命令
比如
clone,init -
argument
是一個 command 需要的參數。
比如執行
clone的時候需要指定一個 repository 的地址,這個地址就是一個argument。 -
option
調整 command 的行為。
比如對於
clone可以增加--verbose參數使其輸出更詳盡的信息。通常以兩個短橫線開頭后跟參數名,比如
--verbose,對於常用的 option 還會有簡寫形式,就是一個短橫線后跟簡寫形式的參數名,比如--verbose支持簡寫形式-v,在幫助里通常以--verbose, -v或者--verbose|-v的形式說明。option 可以有值也可以沒有值,有值的時候,其賦值方式不一而足,常見的有用空格的
--branch dev,用等號的(注意等號兩邊沒有空格)--branch=dev,用冒號的(冒號兩邊沒有空格)--branch:dev
對於相對簡單的控制台程序,可能只有 argument 和 option 而並不包括 command。
看看那些有名的控制台程序,基本上也都遵循這個套路。在這方面,我不想做個怪胎,所以,我希望我寫的控制台程序也能遵照這些“國際慣例”!
易於維護的內部實現
在控制台程序的內部實現上,以前的做法非常簡單粗暴,用一堆 if 或者 switch 配合各種 && 和 || 成功地做到了一開始只有“上帝”和我明白,1個月后只有“上帝”明白的效果。隨着程序參數增多、邏輯越來越復雜,這么搞下去,“上帝”依然可以很瀟灑,我會被搞死的。為了不讓我變成禿頭,為了我可以有更多的時間玩游戲,控制台程序的內部實現必須井井有條、易於維護!
問題來了
了解了一個專業的控制台程序應具備的素質,那么接下來有一個問題縈繞在我心頭久久不肯散去……

作為一個小白,要實現一個有詳細說明信息、調用方式符合“國際慣例”、還能優雅地處理各種參數的控制台程序何其困難?而如此套路化的東西難道沒有一套成型的東西供參考嗎?

CommandLineApplication
天無絕人之路,一次偶然的邂逅,遇到了它—— CommandLineApplication。如果你用 .NET Core 的話,它可以在你構建專業控制台程序的路上助你一臂之力。
它全名是 Microsoft.Extensions.CommandLineUtils.CommandLineApplication,家住 GitHub 省 aspnet 市 Common 區 src 路 Microsoft.Extensions.CommandLineUtils 大院 CommandLine 室.如果你路盲,這里有個傳送門。
實踐是檢驗真理的唯一標准 - MathForKids 程序
讓我們從頭開始,利用 dotnet cli 和 Visual Studio Code 親自體驗一下它到底有多強大。我們將創建一個 MathForKids 程序,它可以根據參數輸出一些加減乘除的算式,讓孩子算算結果。
MathForKids 程序的功能
-
它只輸出算式,所以我們不要搞得太復雜,不需要什么子命令 command。
想了解帶 command 的用法,可以參考本文最后附上的 CommandLineApplication 的官方測試代碼 😛
-
它在執行的時候需要指定輸出的算式是加、減、乘、除還是這4種運算符的組合,因此我們可以設置一個 argument:
operator.這個 argument 可以允許同時設置多個值。
-
它可以設置生成的數字的最大值和最小值,因為它們只是調整輸出的算式種數字的大小,因此我將其歸為 options:
minValue,maxValueminValue的默認值是0maxValue的默認值是100 -
它可以設置生成的算式個數,這也只是調整輸出結果,因此我也將其歸為 option:
countcount的默認值是 10
MathForKids 程序的用法
在我們看代碼之前,先看看這個程序用起來應該是什么樣的。
注意:在本文發布之時,.NET Core 處於 RC 2 階段,還不支持編譯為本地可執行文件。所以目前必須使用 dotnet MathForKids.dll 來運行。圖中紅色下划線表示輸入的命令
首先,它可以提供幫助信息:

從幫助信息中我們可以看到它支持的所有參數,並且支持參數的全寫和簡寫,比如我們可以寫 --minValue 也可以簡寫為 -min。
當我們調用它只生成加法和乘法,其它選項默認時:

注意,我們一次傳入了多個參數: 加 乘。
當我們設置一些 option 來改變輸出結果時:

注意,這里演示了使用全寫和簡寫添加 option 以及以多種方式賦值(使用冒號、等號和空格)。
以上使用方法看起來是不是有點“專業”的味道了?
Show me the code
為了使用 CommandLineApplication 類,我們需要添加對 Microsoft.Extensions.CommandLineUtils 的引用:
{
"version": "1.0.0-*",
"buildOptions": {
"emitEntryPoint": true
},
"dependencies": {
"Microsoft.NETCore.App": {
"type": "platform",
"version": "1.0.0-rc2-3002702"
},
"Microsoft.Extensions.CommandLineUtils": "1.0.0-rc2-final" //<--- add this dependency
},
"frameworks": {
"netcoreapp1.0": {
"imports": "dnxcore50"
}
}
}
這是 MathForKids 程序的主體部分,說明都在注釋里:
using System;
using Microsoft.Extensions.CommandLineUtils;
namespace ConsoleApplication
{
public class Program
{
public static void Main(string[] args)
{
CommandLineApplication app = new CommandLineApplication();
app.HelpOption("--help|-h|-?"); // 使其支持顯示幫助信息
app.VersionOption("--version|-v", "1.0.0"); // 使其支持顯示版本信息。為了簡化起見,直接返回靜態的 1.0.0
// 添加 argument,這里我們允許傳入這個 argument 的多個值。
CommandArgument argOperator = app.Argument("operator", "算式類型,有效值:加、減、乘、除,可以設置多個類型", multipleValues: true);
// 添加多個 options,注意設置全寫和簡寫的方式,很簡單。這應該是基於約定的解析處理方式。
CommandOption optMin = app.Option("--minValue -min <value>", "最小值,默認為0", CommandOptionType.SingleValue);
CommandOption optMax = app.Option("--maxValue -max <value>", "最大值,默認為100", CommandOptionType.SingleValue);
CommandOption optCount = app.Option("--count -c <value>", "生成的算式數量,默認為10", CommandOptionType.SingleValue);
// 傳入一個委托方法,當下面的 Execute 執行后會執行我們的委托方法,完成我們需要處理的工作。 委托方法需要返回一個 int,反映執行結果,一如經典的控制台程序需要的那樣。
app.OnExecute(() =>
{
return OnAppExecute(argOperator, optMin, optMax, optCount);
});
// 開始執行,把控制台傳入的參數直接傳遞給 CommandLineApplication。
app.Execute(args);
}
private static int OnAppExecute(CommandArgument argOperator, CommandOption optMin, CommandOption optMax, CommandOption optCount)
{
// 此處省略 100 行以以下示例取代
List<string> operators = argOperator.Values;
string max;
if(optMax.HasValue())
max = optMax.Value();
return 0;
}
}
}
CommandArgument 和 CommandOption 的使用非常簡單,有 HasValue 和 Value 等方法可以判斷和取值。
所有代碼放在我的 GitHub 里。
官方的測試類也是一個很好的參考資源。
