上个月要写个内部用的小工具:把运营导出的工单 Excel 转成一批 INSERT 语句,方便我直接在测试库里造数据。以前这种东西我都是写个控制台程序,用 args[0]args[1] 硬取,多一个参数就乱。这次试了下 System.CommandLine,写完觉得挺顺手,记一下用法和几个不顺的地方。

项目是 .NET 9 控制台,装的包是 System.CommandLine,版本 2.0.0-beta7。之所以写清楚版本,是因为这个库的 API 在 beta 期间改过好几轮,网上搜到的例子(尤其是 2023 年之前那批)基本都跑不通了,我一开始照着抄,编译不过,还以为是自己的问题。

一、定义选项和子命令

工具最后有两个子命令,convert 做转换,check 只校验不产出文件。代码大概是这样:

var fileOption = new Option<FileInfo>(
    aliases: new[] { "--file", "-f" },
    description: "要转换的 Excel 文件路径")
{ IsRequired = true };

var tableOption = new Option<string>(
    aliases: new[] { "--table", "-t" },
    getDefaultValue: () => "TicketImport");

var convert = new Command("convert", "把 Excel 转成 INSERT 脚本")
{
    fileOption, tableOption
};

convert.SetHandler((FileInfo file, string table) =>
{
    Console.WriteLine($"{file.FullName} -> {table}");
}, fileOption, tableOption);

var root = new RootCommand("工单导入辅助工具") { convert };
return await root.InvokeAsync(args);

几个点:Option<T> 的泛型决定了类型,FileInfoDirectoryInfointbool、枚举都是开箱支持的,会帮你做转换和校验——传个不存在的路径或者不合法的整数,它直接报错并打印用法,不用自己写。SetHandler 最多支持十六个参数,超出的我没试过。

二、自动生成的帮助

加完上面这些,--help-h 直接就有了,会按层级列出子命令、选项、默认值和必填标记,还会带上 --version。这一项对我来说是最大的收益:以前手写的用法说明经常和代码不同步,现在至少描述文字是从 description 来的,改起来在一处。

必填项漏传时的提示也很清楚,会明确告诉你是哪个选项缺了。这比我自己写的 if (args.Length < 2) 强多了。

三、不顺的地方

  • 版本和资料。上面说过了,beta 期间 API 变动频繁,尤其早期那套「定义一个类、用属性绑定命令行参数」的写法(NamingConventionBinder 那套包),现在已经被移出主包,我查了半天才发现不是自己写错。
  • 自定义类型的绑定。内置的那些类型够用,但要绑定一个自己定义的复杂类型,我到现在也没搞明白推荐做法是什么,最后是拆成几个基本类型选项绕过去的。这块我没深入,不确定官方现在有没有更好的方案。
  • 返回值SetHandler 的委托如果返回 Task<int>,这个值会作为进程的退出码,这点一开始我没注意,导致 CI 里判断失败的那几行一直没生效。

四、值不值得用

如果这个工具只有你自己用、参数不超过两个,直接取 args 也无所谓。但只要参数一多、或者要交给别人用,我觉得值得——尤其是帮助信息和参数校验这两块,自己实现一遍并不比学这个库省事。

顺便说一句,那个工具现在还在用,一共两百来行,加了 --dry-run 之后我不担心误操作了。(2026-07 补充:后来把包升到了 2.0.0 正式版,上面这段代码一行没改就能跑,这点比我想的稳。)