Spectre.Console-實現自己的CLI

2023-05-31 09:00:37

引言

最近發現自己喜歡用的 Todo 軟體總是差點意思,畢竟每個人的習慣和工作流不太一樣,我就想著自己寫一個小的Todo 專案,核心的功能是自動記錄 Todo 執行過程中消耗的時間(尤其面向程式設計師),按照自己的想法實現一套 GTD 工作流。

不想寫 WinformWPF 也寫膩了,就想著學學 MAUIAvaloniaUno Platformblazor 之類的。由於前端技術選型糾結,遲遲動不了手,想想還是暫時先不弄了。但為了測試,沒有個介面總是不太行,先搞一個 CLI 吧。

更新:由於想讓程式持續執行,所以後面還是替換了 CLI 。

Spectre. Console

Spectre.Console(spectreconsole.net) 是一個美化 Console 輸出的類庫,通過它可以實現豐富多樣的 Console 輸出。核心的特性有這些:

  • 格式化輸出文字(支援斜體等)
  • 支援對文字著色
  • 渲染複雜的元件(表格、結構樹、ASCII 圖片)
  • 顯示進度條與狀態
  • 強型別輸入驗證
  • 對 exception 輸出著色

除此以外,它還提供了一個 Spectre.Console.Cli 類庫,可以幫助我們實現類似 dotnetgit 之類的 CLI(Command Line Interface)。

基本用法

這裡使用官方的範例:

var app = new CommandApp<FileSizeCommand>();
return app.Run(args);

internal sealed class FileSizeCommand : Command<FileSizeCommand.Settings>
{
    public sealed class Settings : CommandSettings
    {
        [Description("Path to search. Defaults to current directory.")]
        [CommandArgument(0, "[searchPath]")]
        public string? SearchPath { get; init; }

        [CommandOption("-p|--pattern")]
        public string? SearchPattern { get; init; }

        [CommandOption("--hidden")]
        [DefaultValue(true)]
        public bool IncludeHidden { get; init; }
    }

    public override int Execute([NotNull] CommandContext context, [NotNull] Settings settings)
    {
        var searchOptions = new EnumerationOptions
        {
            AttributesToSkip = settings.IncludeHidden
                ? FileAttributes.Hidden | FileAttributes.System
                : FileAttributes.System
        };

        var searchPattern = settings.SearchPattern ?? "*.*";
        var searchPath = settings.SearchPath ?? Directory.GetCurrentDirectory();
        var files = new DirectoryInfo(searchPath)
            .GetFiles(searchPattern, searchOptions);

        var totalFileSize = files
            .Sum(fileInfo => fileInfo.Length);

        AnsiConsole.MarkupLine($"Total file size for [green]{searchPattern}[/] files in [green]{searchPath}[/]: [blue]{totalFileSize:N0}[/] bytes");

        return 0;
    }
}

結構非常簡單,標有 [CommandOption("xxx")] 會自動將引數歸類,通過下列命令進行呼叫。

app.exe
app.exe c:\windows
app.exe c:\windows --pattern *.dll
app.exe c:\windows --hidden --pattern *.dll

多命令

上面這個範例只支援一個預設的命令,但是一般的 CLI 都有很多支援的命令,需要調整一下實現:

var app = new CommandApp();
app.Configure(config =>
{
    config.AddCommand<AddCommand>("add");
    config.AddCommand<CommitCommand>("commit");
    config.AddCommand<RebaseCommand>("rebase");
});

層級命令

更復雜一點的,比如 dotnet add packagedotnet add reference 這種,add 後面還有 package 這個子命令,上面的方法還得繼續拓展,首先定義 add 基礎類別和 package 與 reference 繼承類。

public class AddSettings : CommandSettings
{
    [CommandArgument(0, "[PROJECT]")]
    public string Project { get; set; }
}

public class AddPackageSettings : AddSettings
{
    [CommandArgument(0, "<PACKAGE_NAME>")]
    public string PackageName { get; set; }

    [CommandOption("-v|--version <VERSION>")]
    public string Version { get; set; }
}

public class AddReferenceSettings : AddSettings
{
    [CommandArgument(0, "<PROJECT_REFERENCE>")]
    public string ProjectReference { get; set; }
}

然後對不同的命令,指定不同處理常式。

public class AddPackageCommand : Command<AddPackageSettings>
{
    public override int Execute(CommandContext context, AddPackageSettings settings)
    {
        // Omitted
        return 0;
    }
}

public class AddReferenceCommand : Command<AddReferenceSettings>
{
    public override int Execute(CommandContext context, AddReferenceSettings settings)
    {
        // Omitted
        return 0;
    }
}

最後使用 AddBranch 進行組合:

using Spectre.Console.Cli;

namespace MyApp
{
    public static class Program
    {
        public static int Main(string[] args)
        {
            var app = new CommandApp();

            app.Configure(config =>
            {
                config.AddBranch<AddSettings>("add", add =>
                {
                    add.AddCommand<AddPackageCommand>("package");
                    add.AddCommand<AddReferenceCommand>("reference");
                });
            });

            return app.Run(args);
        }
    }
}

參考