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

用于 IConfiguration 的命令行解析

starIconstarIconstarIconstarIconstarIcon

5.00/5 (3投票s)

2021年10月1日

GPL3

8分钟阅读

viewsIcon

9496

用于解析命令行的 IConfigurationBuilder 插件

引言

市面上有很多适用于 .NET 的命令行解析器,其中一些可以与 Microsoft 的 IConfiguration API 一起使用。我编写 J4JCommandLine 是因为我对命令行解析的内部工作原理感到好奇……也因为我发现的解析器配置起来很麻烦。公平地说,这可能是因为它们比 J4JCommandLine 更灵活,允许您将命令行绑定到各种目标。

将解析器的范围限制在 IConfiguration 系统内,会降低其目标绑定的灵活性。但它获得的收益大于损失(至少在我看来是这样 :)),因为它现在可以利用多种来源的配置信息:命令行、JSON 配置文件、用户机密等等。

背景

最初,J4JCommandLine 是“另一个命令行解析器”,因为我对 LL(1) 解析器感到好奇,并想写一个。

“LL(1)” 表示解析器从 **L**eft(左)到 right(右)扫描标记,并且只向前看 **1** 个标记(第二个“L”与遵循/构建树形数据结构中的“左侧”路径有关;我不太确定,因为我在 J4JCommandLine 中没有使用基于树的方法)。

J4JCommandLine 技术上不是 LL(1) 解析器,因为它在解析生成的标记之前会对其进行一些预处理。其中最主要的一步是将起始“引号”标记和结束“引号”标记之间的所有标记合并成一个单一的文本标记。

您可以通过查阅 Github 文档,了解更多关于 J4JCommandLine 的信息以及它如何解析命令行。但现在,对我来说,关键的里程碑是,在让它工作起来之后,我意识到有一种方法可以让它与 Microsoft 的 IConfiguration 系统协同工作。

您可以通过创建 ConfigurationBuilder 的实例,向其中添加(配置信息)提供程序,然后调用其 Build() 方法来创建 IConfiguration(技术上是 IConfigurationRoot)的实例。

var parser = testConfig.OperatingSystem.Equals( "windows", StringComparison.OrdinalIgnoreCase )
                ? Parser.GetWindowsDefault( logger: Logger )
                : Parser.GetLinuxDefault(logger: Logger);

_options = parser.Collection;

_configRoot = new ConfigurationBuilder()
                .AddJsonFile("some file path")
                .AddUserSecrets<ConfigurationTests>()
                .AddJ4JCommandLine(
                    parser,
                    out _cmdLineSrc, 
                    Logger )
                .Build();

在后台,会查询提供程序以获取属性名称和值的键/值对。当您从 IConfiguration 系统请求一个对象时,这些将用于初始化该对象。

var configObject = _configRoot.Get<SomeConfigurationObject>();

J4JCommandLine 的解析器通过其提供程序工作,将命令行文本转换为配置值。

Using the Code

J4JCommandLine 非常可配置,但基本用法非常简单,前提是默认设置适合您。您只需将提供程序添加到 ConfigurationBuilder 实例,调用 Build() 即可开始使用。

但是,这并不会导致您的命令行被解析……因为您必须告诉解析器它可以遇到的命令行选项是什么,它们的类型是什么,它们是否是必需的,等等。您可以通过将您的 configuration 对象的属性**绑定**到命令行标记来实现这一点。

以下是使用简单 configuration 对象的示例。

public class Configuration
{
    public int IntValue { get; set; }
    public string TextValue { get; set; }
}

在您的启动代码中,您会这样做:

var config = new ConfigurationBuilder()
    .AddJ4JCommandLineForWindows( out var options, out _ )
    .Build();

options!.Bind<Configuration, int>(x => Program.IntValue, "i")!
    .SetDefaultValue(75)
    .SetDescription("An integer value");

options.Bind<Configuration, string>(x => Program.TextValue, "t")!
    .SetDefaultValue("a cool default")
    .SetDescription("A string value");

options.FinishConfiguration();

