dotnet-spectre-console について
.NET 開発者向けの Spectre.Console スキルです。リッチなターミナル出力と Spectre.Console.Cli による構造化されたコマンドラインアプリの構築を扱い、対話表示やテスト可能な出力も解説します。
主な機能
- 01スタイル付きテキスト、テーブル、ツリー、パネルの表示
- 02進捗、ステータス、ライブ表示
- 03テキスト、確認、選択プロンプト
- 04Spectre.Console.Cli によるコマンド階層の構成
- 05型付き設定、検証、自動ヘルプのサポート
- 06IAnsiConsole を使った出力テスト
活用シーン
- 01コマンドラインプログラムでのテーブルと進捗表示
- 02階層型コマンドを持つ CLI の構築
- 03入力の収集と設定の検証
- 04テストでのコンソール出力の記録と確認
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ wshaddix/ dotnet-skills --skill dotnet-spectre-consoleインストール後、Claude Code で/dotnet-spectre-consoleを入力して読み込まれたか確認します。
詳細
dotnet-spectre-console
Spectre.Console は、リッチなコンソール出力(テーブル、ツリー、プログレスバー、プロンプト、マークアップ、ライブ表示)の構築に使用します。Spectre.Console.Cli は、構造化されたコマンドラインアプリケーションの解析に使用します。Windows、macOS、Linux のターミナルでクロスプラットフォームに動作します。
バージョンの前提: .NET 8.0 以降をベースラインとします。Spectre.Console 0.54.0(最新の安定版)。Spectre.Console.Cli 0.53.1(最新の安定版)。両パッケージは net8.0 以降および netstandard2.0 をターゲットとしています。
対象範囲: このスキルは、リッチなコンソール出力と Spectre.Console.Cli によるコマンド解析を担当します。フル機能の TUI アプリケーション(ウィンドウ、メニュー、ダイアログ、ビュー)は [skill:dotnet-terminal-gui] が担当します。System.CommandLine による解析は [skill:dotnet-system-commandline] が担当します。CLI アプリケーションのアーキテクチャと配布は [skill:dotnet-cli-architecture] および [skill:dotnet-cli-distribution] が担当します。
相互参照:フル機能の TUI の代替手段については [skill:dotnet-terminal-gui]、System.CommandLine の対象範囲については [skill:dotnet-system-commandline]、CLI の構造については [skill:dotnet-cli-architecture]、非同期パターンについては [skill:dotnet-csharp-async-patterns]、Spectre.Console.Cli での DI については [skill:dotnet-csharp-dependency-injection]、TUI のアクセシビリティ上の制約とスクリーンリーダーの考慮事項については [skill:dotnet-accessibility] を参照してください。
パッケージ参照
<ItemGroup>
<PackageReference Include="Spectre.Console" Version="0.54.0" />
<PackageReference Include="Spectre.Console.Cli" Version="0.53.1" />
</ItemGroup>
Spectre.Console.Cli は Spectre.Console に依存しています。CLI フレームワークが必要な場合にのみ、両方をインストールしてください。リッチな出力のみが必要な場合は、Spectre.Console だけで十分です。
マークアップとスタイル設定
Spectre.Console は、スタイル付きコンソール出力に BBCode に着想を得たマークアップ構文を使用します。
基本的なマークアップ
using Spectre.Console;
// Styled text with markup tags
AnsiConsole.MarkupLine("[bold red]Error:[/] File not found.");
AnsiConsole.MarkupLine("[green]Success![/] Build completed in [blue]2.3s[/].");
AnsiConsole.MarkupLine("[underline]https://example.com[/]");
AnsiConsole.MarkupLine("[dim italic]This is subtle text[/]");
// Nested styles
AnsiConsole.MarkupLine("[bold [red on white]Warning:[/] check config[/]");
// Escape brackets with double brackets
AnsiConsole.MarkupLine("Use [[bold]] for bold text.");
Figlet テキスト
AnsiConsole.Write(
new FigletText("Hello!")
.Color(Color.Green)
.Centered());
ルール(水平線)
// Simple rule
AnsiConsole.Write(new Rule());
// Titled rule
AnsiConsole.Write(new Rule("[yellow]Section Title[/]"));
// Aligned rule
AnsiConsole.Write(new Rule("[blue]Left Aligned[/]").LeftJustified());
テーブル
var table = new Table();
// Add columns
table.AddColumn("Name");
table.AddColumn(new TableColumn("Age").Centered());
table.AddColumn(new TableColumn("City").RightAligned());
// Add rows
table.AddRow("Alice", "30", "Seattle");
table.AddRow("[green]Bob[/]", "25", "Portland");
table.AddRow("Charlie", "35", "Vancouver");
// Styling
table.Border(TableBorder.Rounded);
table.BorderColor(Color.Grey);
table.Title("[underline]Team Members[/]");
table.Caption("[dim]Updated daily[/]");
// Column configuration
table.Columns[0].PadLeft(2);
table.Columns[0].NoWrap();
AnsiConsole.Write(table);
入れ子のテーブル
var innerTable = new Table()
.AddColumn("Detail")
.AddColumn("Value")
.AddRow("Role", "Developer")
.AddRow("Level", "Senior");
var outerTable = new Table()
.AddColumn("Name")
.AddColumn("Info")
.AddRow("Alice", innerTable);
AnsiConsole.Write(outerTable);
ツリー
var tree = new Tree("Solution");
// Add nodes
var srcNode = tree.AddNode("[yellow]src[/]");
var apiNode = srcNode.AddNode("Api");
apiNode.AddNode("Controllers/");
apiNode.AddNode("Program.cs");
var libNode = srcNode.AddNode("Library");
libNode.AddNode("Services/");
var testNode = tree.AddNode("[blue]tests[/]");
testNode.AddNode("Api.Tests/");
// Styling
tree.Style = Style.Parse("dim");
AnsiConsole.Write(tree);
パネル
var panel = new Panel("This is [green]important[/] content.")
.Header("[bold]Notice[/]")
.Border(BoxBorder.Rounded)
.BorderColor(Color.Blue)
.Padding(2, 1) // horizontal, vertical
.Expand(); // fill available width
AnsiConsole.Write(panel);
列を使ったレンダラブルの合成
AnsiConsole.Write(new Columns(
new Panel("Left panel").Expand(),
new Panel("Right panel").Expand()));
プログレス表示
プログレスバー
await AnsiConsole.Progress()
.AutoClear(false) // keep completed tasks visible
.HideCompleted(false)
.Columns(
new TaskDescriptionColumn(),
new ProgressBarColumn(),
new PercentageColumn(),
new RemainingTimeColumn(),
new SpinnerColumn())
.StartAsync(async ctx =>
{
var downloadTask = ctx.AddTask("[green]Downloading[/]", maxValue: 100);
var extractTask = ctx.AddTask("[blue]Extracting[/]", maxValue: 100);
while (!ctx.IsFinished)
{
await Task.Delay(50);
downloadTask.Increment(1.5);
if (downloadTask.Value > 50)
{
extractTask.Increment(0.8);
}
}
});
ステータススピナー
await AnsiConsole.Status()
.Spinner(Spinner.Known.Dots)
.SpinnerStyle(Style.Parse("green bold"))
.StartAsync("Processing...", async ctx =>
{
await Task.Delay(1000);
ctx.Status("Compiling...");
ctx.Spinner(Spinner.Known.Star);
await Task.Delay(1000);
ctx.Status("Publishing...");
await Task.Delay(1000);
});
プロンプト
テキストプロンプト
// Simple typed input
var name = AnsiConsole.Ask<string>("What's your [green]name[/]?");
var age = AnsiConsole.Ask<int>("What's your [green]age[/]?");
// With default value
var city = AnsiConsole.Prompt(
new TextPrompt<string>("Enter [green]city[/]:")
.DefaultValue("Seattle")
.ShowDefaultValue());
// Secret input (password)
var password = AnsiConsole.Prompt(
new TextPrompt<string>("Enter [green]password[/]:")
.Secret());
// With validation
var email = AnsiConsole.Prompt(
new TextPrompt<string>("Enter [green]email[/]:")
.Validate(input =>
input.Contains('@') && input.Contains('.')
? ValidationResult.Success()
: ValidationResult.Error("[red]Invalid email address[/]")));
// Optional (allow empty)
var nickname = AnsiConsole.Prompt(
new TextPrompt<string>("Enter [green]nickname[/] (optional):")
.AllowEmpty());
確認プロンプト
bool proceed = AnsiConsole.Confirm("Continue with deployment?");
選択プロンプト
var fruit = AnsiConsole.Prompt(
new SelectionPrompt<string>()
.Title("Pick a [green]fruit[/]:")
.PageSize(10)
.EnableSearch()
.WrapAround()
.AddChoices("Apple", "Banana", "Orange", "Mango", "Grape"));
// Grouped choices
var country = AnsiConsole.Prompt(
new SelectionPrompt<string>()
.Title("Select [green]destination[/]:")
.AddChoiceGroup("Europe", "France", "Italy", "Spain")
.AddChoiceGroup("Asia", "Japan", "Thailand", "Vietnam"));
複数選択プロンプト
var toppings = AnsiConsole.Prompt(
new MultiSelectionPrompt<string>()
.Title("Choose [green]toppings[/]:")
.PageSize(10)
.Required()
.InstructionsText("[grey](Press [blue]<space>[/] to toggle, [green]<enter>[/] to accept)[/]")
.AddChoices("Cheese", "Pepperoni", "Mushrooms", "Olives", "Onions"));
ライブ表示
ライブ表示は、時間とともに変化する動的なコンテンツをその場で更新します。
var table = new Table()
.AddColumn("Time")
.AddColumn("Status");
await AnsiConsole.Live(table)
.AutoClear(false)
.Overflow(VerticalOverflow.Ellipsis)
.Cropping(VerticalOverflowCropping.Bottom)
.StartAsync(async ctx =>
{
table.AddRow(DateTime.Now.ToString("T"), "[yellow]Starting...[/]");
ctx.Refresh();
await Task.Delay(1000);
table.AddRow(DateTime.Now.ToString("T"), "[green]Processing...[/]");
ctx.Refresh();
await Task.Delay(1000);
table.AddRow(DateTime.Now.ToString("T"), "[blue]Complete![/]");
ctx.Refresh();
});
ターゲットの置き換え
await AnsiConsole.Live(new Markup("[yellow]Initializing...[/]"))
.StartAsync(async ctx =>
{
await Task.Delay(1000);
ctx.UpdateTarget(new Markup("[green]Ready![/]"));
await Task.Delay(1000);
ctx.UpdateTarget(
new Panel("Final result: [bold]42[/]")
.Header("Done")
.Border(BoxBorder.Rounded));
});
Spectre.Console.Cli フレームワーク
Spectre.Console.Cli は、コマンド階層、型付き設定、検証、自動ヘルプ生成に対応した構造化コマンドライン解析フレームワークです。
基本的なコマンドアプリ
using Spectre.Console.Cli;
var app = new CommandApp<GreetCommand>();
return app.Run(args);
// Command with typed settings
public sealed class GreetSettings : CommandSettings
{
[CommandArgument(0, "<name>")]
[Description("The person to greet")]
public string Name { get; init; } = string.Empty;
[CommandOption("-c|--count")]
[Description("Number of times to greet")]
[DefaultValue(1)]
public int Count { get; init; }
[CommandOption("--shout")]
[Description("Greet in uppercase")]
public bool Shout { get; init; }
}
public sealed class GreetCommand : Command<GreetSettings>
{
public override int Execute(CommandContext context, GreetSettings settings)
{
for (int i = 0; i < settings.Count; i++)
{
var greeting = $"Hello, {settings.Name}!";
AnsiConsole.MarkupLine(settings.Shout
? $"[bold]{greeting.ToUpperInvariant()}[/]"
: greeting);
}
return 0; // exit code
}
}
ブランチを含むコマンド階層
var app = new CommandApp();
app.Configure(config =>
{
config.AddBranch<RemoteSettings>("remote", remote =>
{
remote.AddCommand<RemoteAddCommand>("add")
.WithDescription("Add a remote");
remote.AddCommand<RemoteRemoveCommand>("remove")
.WithDescription("Remove a remote");
});
config.AddCommand<CloneCommand>("clone")
.WithDescription("Clone a repository");
});
return app.Run(args);
// Shared settings for the branch -- inherited by subcommands
public class RemoteSettings : CommandSettings
{
[CommandOption("-v|--verbose")]
[Description("Verbose output")]
public bool Verbose { get; init; }
}
// Subcommand settings inherit from branch settings
public sealed class RemoteAddSettings : RemoteSettings
{
[CommandArgument(0, "<name>")]
public string Name { get; init; } = string.Empty;
[CommandArgument(1, "<url>")]
public string Url { get; init; } = string.Empty;
}
public sealed class RemoteAddCommand : Command<RemoteAddSettings>
{
public override int Execute(CommandContext context, RemoteAddSettings settings)
{
if (settings.Verbose)
{
AnsiConsole.MarkupLine($"[dim]Adding remote...[/]");
}
AnsiConsole.MarkupLine($"Added remote [green]{settings.Name}[/] -> {settings.Url}");
return 0;
}
}
設定の検証
public sealed class DeploySettings : CommandSettings
{
[CommandArgument(0, "<environment>")]
public string Environment { get; init; } = string.Empty;
[CommandOption("--timeout")]
[DefaultValue(30)]
public int TimeoutSeconds { get; init; }
public override ValidationResult Validate()
{
var validEnvs = new[] { "dev", "staging", "prod" };
if (!validEnvs.Contains(Environment, StringComparer.OrdinalIgnoreCase))
{
return ValidationResult.Error(
$"Environment must be one of: {string.Join(", ", validEnvs)}");
}
if (TimeoutSeconds <= 0)
{
return ValidationResult.Error("Timeout must be positive");
}
return ValidationResult.Success();
}
}
非同期コマンド
public sealed class FetchCommand : AsyncCommand<FetchSettings>
{
public override async Task<int> ExecuteAsync(
CommandContext context, FetchSettings settings)
{
await AnsiConsole.Status()
.StartAsync("Fetching data...", async ctx =>
{
await Task.Delay(2000); // simulate work
});
AnsiConsole.MarkupLine("[green]Done![/]");
return 0;
}
}
ITypeRegistrar を使った依存性注入
using Microsoft.Extensions.DependencyInjection;
using Spectre.Console.Cli;
// Set up DI container
var services = new ServiceCollection();
services.AddSingleton<IGreetingService, GreetingService>();
services.AddSingleton<IAnsiConsole>(AnsiConsole.Console);
var registrar = new TypeRegistrar(services);
var app = new CommandApp<GreetCommand>(registrar);
return app.Run(args);
// TypeRegistrar bridges Microsoft DI to Spectre.Console.Cli
public sealed class TypeRegistrar(IServiceCollection services) : ITypeRegistrar
{
public ITypeResolver Build() => new TypeResolver(services.BuildServiceProvider());
public void Register(Type service, Type implementation)
=> services.AddSingleton(service, implementation);
public void RegisterInstance(Type service, object implementation)
=> services.AddSingleton(service, implementation);
public void RegisterLazy(Type service, Func<object> factory)
=> services.AddSingleton(service, _ => factory());
}
public sealed class TypeResolver(IServiceProvider provider) : ITypeResolver
{
public object? Resolve(Type? type)
=> type is null ? null : provider.GetService(type);
}
// Command receives services via constructor injection
public sealed class GreetCommand(IGreetingService greetingService) : Command<GreetSettings>
{
public override int Execute(CommandContext context, GreetSettings settings)
{
var message = greetingService.GetGreeting(settings.Name);
AnsiConsole.MarkupLine(message);
return 0;
}
}
テスト可能なコンソール出力
Spectre.Console は、実際のコンソールに直接書き込む代わりに、テスト可能な出力を実現する IAnsiConsole を提供します。
// Production: use AnsiConsole.Console (the real console)
IAnsiConsole console = AnsiConsole.Console;
// Testing: use a recording console
var console = AnsiConsole.Create(new AnsiConsoleSettings
{
Out = new AnsiConsoleOutput(new StringWriter())
});
// Use the abstraction instead of static AnsiConsole methods
console.MarkupLine("[green]Testable output[/]");
console.Write(new Table().AddColumn("Col").AddRow("Val"));
エージェントでの注意点
- リダイレクトされた出力では
AnsiConsole.Markup*を使用しないでください。 標準出力がリダイレクトされている場合(ファイルや別のプロセスにパイプされている場合)、ANSI エスケープコードによって出力が破損します。マークアップを使用する前にAnsiConsole.Profile.Capabilities.Ansiを確認するか、適切な設定でIAnsiConsoleを使用してください。非同期パイプラインのパターンについては、[skill:dotnet-csharp-async-patterns] を参照してください。 - CI 環境で ANSI がサポートされていると想定しないでください。 CI ランナー(GitHub Actions、Azure Pipelines)では、ANSI エスケープコードがサポートされていない場合があります。CI で安全に出力するには、
TERM=dumbを設定するか、ColorSystemSupport.NoColorsを指定してAnsiConsole.Create()を使用してください。Spectre.Console は機能を自動検出しますが、明示的に設定すると表示の不安定さを防げます。 AnsiConsoleの静的呼び出しとIAnsiConsoleのインスタンス呼び出しを混在させないでください。 静的なAnsiConsole.Write()は常に実際のコンソールを対象とします。DI でIAnsiConsoleを使用する場合は、注入されたインスタンスを一貫して使用してください。両方を混在させると、出力が重複したり、入り混じったりします。Live()の実行中に、バックグラウンドスレッドからレンダリング可能オブジェクトを変更しないでください。 ライブ表示はスレッドセーフではありません。対象のレンダリング可能オブジェクトはStart/StartAsyncコールバック内でのみ変更し、その後ctx.Refresh()を呼び出してください。並行して変更すると、ターミナル出力が破損します。- 非対話型のコンテキストではプロンプトを使用しないでください。
TextPrompt、SelectionPrompt、ConfirmationPromptはユーザー入力を待つために処理をブロックします。CI や自動化スクリプトでは、プロンプトの代わりに環境変数またはコマンドライン引数を使って入力してください。プロンプトを表示する前にAnsiConsole.Profile.Capabilities.Interactiveを確認してください。 - Spectre.Console.Cli と System.CommandLine を混同しないでください。 これらは異なる API を持つ独立したフレームワークです。Spectre.Console.Cli は
[CommandArgument]/[CommandOption]属性を持つCommandSettingsクラスを使用しますが、System.CommandLine はOption<T>とArgument<T>のビルダーパターンを使用します。API を混在させないでください。System.CommandLine については、[skill:dotnet-system-commandline] を参照してください。 - ライブ表示の内容を変更した後は、必ず
ctx.Refresh()を呼び出してください。Live()コールバック内でテーブル、ツリー、パネルを変更しても、ctx.Refresh()を呼び出すまではレンダリングされません。これを省略すると、表示が更新されないままになります。 - フォールバックなしで色の値をハードコードしないでください。 色のサポートが限られたターミナルでは、TrueColor の値が警告なしに劣化します。可能であれば名前付きの色(
Color.Green)を使用し、NO_COLOR=1環境変数を使ってテストし、適切に劣化することを確認してください。
前提条件
- NuGet パッケージ: リッチな出力には
Spectre.Console0.54.0、CLI フレームワークにはSpectre.Console.Cli0.53.1 を追加してください - ターゲットフレームワーク: net8.0 以降(netstandard2.0 にも対応)
- ターミナル: ANSI エスケープシーケンスをサポートする任意のターミナルエミュレーター。最適な使用感(TrueColor、Unicode)を得るには、Windows Terminal、iTerm2、または最新の Linux ターミナルを推奨します。コンソール出力は、機能が限られたターミナルでも適切に劣化します。
- Spectre.Console.Cli での DI:
ITypeRegistrar/ITypeResolverブリッジにはMicrosoft.Extensions.DependencyInjectionパッケージが必要です
参考資料
- Spectre.Console GitHub -- ソースコード、課題、サンプル
- Spectre.Console Documentation -- 公式ガイドと API リファレンス
- Spectre.Console NuGet -- パッケージのダウンロードとバージョン履歴
- Spectre.Console.Cli NuGet -- CLI フレームワークのパッケージ
- Spectre.Console Examples -- 公式サンプルプロジェクト