2

向專業 CLI 程式看齊 - .NET 程式支援 POSIX 參數語法

 1 year ago
source link: https://blog.darkthread.net/blog/dotnet-console-cli-posix-args/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

.NET 程式支援 POSIX 參數語法-黑暗執行緒

昨天介紹了 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 顯示各命令說明
    Fig1_638173419988423745.png
  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
    Fig2_638173419990290310.png
  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,亂給會出錯
    Fig7_638173432337846428.png
  4. 自動抓最近日期 json 檔讀取,圖表檔名用現在時間 iis-burnout-chart chart
    Fig4_638173419994138430.png
  5. 讀取 result.json,產生圖表存成 crash.html iis-burnout-chart chart -output crash.html result.json(另展示選項在前或選項在後均可)
    Fig5_638173419996049294.png
  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"..." 三種寫法均可)
    Fig6_638173419998023827.png

最後附上範例程式:

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 輸入參數語法囉。


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK