65.9K
CodeProject 正在变化。 阅读更多。
Home

使用 CmdStarter 层轻松集成 System.CommandLine

starIconstarIconstarIconstarIconstarIcon

5.00/5 (11投票s)

2023 年 7 月 12 日

MIT

5分钟阅读

viewsIcon

10620

System.CommandLine 之上的一个层,总体上简化了 POSIX 风格命令集成到现有项目中。

引言

Microsoft 有一个名为 System.CommandLine 的包,它仍处于 Beta 版本,但功能齐全。然而,集成不能在现有类上进行(除非继承),也没有将属性自动绑定到命令选项的功能。这个库是 System.CommandLine 之上的一个层,旨在简化到现有项目的集成。

这是一个 MIT 许可下的开源项目。

特点

  • 使用抽象类或接口实现命令
  • 按命名空间或完整类名过滤当前执行使用的类
  • 支持使用依赖注入的类
  • 将类标记为全局选项容器
  • 在执行方法中轻松访问全局选项
  • 单个命令可以被根化
  • 将属性自动绑定到 System.CommandLine 命令选项
  • 将执行方法参数自动绑定到 System.CommandLine 命令参数
  • 提供了 AliasHiddenDescriptionAutoComplete 属性来设置命令选项/参数的属性
  • 通过命名空间或 Parent | Children 属性自动加载命令树

第一个简单示例

  • 导入 Nuget 包。

确保选中 Prerelease (预发行) 复选框

  • 命令集成
    1. 创建一个继承自 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);
          }
      }
    2. 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.NamespacesStarter.Classes 属性进行过滤。类型可通过 CommandsTypes 获取。
  • Starter.BuildTree: 使用命名空间、[Parent]/[Children] 属性或两者来构建命令树。树可通过 CommandsTypesTree 获取。
  • Starter.InstantiateCommands: 实例化带选项的命令。如果只有一个命令且 Starter.IsRootingLonelyCommand 已启用,则该命令将被根化。这意味着从命令行执行它不需要其名称,如上面的第一个示例所示。此方法还会加载全局选项。命令可通过 RootCommand 获取。
  • Starter.Start: 使用程序参数执行命令。

最后一个方法按以下顺序链接其他方法:

除了 Start 方法,其他所有方法都只能执行一次,除非其相应变量已更改。它们有一个保护机制,确保第二次执行无效。这是这样构建的,因为它允许在执行链中进行暂停。

例如,执行第一个方法 FindCommandsTypes,更改 CommandsTypes,然后继续使用 Start

图中的白色框是来自 System.CommandLine 的解析执行。它也连接回 StarterCommand.HandleCommand,该方法将解析的选项加载到其各自的对象属性中,并使用参数执行命令的处理方法。

过滤找到的类型

该库提供了两个过滤器,可用于缩小 FindCommandsTypes 找到的命令类型范围:NamespacesClasses。两者都有一个排除符号 "~" 可用。

Namespaces 属性的工作方式如下:

  1. 从找到的类型中,如果存在要包含的命名空间,则只保留这些。
  2. 在剩余的类型中,删除所有属于要排除的命名空间中的类型。

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.Commands
  • 集成可以非常简洁
  • 扩展性已存在,尽管总有改进的空间
  • 完全使用 TDD(测试驱动开发)

特别感谢!

我想向 Norbert Ormándi 致以巨大的感谢,感谢他为本项目做出的贡献。

未来可能的开发

  • REPL 模式
  • 过滤将成为选项的属性
  • Fluent 支持
  • 其他:请参阅 Jira 项目

历史

  • 1.0-2023.06.23: 第一篇文稿版本
  • 1.1-2023.07.12: 增加了关于工作原理及其可能性的很多内容
© . All rights reserved.