昨天介紹了 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 工具普遍採用,估計不會有嚴重問題,可安心服用。

先看實作效果。

  1. 未指定 parse 或 chart 命令時顯示說明;parse -hchart --help 顯示各命令說明
  2. 指定分析 logs\u_ex202304*.log 存成 result.json 檔 iis-burnout-chart parse -o result.json logs\u_ex202304*.logiis-burnout-chart parse -output result.json logs\u_ex202304*.log
  3. 分析 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,亂給會出錯
  4. 自動抓最近日期 json 檔讀取,圖表檔名用現在時間 iis-burnout-chart chart
  5. 讀取 result.json,產生圖表存成 crash.html iis-burnout-chart chart -output crash.html result.json(另展示選項在前或選項在後均可)
  6. 讀取 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

Post a comment