现在,当您调用

var parsed = config.Get<Configuration>();

时,生成的 Configuration 对象将反映命令行参数。

还有一个 TryBind<>() 方法,您可以代替 Bind 使用,这样您就不必检查返回值是否为非 null(这表示绑定失败)。为了保持示例简单,我没有包含 null 检查代码。

支持哪些操作系统?

我试图让 J4JCommandLine“与操作系统无关”,因为如今 .NET 可以在非 Windows 平台上运行。坦率地说,我只在 Windows 上测试过它……但支持其他系统的逻辑在那里。 

您可以通过告诉它使用什么“词法元素”以及命令行键(例如 /x)是否区分大小写来控制 J4JCommandLine 的操作系统行为。在后台,这就是 AddJ4JCommandLineForWindows()AddJ4JCommandLineForLinux() 扩展方法所做的一部分:它们将这些特定于操作系统的参数设置为合理的默认值。

以下是默认值:

操作系统默认值
 
  词法元素 键区分大小写?
Windows 引号: " '
键前缀: /
分隔符: [空格] [制表符]
值前缀: =
Linux 引号: " '
键前缀: - --
分隔符: [空格] [制表符]
值前缀: =

引号定义应被视为单个块的文本元素。键前缀指示选项键的开始(例如 /x 或 -x)。分隔符分隔命令行上的标记。值前缀(我很少使用,因为常规分隔符似乎效果很好)将选项键链接到文本元素(例如 /x=abc)。

请记住,当存在多个有效的键前缀时(例如 - 和 --),可以使用任意一个前缀与任何键。因此,从 J4JCommandLine 来看,-a、--ALongerKey、-ALongerKey 和 --a 都是有效的。这并非“Linux 方式”,但我还没有弄清楚如何“正确”地做到这一点。

您可以将属性绑定到哪些类型?

J4JCommandLine 不能将命令行选项绑定到任何随机的 C# 类型。它必须能够将一个或多个文本值转换为目标类型的实例,这需要存在一个转换方法。在这方面,它与 IConfiguration API 本身没有区别,后者依赖于 C# 内置的 Convert 类将文本转换为类型实例。

但是,J4JCommandLine 在转换方面是可扩展的;您可以定义自己的转换器方法,将文本转换为自定义类型的实例。有关详细信息,请查阅 Github 文档……并请记住,我代码库的那部分未经充分测试。

实际上,我怀疑您需要定义自己的转换器。默认情况下,J4JCommandLine 可以转换 C# 内置 Convert 类中具有相应方法的任何类型(实际上,J4JCommandLine 只是包装了这些方法以满足其特定需求)。

绑定到集合

