使用 CmdStarter 层轻松集成 System.CommandLine





5.00/5 (11投票s)
System.CommandLine 之上的一个层,总体上简化了 POSIX 风格命令集成到现有项目中。
引言
Microsoft 有一个名为 System.CommandLine
的包,它仍处于 Beta 版本,但功能齐全。然而,集成不能在现有类上进行(除非继承),也没有将属性自动绑定到命令选项的功能。这个库是 System.CommandLine
之上的一个层,旨在简化到现有项目的集成。
这是一个 MIT 许可下的开源项目。
- 代码仓库: https://github.com/CyberInternauts/CmdStarter
- Nuget 包: https://nuget.net.cn/packages/cints.CmdStarter
- Jira: https://cyberinternauts.atlassian.net/browse/CMD
特点
- 使用抽象类或接口实现命令
- 按命名空间或完整类名过滤当前执行使用的类
- 支持使用依赖注入的类
- 将类标记为全局选项容器
- 在执行方法中轻松访问全局选项
- 单个命令可以被根化
- 将属性自动绑定到
System.CommandLine
命令选项 - 将执行方法参数自动绑定到
System.CommandLine
命令参数 - 提供了
Alias
、Hidden
、Description
和AutoComplete
属性来设置命令选项/参数的属性 - 通过命名空间或
Parent
|Children
属性自动加载命令树
第一个简单示例
- 导入 Nuget 包。
确保选中 Prerelease (预发行) 复选框
- 命令集成
- 创建一个继承自
StarterCommand
的新类。using com.cyberinternauts.csharp.CmdStarter.Lib; using System.ComponentModel; internal class Files : StarterCommand { public override Delegate HandlingMethod => Execute; private void Execute([Description("Folder to list files")] string folder) { Console.WriteLine("Should list " + folder); } }
- 将
IStarterCommand
接口添加到具有无参数构造函数的现有类中。using com.cyberinternauts.csharp.CmdStarter.Lib; using com.cyberinternauts.csharp.CmdStarter.Lib.Interfaces; internal class ShowInt : IStarterCommand { private int MyInt { get; set; } = 111; public GlobalOptionsManager? GlobalOptionsManager { get; set; } public Delegate HandlingMethod => () => { Console.WriteLine(nameof(MyInt) + "=" + MyInt); }; }
有关依赖注入,请参见下文。
- 创建一个继承自
- 创建下面的
Program
类internal class Program { public static async Task Main(string[] args) { var starter = new CmdStarter.Lib.Starter(); await starter.Start(args); } }
- 编译并运行
PROGRAM_NAME.exe "my\path\to\list".
- 它会在控制台打印 "
Should list my\path\to\list
"。
依赖注入
这些方法允许类具有带参数的构造函数。
- 可以重写
IStarterCommand.GetInstance
方法 - 可以使用
Starter.SetFactory
来更改默认的实例化行为 - 可以调用
(new Starter()).Start(IServiceManager, string[])
,其中IServiceManager
是一个实现了IServiceManager
接口的对象。
可以使用您喜欢的任何库来支持依赖注入。代码仓库包含一个使用 Simple Injector 的示例。
更广泛地说,它提供了对类实例化的完全控制。
幕后原理
该库分为四个任务(对应四个 Starter
方法)
Starter.FindCommandsTypes
: 查找实现了IStarterCommand
接口或继承了StarterCommand
抽象类的类型。此方法还使用Starter.Namespaces
和Starter.Classes
属性进行过滤。类型可通过CommandsTypes
获取。Starter.BuildTree
: 使用命名空间、[Parent]/[Children] 属性或两者来构建命令树。树可通过CommandsTypesTree
获取。Starter.InstantiateCommands
: 实例化带选项的命令。如果只有一个命令且Starter.IsRootingLonelyCommand
已启用,则该命令将被根化。这意味着从命令行执行它不需要其名称,如上面的第一个示例所示。此方法还会加载全局选项。命令可通过RootCommand
获取。Starter.Start
: 使用程序参数执行命令。
最后一个方法按以下顺序链接其他方法:
除了 Start
方法,其他所有方法都只能执行一次,除非其相应变量已更改。它们有一个保护机制,确保第二次执行无效。这是这样构建的,因为它允许在执行链中进行暂停。
例如,执行第一个方法 FindCommandsTypes
,更改 CommandsTypes
,然后继续使用 Start
。
图中的白色框是来自 System.CommandLine
的解析执行。它也连接回 StarterCommand.HandleCommand
,该方法将解析的选项加载到其各自的对象属性中,并使用参数执行命令的处理方法。
过滤找到的类型
该库提供了两个过滤器,可用于缩小 FindCommandsTypes
找到的命令类型范围:Namespaces
和 Classes
。两者都有一个排除符号 "~
" 可用。
Namespaces
属性的工作方式如下:
- 从找到的类型中,如果存在要包含的命名空间,则只保留这些。
- 在剩余的类型中,删除所有属于要排除的命名空间中的类型。
Classes
属性的工作方式与 Namespaces
属性相同,但它也接受通配符。
它们是:
- 一个排除点的单字符通配符:"?"
- 一个包含点的单字符通配符:"??"
- 零个或多个字符(不含点):"*"
- 零个或多个字符(包含点):"**"
是的,Classes
过滤器也考虑命名空间,并且更通用,但为了方便起见,该库继续提供 Namespaces
过滤器。
使用属性
树定位属性
Children 属性
使用命名空间手动设置命令类的子项。
[Children<Child1>]
public class ChildingParent : StarterCommand
{
}
这里,ChildrenAttribute
可以接受一个用于其命名空间的类,或者一个命名空间字符串。
Parent 属性
使用另一个 IStarterCommand
手动设置命令类的父项。
[Parent<ParentWithChildren>]
public class ChildWithParent : StarterCommand
{
}
GlobalOption 属性
命令/选项功能属性
Alias 属性
如果您需要为命令或选项设置别名,可以使用 AliasAttribute
类来关联一个新别名。
[Alias(COMMAND_ALIAS)]
public sealed class OneAlias : StarterCommand
{
private const string COMMAND_ALIAS = "oa";
private const string OPTION_ALIAS = "o";
[Alias(true, OPTION_ALIAS)]
public int Option { get; set; }
}
AutoComplete 属性
此属性将不会被完全详细介绍,因为它可能值得专门写一篇文章。
简而言之,它允许在键入选项或参数的值时提供自动补全建议。选择可以来自静态列表或可以连接到数据库的提供程序。还有一个 IAutoCompleteFactory
,可用于完全控制每个自动补全建议。
Description 属性
description
属性用于构建帮助信息。它可以应用于命令类、选项属性或参数方法参数。
// ---- On a command class ----
[Description(DESC)]
public class SingleDesc : StarterCommand
{
public const string DESC = "One liner";
// ---- On an option property ----
[Description(STRING_OPT_DESC)]
public string? StringOpt { get; set; }
public const string STRING_OPT_DESC = "My first string option";
public override Delegate HandlingMethod => Handle;
public const string FIRST_PARAM_DESC = "My first parameter";
private void Handle(
// ---- On an argument method parameter ----
[Description(FIRST_PARAM_DESC)] string myFirstParam
)
{
// Code handling the command
}
}
Hidden 属性
正如上一个属性提供了一种在帮助信息中显示描述的方法一样,这个属性则相反。它会完全隐藏帮助信息中的命令、选项或参数。
[Hidden]
public sealed class HiddenCommand : StarterCommand
{
public override Delegate HandlingMethod => ([Hidden] int parameter) => { };
[Hidden]
public bool Option { get; set; }
}
Required 属性
RequiredAttribute
唯一适用的组件是选项。尽管对于没有默认值的参数参数,有一个隐式的 [Required]
。
public class OptRequired : StarterCommand
{
[Required]
public int IntOpt { get; set; }
public const string INT_OPT_KEBAB = "int-opt";
}
全局选项容器示例
全局选项在任何命令执行中都可以访问。
首先,像这段代码中的一样,将 IGlobalOptionsContainer
接口应用于一个类。
public class MainGlobalOptions : IGlobalOptionsContainer
{
[Description(INT_GLOBAL_OPTION_DESC)]
public int IntGlobalOption { get; set; } = INT_GLOBAL_OPTION_VALUE;
public const int INT_GLOBAL_OPTION_VALUE = 888;
public const string INT_GLOBAL_OPTION_DESC = "My first global option";
}
其次,访问选项值并使用它。
public class ExecSum : StarterCommand
{
private const int DEFAULT_INT_OPTION_VALUE = 11;
public int MyInt { get; set; } = DEFAULT_INT_OPTION_VALUE;
public override Delegate HandlingMethod => Execute;
public int Execute(int param1)
{
// ---- Obtain the global option container object ----
var globalOptions =
this.GlobalOptionsManager?.GetGlobalOptions<MainGlobalOptions>();
// ---- Read the global option value ---
var globalInt = (globalOptions?.IntGlobalOption ?? 0);
// ---- Sum it with an option and an argument
return globalInt + MyInt + param1;
}
}
编译后的最终用法
PROGRAM_NAME.exe \
--global-option1 "global option value 1" \
command-one \
--option1 "option value 1" \
"param value 1"
结论
该库充当 System.CommandLine
之上的一个层。它查找命令,将选项和参数与它们关联。它查找全局选项。该库负责在执行时填充适当的属性和参数。System.CommandLine
提供的所有功能都应该被 CmdStarter
支持。
关注点
- 无需创建和/或填充
System.CommandLine.Command
s - 集成可以非常简洁
- 扩展性已存在,尽管总有改进的空间
- 完全使用 TDD(测试驱动开发)
特别感谢!
我想向 Norbert Ormándi 致以巨大的感谢,感谢他为本项目做出的贡献。
未来可能的开发
- REPL 模式
- 过滤将成为选项的属性
- Fluent 支持
- 其他:请参阅 Jira 项目
历史
- 1.0-2023.06.23: 第一篇文稿版本
- 1.1-2023.07.12: 增加了关于工作原理及其可能性的很多内容