用 System.CommandLine 构建工程级 CLI 工具

用 System.CommandLine 构建工程级 CLI 工具

—— 从“手写 Command 模式”到工业级命令行框架

在实际工程中,我们经常需要构建工具型程序

  • 数据预处理工具
  • 离线算法管线
  • 图像 / 点云 / 配置文件批处理
  • Unity / C# 项目的辅助命令行工具

一开始,最自然的做法是:
👉 使用 Command Pattern(命令模式)自行实现一套 CLI 调度系统

这在架构上是完全正确的

但当命令逐渐增多、参数变复杂之后,你会发现:

我们并不是在实现“业务命令”,
而是在不断重复造一个 命令行解析框架

这正是 System.CommandLine 存在的意义。


一、回顾:传统的 Command 模式 CLI 设计

典型的手写方案通常长这样:

- ICommand
- CommandGenerate
- CommandParse
- CommandExport
- Program.cs 中手动解析 args
- switch / dictionary 做 dispatch

这种方式的优点

  • 命令解耦
  • 易扩展
  • 符合 Command Pattern
  • 业务逻辑清晰

但问题也很明显

  • 参数解析需要自己写
  • --help / usage 要自己维护
  • 子命令嵌套成本高
  • 参数校验、默认值、类型转换重复劳动

问题不在设计,而在“你做了太多框架层的工作”。


二、System.CommandLine 是什么?

一句话定义:

System.CommandLine 是微软官方提供的、
基于 Command Pattern 的命令行应用框架

它做的事情只有一件:

把“命令行解析 + 路由”这层脏活累活标准化

而你只需要关心:

  • 命令是什么
  • 参数是什么
  • 业务逻辑是什么

三、核心设计思想(重要)

很多人误解 System.CommandLine 是“替代 Command 模式”。

实际上正好相反:

System.CommandLine 是 Command 模式在 CLI 场景下的成熟实现

你之前的设计思想是对的,只是:

  • ❌ 手写了解析和调度
  • ✅ 现在交给框架

四、整体结构设计(推荐)

一个工程级 CLI 工具,我推荐这样的结构:

ProjectRoot
│
├── Program.cs              // CLI 装配层
│
├── Commands/               // 命令定义层
│   ├── GenerateCommand.cs
│   ├── ParseCommand.cs
│   └── ExportCommand.cs
│
├── Services/               // 核心业务
│   ├── Generator.cs
│   ├── Parser.cs
│   └── Exporter.cs
│
└── Infrastructure/
    └── PathResolver.cs

👉 Command 层不写业务,只表达“意图”


五、最小可用示例

1️⃣ Program.cs:只负责注册命令

using System.CommandLine;

var root = new RootCommand("Sample CLI Tool");

root.AddCommand(GenerateCommand.Build());
root.AddCommand(ParseCommand.Build());

return await root.InvokeAsync(args);

没有 switch,没有 if,没有手写解析。


2️⃣ 一个命令 = 一个文件

using System.CommandLine;

public static class GenerateCommand
{
    public static Command Build()
    {
        var command = new Command(
            "generate",
            "Generate output from input data"
        );

        var input = new Option<string>(
            "--input",
            description: "Input file path"
        ) { IsRequired = true };

        var output = new Option<string>(
            "--output",
            description: "Output file path"
        ) { IsRequired = true };

        command.AddOption(input);
        command.AddOption(output);

        command.SetHandler((string i, string o) =>
        {
            Console.WriteLine($"Generate: {i} -> {o}");
            // Generator.Run(i, o);
        }, input, output);

        return command;
    }
}

3️⃣ 自动获得的能力

不需要你写任何额外代码,就已经支持:

tool generate --input data.json --output result.bin
tool generate --help
tool --help

包括:

  • 参数校验
  • Help 文档
  • 子命令结构
  • 类型安全绑定

六、支持 --relative / --absolute 的工程实践

在实际项目中,路径处理是高频需求。

推荐做法是:

  • CLI 层只接收“路径意图”
  • 路径解析统一放到 Infrastructure 层
public static class PathResolver
{
    public static string Resolve(string path, bool relative)
    {
        return relative
            ? Path.GetFullPath(path)
            : path;
    }
}

Command 里只做:

var relative = new Option<bool>("--relative", "Use relative path");

👉 这样命令层保持干净,后续也方便迁移到 GUI / Editor 工具。


七、和手写 Command 模式的关系

维度 手写 Command System.CommandLine
架构思想 Command Pattern Command Pattern
参数解析 自己写 框架内置
Help 文档 自维护 自动生成
扩展成本
工业成熟度 自制 官方

这不是推翻重来,而是自然升级。


八、适用场景总结

System.CommandLine 非常适合:

  • 离线工具
  • 算法管线
  • Unity / C# 辅助工具
  • 内部工程工具
  • 自动化脚本入口

如果你发现自己正在:

“为了 CLI 写大量非业务代码”

那就是一个明确的信号
👉 该用它了。


结语

命令模式本身没有问题,
问题在于:你不该在每个项目里重复造轮子

System.CommandLine 做的不是“帮你偷懒”,
而是把精力从框架层解放出来,让你专注真正重要的事。\

posted @ 2026-01-22 16:52  世纪末の魔术师  阅读(2)  评论(0)    收藏  举报