向專業 CLI 程式看齊 - .NET 程式支援 POSIX 參數語法
0 |
昨天介紹了 POSIX 參數慣例,它是主流 CLI 工具一致遵守的參數語法規則,以 git 或 dotnet 為例,指令工具要能指定動作命令,選項名稱支援 --long-option-name 或單一字元 -o 兩種表示法,選項可接參數值 (--verbosity n)或可加可不加,參數選項可自由調換順序 OK,使用上十分彈性方便。
git merge --no-ff -m "commit message" fix/crash-issue
dotnet publish -c Release -r win-x64 --no-self-contained -p:PublishSingleFile=true
當大部分 CLI 都採一致又方便的參數語法,而使用者也已習慣這種做法,靠直覺即可上手,無形中便成為 CLI 程式的基本要求。若我們自己寫的 .NET Console 應用程式還在自己訂規則,寫死 args[0]、args[1] 抓參數,死板板規定第一個參數是動作、第二個是路徑,第三個是顯示格式,還沒有內建使用說明,整個感覺就是業餘作品啊~
這篇將演練一次,如何使用既有程式庫,讓我們自製的 .NET Console 程式也能像專業 CLI 支援 --option-name、-o 長短版選項名稱、允許選項參數可加可不加、選項及參數不限順序自由排序。
先模擬規格如下,假設我們要寫一支 IIS Log 分析工具,以下是幾種(但不限於)選項及參數組合:
# 分析目前目錄下 u_ex*.log 並依 Log 日期存成 yyyyMMdd.json 檔
iis-burnout-chart parse
# 分析 logs\u_ex202304*.log 存成 result.json 檔
iis-burnout-chart parse -o result.json logs\u_ex202304*.log
iis-burnout-chart parse -output result.json logs\u_ex202304*.log
# 分析 u_ex20230416.log u_ex20230417.log,限定 POST 方法,URL 為 /api/login
iis-burnout-chart parse -m POST -urlPath /api/login u_ex20230416.log u_ex20230417.log
# 讀取目前目錄下最近寫入的一筆 yyyyMMdd.json 檔,產生圖表存成 yyyyMMddHHmmss.html
iis-burnout-chart chart
# 讀取 result.json,產生圖表存成 crash.html
iis-burnout-chart chart -output crash.html result.json
# 讀取 data1.json,分析 2023-04-17 10:00:00 ~ 11:00:00 間的資料,將圖表存為 peak.html
iis-burnout-chart chart -o peak.html -s "2023-04-17 10:00:00" --endTime="2023-04-17 11:00:00" -- data1.json data2.json
感覺有點小複雜,但 POSIX 格式既然是通用標準,自然不乏現成程式庫可用,沒必要自己造輪子。之前介紹過 System.CommandLine,但當時的範例很簡單,沒有包含動作命令(本例中分 parse 及 chart 兩種動作),這回補上更完整的範例。
時隔一年多,System.CommandLine 至今仍是 beta 版沒正式發行,但考量它是微軟出品,且被 .NET CLI (dotnet) 及附屬 CLI 工具普遍採用,估計不會有嚴重問題,可安心服用。
先看實作效果。
- 未指定 parse 或 chart 命令時顯示說明;
parse -h
、chart --help
顯示各命令說明
- 指定分析 logs\u_ex202304*.log 存成 result.json 檔
iis-burnout-chart parse -o result.json logs\u_ex202304*.log
、iis-burnout-chart parse -output result.json logs\u_ex202304*.log
- 分析 u_ex20230416.log u_ex20230417.log,限定 POST 方法,URL 為 /api/login
iis-burnout-chart parse -m POST -urlPath /api/login u_ex20230416.log u_ex20230417.log
,-m 限定 *、GET 或 POST,亂給會出錯
- 自動抓最近日期 json 檔讀取,圖表檔名用現在時間
iis-burnout-chart chart
- 讀取 result.json,產生圖表存成 crash.html
iis-burnout-chart chart -output crash.html result.json
(另展示選項在前或選項在後均可)
- 讀取 data.json,分析 2023-04-17 10:00:00 ~ 11:00:00 間的資料,將圖表存為 peak.html
iis-burnout-chart chart -o peak.html -s "2023-04-17 10:00:00" --endTime="2023-04-17 11:00:00" -- data.json
(另展示 -s "..."、-s="..."、-s"..." 三種寫法均可)
最後附上範例程式:
using System.CommandLine;
using System.Globalization;
using System.Text.RegularExpressions;
class Program
{
static async Task<int> Main(string[] args)
{
// 定義 Root Command
var rootCommand = new RootCommand("IIS Burnout Chart ver 0.9b");
// 未指定 Command 時,顯示 --help 說明
rootCommand.SetHandler(() => rootCommand.InvokeAsync("--help"));
// 定義 Parse Command
var parseCommand = new Command("parse", "分析 Log 檔轉為 JSON 資料檔");
// 定義參數 (不加 -- 或 - 前綴)
var logPathArgument = new Argument<FileInfo[]?>(
"logPaths",
// 預設值抓當前目錄下所有 u_ex*.log 檔案
() => Directory.GetFiles(Directory.GetCurrentDirectory(), "u_ex*.log")
.Select(f => new FileInfo(f)).OrderByDescending(f => f.Name).ToArray(),
description: "待解析 Log 檔,支援多筆,預設為所在目錄下所有 u_ex*.log。"
);
// 定義選項,同時提供長短選項名稱
var methodOptions = new Option<string>(
new[] { "--method", "-m" }, () => "*",
"篩選 HTTP 方法")
.FromAmong("*", "GET", "POST"); // 限定可輸入的值
var pathOptions = new Option<string>(
new[] { "--urlPath", "-p" }, () => ".+",
"篩選 URL 路徑 (使用正規表示式)");
var jsonFilePath = new Option<FileInfo?>(new[] { "--output", "-o" },
"輸出結果檔案名稱,預設為 Log 日期 yyyyMMdd.json");
// 為 Command 加入參數與選項
parseCommand.AddArgument(logPathArgument);
parseCommand.AddOption(methodOptions);
parseCommand.AddOption(pathOptions);
parseCommand.AddOption(jsonFilePath);
// 設定 Command 執行函式,參數與選項為函式之輸入參數
parseCommand.SetHandler((files, method, path, jsonFile) =>
{
if (files == null || files.Length == 0)
{
Console.WriteLine("沒有資料來源");
return;
}
Console.WriteLine($"解析檔名:{string.Join(",", files!.Select(f => f.Name))}");
Console.WriteLine($"過濾條件:Method={method} Path={path}");
Console.WriteLine($"輸出檔名:{jsonFile?.FullName ?? "Log 日期 yyyyMMdd.json"}");
},
// 依序帶入參數與選項,要對映函式輸入參數
logPathArgument, methodOptions, pathOptions, jsonFilePath);
rootCommand.AddCommand(parseCommand); // 將 Command 加入 Root Command
// 定義 Chart Command
var chartCommand = new Command("chart", "分析資料繪製效能數圖表");
// 定義參數及選項
var jsonPathArgument = new Argument<FileInfo?>(
"jsonPath",
() => Directory.GetFiles(Directory.GetCurrentDirectory(), "*.json")
.Where(o => Regex.IsMatch(Path.GetFileName(o), @"\d{8}\.json$") &&
DateTime.TryParseExact(Path.GetFileNameWithoutExtension(o).Substring(0, 8), "yyyyMMdd",
null, DateTimeStyles.None, out _))
.Select(f => new FileInfo(f)).OrderByDescending(f => f.Name).FirstOrDefault(),
description: "資料來源 JSON,預設讀取所在目錄日期最新的 yyyyMMdd.json"
);
var startTimeOption = new Option<DateTime?>(new[] { "--startTime", "-s" }, "分析時段之開始時間");
var endTimeOption = new Option<DateTime?>(new[] { "--endTime", "-e" }, "分析時段之結束時間");
var htmlFileOption = new Option<FileInfo?>(new[] { "--output", "-o" },
() => new FileInfo(DateTime.Now.ToString("yyyyMMddHHmmss") + ".html"),
"輸出圖表 HTML 檔名");
// 加入參數與選項
chartCommand.AddArgument(jsonPathArgument);
chartCommand.AddOption(startTimeOption);
chartCommand.AddOption(endTimeOption);
chartCommand.AddOption(htmlFileOption);
// 設定 Command 執行函式
chartCommand.SetHandler((jsonPath, startTime, endTime, htmlFile) =>
{
if (jsonPath == null)
{
Console.WriteLine("沒有資料來源");
return;
}
Console.WriteLine($"解析資料檔:{jsonPath?.FullName ?? "無"}");
Console.WriteLine($"分析時段:{startTime?.ToString("yyyy/MM/dd HH:mm:ss") ?? "無"} ~ {endTime?.ToString("yyyy/MM/dd HH:mm:ss") ?? "無"}");
Console.WriteLine($"輸出檔名:{htmlFile?.FullName}");
}, jsonPathArgument, startTimeOption, endTimeOption, htmlFileOption);
rootCommand.AddCommand(chartCommand); // 將 Command 加入 Root Command
// 執行 Root Command
return await rootCommand.InvokeAsync(args);
}
}
演練完畢,未來要用 .NET 寫 CLI 工具,依此要領就能支援專業等級的 POSIX 輸入參數語法囉。
Parsing arguments and options in POSIX in .NET console applications by System.CommandLine library.
Comments
Be the first to post a comment