除了能够绑定到大多数常用的配置类型外,J4JCommandLine 还可以绑定到这些类型的某些集合类型。

  • 支持类型的数组(例如 string[]
  • 支持类型的列表(例如 List<string>

IConfiguration 系统还允许您绑定到 Dictionary。但我还没有弄清楚如何将其用于命令行。

可绑定内容还有另一个不太明显的限制。J4JCommandLine 可以直接绑定到 enum,甚至是 flagged enum(即用 [Flag] 属性标记的 enum)。但它不能绑定到 flagged enum 的集合。这是因为它无法判断对应于 enum 值的文本标记序列是应合并成单个 OR 结果,还是应被视为 unconcatenated enum 的集合。

嵌套属性和公共无参构造函数

J4JCommandLineIConfiguration 一样,要求配置对象和要绑定的属性通常必须具有 public parameterless constructors。这是因为 IConfiguration API 必须能够在内部创建实例,而无需了解实际如何执行此操作。

但是,该要求有一个例外:如果您的配置对象的构造函数逻辑负责初始化属性,那么这些属性就不需要 public parameterless constructors。例如:

public class EmbeddedTargetNoSetter
{
    public BasicTargetParameteredCtor Target1 { get; } = new( 0 );
    public BasicTargetParameteredCtor Target2 { get; } = new( 0 );
}

public class BasicTargetParameteredCtor
{
    private readonly int _value;

    public BasicTargetParameteredCtor( int value )
    {
        _value = value;
    }

    public bool ASwitch { get; set; }
    public string ASingleValue { get; set; } = string.Empty;
    public List<string> ACollection { get; set; } = new();
    public TestEnum AnEnumValue { get; set; }
    public TestFlagEnum AFlagEnumValue { get; set; }
}

即使类型 BasicTargetParameteredCtor 没有 public parameterless constructor,您仍然可以绑定到 Target1Target2 属性,因为它们是由 EmbeddedTargetNoSetter 的构造函数逻辑初始化的(在这种情况下,是通过那些 new() 调用隐式实现的)。

此示例还突出了 J4JCommandLine 的另一项功能:您可以绑定到嵌套属性。

Bind<EmbeddedTargetNoSetter, bool>( _options!, x => x.Target1.ASwitch, testConfig );
Bind<EmbeddedTargetNoSetter, string>( _options!, x => x.Target1.ASingleValue, testConfig );
Bind<EmbeddedTargetNoSetter, TestEnum>( _options!, x => x.Target1.AnEnumValue, testConfig );
Bind<EmbeddedTargetNoSetter, TestFlagEnum>
    ( _options!, x => x.Target1.AFlagEnumValue, testConfig );
Bind<EmbeddedTargetNoSetter, List<string>>
    ( _options!, x => x.Target1.ACollection, testConfig );

提供帮助

用户在命令行中提供无效参数是很常见的。让他们知道他们应该输入什么很重要。J4JCommandLine 通过提供一个接口来解决此问题,您可以使用该接口显示您与绑定选项关联的帮助信息。您可以这样做:

options!.Bind<Program, int>(x => Program.IntValue, "i")!
    .SetDefaultValue(75)
    .SetDescription("An integer value")
    .IsOptional();

options.Bind<Program, string>(x => Program.TextValue, "t")!
    .SetDefaultValue("a cool default")
    .SetDescription("A string value")
    .IsRequired();

options.FinishConfiguration();

var help = new ColorHelpDisplay(new WindowsLexicalElements(), options);
help.Display();

您通过调用其扩展方法来描述一个选项。这些方法允许您设置默认值,描述其用途,并指示它是必需的还是可选的(默认是可选的)。

当然,通常情况下,您不会像此代码片段那样显示帮助信息。您只会显示帮助信息,如果用户请求了(例如,通过设置命令行选项),或者如果创建的配置对象有问题。

请注意,J4JCommandLine 不会自动显示帮助信息当它检测到问题时。这取决于您。

J4JCommandLine 程序集包含一个普通的帮助显示引擎,名为 DefaultHelpDisplay。但我通常更喜欢使用 Github 存储库中包含的 ColorfulHelp 库提供的更具色彩的显示。无论您使用哪个(或者自己编写),都必须使用有关正在使用的“词法元素”(即特殊字符,如 / ? = ")以及您定义的选项集合的信息来配置帮助系统。这样,帮助系统就可以提取并显示有关选项应如何指定/格式化、它们的默认值、它们是否是必需的等信息。

日志记录

J4JCommandLine 使用我的配套库 J4JLogger 来记录问题。日志记录功能是可选的——默认情况下,不记录任何内容。但是,如果您想了解 J4JCommandLine 内部发生的情况,包含它是一个好主意。

有关更多信息,请参阅 CodeProject 关于 J4JLogger 的文章 或其 github 存储库

关注点

当我开始深入研究 IConfiguration 系统的工作原理时,我期望找到类似于我构建 J4JCommandLine 转换功能的方式。有趣的是,我没有找到。相反,看起来 IConfiguration API 仅依赖于内置的 Convert 类,如果不存在转换方法,则会失败。

同样,IConfiguration 系统以非结构化的方式执行其检查(例如,检查属性类型是否具有 public 参数less 构造函数)。似乎没有一组(可能可扩展的)规则。相反,这些检查只是硬编码到代码库中。

历史

  • 2021年10月1日:在 CodeProject 上发布初始版本
© . All rights reserved.