.NET Core 程式寫久了,用命令列參數控制程式執行已是日常,像發佈 .NET 6 程式時便少不了 --no-self-contained、-c Release、-r win-x64 等參數,而啟動 ASP.NET Core 時,使用 --urls 指定 HTTP Port 更是必備技巧。

.NET 程式接收 .exe 啟動參數不難,由 void Main(string[] args) 取出 args 進行解析即可,但要做到參數可有可無,不限順序的彈性得花點功夫,一般不需要自己造輪子,多會借助 System.CommandLineCommand Line Parser Library... 等程式庫。

而在 .NET Core 程式裡,命令列參數還有個重要用途 - 作為 IConfiguration 的設定來源之一。ASP.NET Core 的 IConfiguration 支援多種來源,預設可透過 appsettings.json、appsettings..json、環境變數、命令列參數等方式設定,並有「重複設定以後者為準」的順序概念,可以做到在 appsettings.json 給預設值,實際部署時以環境變數為準,遇特別狀況可手動執行以命令列參數覆寫設定,應用起來非常有彈性。

而透過命令列參數提供 IConfiguration 設定值的做法,也可應用在一般的 Console 程式,例如:

using System.Text.Json;
using Microsoft.Extensions.Configuration;

//dotnet run -- param1=value1 --param2 value2 /param3 value3
//三種格式可同並存,但不建議混用,應統一用 =、-- 或 /

Console.WriteLine($"args.Length = {args.Length}");
Console.WriteLine($"args = {JsonSerializer.Serialize(args)}");

IConfiguration config = new ConfigurationBuilder()
    .AddCommandLine(args)
    .Build();

foreach (var s in config.AsEnumerable()) {
    Console.WriteLine($"* {s.Key} = {s.Value}");
}
Console.WriteLine(config.GetValue<string>("param1", "N/A"));
Console.WriteLine(config.GetValue<string>("param4", "N/A"));

我自己在寫 ASP.NET Core 時,有時會設計成靠命令列參數執行一次性作業,例如:在 EF Core 資料庫寫入範例資料:

var sqliteDbPath = Path.Combine(builder.Environment.ContentRootPath, "static-files.sqlite");
builder.Services.AddDbContext<StaticFileDbContext>(options =>
{
    options.UseSqlite($"data source={sqliteDbPath}");
});
builder.Services.AddScoped<StaticFileDbRepository>();
var app = builder.Build();

// init db
if (!File.Exists(sqliteDbPath))
{
    using (var scope = app.Services.CreateScope())
    {
        scope.ServiceProvider.GetRequiredService<StaticFileDbContext>()
              .Database.Migrate();
    }
}

// insert the demo data on-demand
if (app.Configuration.GetValue<string>("insertDemoData", "false") == "true")
{
    using (var scope = app.Services.CreateScope())
    {
        var rsp = scope.ServiceProvider.GetRequiredService<StaticFileDbRepository>();
        var userId = "jeffrey";
        var clientIp = "::1";
        rsp.UpdateFile("/index.html", Encoding.UTF8.GetBytes("<html><body><link href=css/site.css rel=stylesheet /> Web in JSON</body></html>"), userId, clientIp);
        rsp.UpdateFile("/css/site.css", Encoding.UTF8.GetBytes("body { font-size: 9pt }"), userId, clientIp);
    }

當需要展示資料測試或展示時,加上參數執行 DemoWeb.exe --insertDemoData true 即可在資料庫新增展示資料。

不過,透過 IConfiguration 讀取命令列參數格式比較死板,只接受名稱對映值的寫法(--paramName paramValue),沒法提供短版名稱(如:--help 可寫成 -h)、也不支援開關式參數(如:--no-self-contained)。

若有 Console 程式有較複雜或彈性的參數樣式需求,需回歸 System.CommandLine、Command Line Parser 等專門程式庫才有完整支援。這裡用微軟的 System.CommandLine 來練習,由於它仍在 Beta 階段,參照時需加上 --prerelease 參數 dotnet add package System.CommandLine --prerelease

using System.CommandLine;

var cmd = new RootCommand
{
    Description = "參數測試程式",
    //命令及參數解析失敗時視為錯誤
    TreatUnmatchedTokensAsErrors = true
};
var inputOption = new Option<FileInfo>(
    aliases: new[] { "--input", "-i" },
    description: "檔案路徑") {
        IsRequired = true
    };
cmd.AddOption(inputOption);
var colorOption = new Option<bool>(
    aliases: new[] { "--color", "-c" },
    description: "彩色顯示"
);
cmd.AddOption(colorOption);
cmd.SetHandler((FileInfo input, bool color) => {
    Console.WriteLine(input.FullName);
    Console.WriteLine(color);
}, inputOption, colorOption);

return cmd.Invoke(args);

System.CommandLine 支援的命令、參數格式變化很多,細節可參考官方文件

【參考資料】

Tips of using command line argument for logic control in .NET 6.


Comments

# by Huang

命令列參數執控制程式 是否是要輸入參數"值"

# by Jeffrey

to Huang, 謝,已修正。

Post a comment