name: dotnet-spectre-console description: “渲染丰富的控制台输出。Spectre.Console 表格、树、进度、提示、实时显示。” user-invocable: false
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 丰富输出:标记、表格、树、进度条、提示、实时显示
- Spectre.Console.Cli 命令行应用程序框架
超出范围
- 完整的 TUI 应用程序(窗口、菜单、对话框、视图)—— 参见 [skill:dotnet-terminal-gui]
- System.CommandLine 解析 —— 参见 [skill:dotnet-system-commandline]
- CLI 应用程序架构和分发 —— 参见 [skill:dotnet-cli-architecture] 和 [skill:dotnet-cli-distribution]
交叉引用:[skill:dotnet-terminal-gui] 用于完整的 TUI 替代方案,[skill:dotnet-system-commandline] 用于 System.CommandLine 范围边界,[skill:dotnet-cli-architecture] 用于 CLI 结构,[skill:dotnet-csharp-async-patterns] 用于异步模式,[skill:dotnet-csharp-dependency-injection] 用于 DI 与 Spectre.Console.Cli,[skill:dotnet-accessibility] 用于 TUI 可访问性限制和屏幕阅读器考虑。
包引用
<ItemGroup>
<!-- 丰富的控制台输出:标记、表格、树、进度、提示、实时显示 -->
<PackageReference Include="Spectre.Console" Version="0.54.0" />
<!-- CLI 命令框架(添加命令解析、设置、DI 支持) -->
<PackageReference Include="Spectre.Console.Cli" Version="0.53.1" />
</ItemGroup>
Spectre.Console.Cli 依赖于 Spectre.Console —— 仅当需要 CLI 框架时安装两者。对于仅丰富输出,单独使用 Spectre.Console 足够。
标记和样式
Spectre.Console 使用受 BBCode 启发的标记语法用于样式化控制台输出。
基本标记
using Spectre.Console;
// 使用标记标签的样式化文本
AnsiConsole.MarkupLine("[bold red]错误:[/] 文件未找到。");
AnsiConsole.MarkupLine("[green]成功![/] 构建在 [blue]2.3 秒[/] 内完成。");
AnsiConsole.MarkupLine("[underline]https://example.com[/]");
AnsiConsole.MarkupLine("[dim italic]这是细微文本[/]");
// 嵌套样式
AnsiConsole.MarkupLine("[bold [red on white]警告:[/] 检查配置[/]");
// 使用双括号转义括号
AnsiConsole.MarkupLine("使用 [[bold]] 表示粗体文本。");
Figlet 文本
AnsiConsole.Write(
new FigletText("Hello!")
.Color(Color.Green)
.Centered());
规则(水平线)
// 简单规则
AnsiConsole.Write(new Rule());
// 带标题的规则
AnsiConsole.Write(new Rule("[yellow]章节标题[/]"));
// 对齐规则
AnsiConsole.Write(new Rule("[blue]左对齐[/]").LeftJustified());
表格
var table = new Table();
// 添加列
table.AddColumn("名称");
table.AddColumn(new TableColumn("年龄").Centered());
table.AddColumn(new TableColumn("城市").RightAligned());
// 添加行
table.AddRow("Alice", "30", "西雅图");
table.AddRow("[green]Bob[/]", "25", "波特兰");
table.AddRow("Charlie", "35", "温哥华");
// 样式化
table.Border(TableBorder.Rounded);
table.BorderColor(Color.Grey);
table.Title("[underline]团队成员[/]");
table.Caption("[dim]每日更新[/]");
// 列配置
table.Columns[0].PadLeft(2);
table.Columns[0].NoWrap();
AnsiConsole.Write(table);
嵌套表格
var innerTable = new Table()
.AddColumn("详情")
.AddColumn("值")
.AddRow("角色", "开发者")
.AddRow("级别", "高级");
var outerTable = new Table()
.AddColumn("名称")
.AddColumn("信息")
.AddRow("Alice", innerTable);
AnsiConsole.Write(outerTable);
树
var tree = new Tree("解决方案");
// 添加节点
var srcNode = tree.AddNode("[yellow]src[/]");
var apiNode = srcNode.AddNode("Api");
apiNode.AddNode("Controllers/");
apiNode.AddNode("Program.cs");
var libNode = srcNode.AddNode("库");
libNode.AddNode("服务/");
var testNode = tree.AddNode("[blue]tests[/]");
testNode.AddNode("Api.测试/");
// 样式化
tree.Style = Style.Parse("dim");
AnsiConsole.Write(tree);
面板
var panel = new Panel("这是 [green]重要[/] 内容。")
.Header("[bold]通知[/]")
.Border(BoxBorder.Rounded)
.BorderColor(Color.Blue)
.Padding(2, 1) // 水平, 垂直
.Expand(); // 填充可用宽度
AnsiConsole.Write(panel);
使用列组合可渲染对象
AnsiConsole.Write(new Columns(
new Panel("左面板").Expand(),
new Panel("右面板").Expand()));
进度显示
进度条
await AnsiConsole.Progress()
.AutoClear(false) // 保持已完成任务可见
.HideCompleted(false)
.Columns(
new TaskDescriptionColumn(),
new ProgressBarColumn(),
new PercentageColumn(),
new RemainingTimeColumn(),
new SpinnerColumn())
.StartAsync(async ctx =>
{
var downloadTask = ctx.AddTask("[green]下载中[/]", maxValue: 100);
var extractTask = ctx.AddTask("[blue]解压中[/]", 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("处理中...", async ctx =>
{
await Task.Delay(1000);
ctx.Status("编译中...");
ctx.Spinner(Spinner.Known.Star);
await Task.Delay(1000);
ctx.Status("发布中...");
await Task.Delay(1000);
});
提示
文本提示
// 简单类型输入
var name = AnsiConsole.Ask<string>("你的 [green]名字[/] 是什么?");
var age = AnsiConsole.Ask<int>("你的 [green]年龄[/] 是多少?");
// 带默认值
var city = AnsiConsole.Prompt(
new TextPrompt<string>("输入 [green]城市[/]:")
.DefaultValue("西雅图")
.ShowDefaultValue());
// 秘密输入(密码)
var password = AnsiConsole.Prompt(
new TextPrompt<string>("输入 [green]密码[/]:")
.Secret());
// 带验证
var email = AnsiConsole.Prompt(
new TextPrompt<string>("输入 [green]电子邮件[/]:")
.Validate(input =>
input.Contains('@') && input.Contains('.')
? ValidationResult.Success()
: ValidationResult.Error("[red]无效的电子邮件地址[/]")));
// 可选(允许空)
var nickname = AnsiConsole.Prompt(
new TextPrompt<string>("输入 [green]昵称[/](可选):")
.AllowEmpty());
确认提示
bool proceed = AnsiConsole.Confirm("继续部署吗?");
选择提示
var fruit = AnsiConsole.Prompt(
new SelectionPrompt<string>()
.Title("选择一个 [green]水果[/]:")
.PageSize(10)
.EnableSearch()
.WrapAround()
.AddChoices("苹果", "香蕉", "橙子", "芒果", "葡萄"));
// 分组选择
var country = AnsiConsole.Prompt(
new SelectionPrompt<string>()
.Title("选择 [green]目的地[/]:")
.AddChoiceGroup("欧洲", "法国", "意大利", "西班牙")
.AddChoiceGroup("亚洲", "日本", "泰国", "越南"));
多选提示
var toppings = AnsiConsole.Prompt(
new MultiSelectionPrompt<string>()
.Title("选择 [green]配料[/]:")
.PageSize(10)
.Required()
.InstructionsText("[grey](按 [blue]<空格>[/] 切换,[green]<回车>[/] 接受)[/]")
.AddChoices("奶酪", "意大利辣香肠", "蘑菇", "橄榄", "洋葱"));
实时显示
实时显示在原地更新动态内容。
var table = new Table()
.AddColumn("时间")
.AddColumn("状态");
await AnsiConsole.Live(table)
.AutoClear(false)
.Overflow(VerticalOverflow.Ellipsis)
.Cropping(VerticalOverflowCropping.Bottom)
.StartAsync(async ctx =>
{
table.AddRow(DateTime.Now.ToString("T"), "[yellow]启动中...[/]");
ctx.Refresh();
await Task.Delay(1000);
table.AddRow(DateTime.Now.ToString("T"), "[green]处理中...[/]");
ctx.Refresh();
await Task.Delay(1000);
table.AddRow(DateTime.Now.ToString("T"), "[blue]完成![/]");
ctx.Refresh();
});
替换目标
await AnsiConsole.Live(new Markup("[yellow]初始化中...[/]"))
.StartAsync(async ctx =>
{
await Task.Delay(1000);
ctx.UpdateTarget(new Markup("[green]准备就绪![/]"));
await Task.Delay(1000);
ctx.UpdateTarget(
new Panel("最终结果: [bold]42[/]")
.Header("完成")
.Border(BoxBorder.Rounded));
});
Spectre.Console.Cli 框架
Spectre.Console.Cli 提供了一个结构化的命令行解析框架,支持命令层次、类型化设置、验证和自动帮助生成。
基本命令应用
using Spectre.Console.Cli;
var app = new CommandApp<GreetCommand>();
return app.Run(args);
// 带类型化设置的命令
public sealed class GreetSettings : CommandSettings
{
[CommandArgument(0, "<名称>")]
[Description("要问候的人")]
public string Name { get; init; } = string.Empty;
[CommandOption("-c|--count")]
[Description("问候次数")]
[DefaultValue(1)]
public int Count { get; init; }
[CommandOption("--shout")]
[Description("以大写字母问候")]
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 = $"你好, {settings.Name}!";
AnsiConsole.MarkupLine(settings.Shout
? $"[bold]{greeting.ToUpperInvariant()}[/]"
: greeting);
}
return 0; // 退出码
}
}
带分支的命令层次
var app = new CommandApp();
app.Configure(config =>
{
config.AddBranch<RemoteSettings>("remote", remote =>
{
remote.AddCommand<RemoteAddCommand>("add")
.WithDescription("添加远程");
remote.AddCommand<RemoteRemoveCommand>("remove")
.WithDescription("移除远程");
});
config.AddCommand<CloneCommand>("clone")
.WithDescription("克隆仓库");
});
return app.Run(args);
// 分支的共享设置 —— 被子命令继承
public class RemoteSettings : CommandSettings
{
[CommandOption("-v|--verbose")]
[Description("详细输出")]
public bool Verbose { get; init; }
}
// 子命令设置从分支设置继承
public sealed class RemoteAddSettings : RemoteSettings
{
[CommandArgument(0, "<名称>")]
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]正在添加远程...[/]");
}
AnsiConsole.MarkupLine($"已添加远程 [green]{settings.Name}[/] -> {settings.Url}");
return 0;
}
}
设置验证
public sealed class DeploySettings : CommandSettings
{
[CommandArgument(0, "<环境>")]
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(
$"环境必须是以下之一: {string.Join(", ", validEnvs)}");
}
if (TimeoutSeconds <= 0)
{
return ValidationResult.Error("超时必须为正数");
}
return ValidationResult.Success();
}
}
异步命令
public sealed class FetchCommand : AsyncCommand<FetchSettings>
{
public override async Task<int> ExecuteAsync(
CommandContext context, FetchSettings settings)
{
await AnsiConsole.Status()
.StartAsync("正在获取数据...", async ctx =>
{
await Task.Delay(2000); // 模拟工作
});
AnsiConsole.MarkupLine("[green]完成![/]");
return 0;
}
}
使用 ITypeRegistrar 的依赖注入
using Microsoft.Extensions.DependencyInjection;
using Spectre.Console.Cli;
// 设置 DI 容器
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 桥接 Microsoft DI 到 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);
}
// 命令通过构造函数注入接收服务
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 用于可测试的输出,而不是直接写入真实控制台。
// 生产环境:使用 AnsiConsole.Console(真实控制台)
IAnsiConsole console = AnsiConsole.Console;
// 测试环境:使用录制控制台
var console = AnsiConsole.Create(new AnsiConsoleSettings
{
Out = new AnsiConsoleOutput(new StringWriter())
});
// 使用抽象而不是静态 AnsiConsole 方法
console.MarkupLine("[green]可测试输出[/]");
console.Write(new Table().AddColumn("列").AddRow("值"));
代理注意事项
- 不要在重定向输出中使用
AnsiConsole.Markup*。 当 stdout 被重定向时(管道到文件或其他进程),ANSI 转义码会破坏输出。在使用标记前检查AnsiConsole.Profile.Capabilities.Ansi,或使用具有适当设置的IAnsiConsole。参见 [skill:dotnet-csharp-async-patterns] 获取异步管道模式。 - 不要假设 CI 环境支持 ANSI。 CI 运行器(GitHub Actions、Azure Pipelines)可能不支持 ANSI 转义码。设置
TERM=dumb或使用带ColorSystemSupport.NoColors的AnsiConsole.Create()以确保 CI 安全输出。Spectre.Console 自动检测能力,但显式配置可防止渲染问题。 - 不要混合
AnsiConsole静态调用和IAnsiConsole实例调用。 静态AnsiConsole.Write()始终针对真实控制台。当使用带有IAnsiConsole的 DI 时,应一致使用注入的实例。混合两者会导致重复或交错输出。 - 不要在后台线程中从
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 用于丰富输出;添加Spectre.Console.Cli0.53.1 用于 CLI 框架 - 目标框架: net8.0 或更高版本(也支持 netstandard2.0)
- 终端: 任何支持 ANSI 转义序列的终端模拟器。推荐 Windows Terminal、iTerm2 或现代 Linux 终端以获得最佳体验(TrueColor、Unicode)。在有限终端上控制台输出优雅降级。
- 对于带 Spectre.Console.Cli 的 DI:
Microsoft.Extensions.DependencyInjection包用于ITypeRegistrar/ITypeResolver桥接
参考
- Spectre.Console GitHub —— 源代码、问题、示例
- Spectre.Console 文档 —— 官方指南和 API 参考
- Spectre.Console NuGet —— 包下载和版本历史
- Spectre.Console.Cli NuGet —— CLI 框架包
- Spectre.Console 示例 —— 官方示例项目