C#/.NET 控制台参数解析器和验证(使用 ConsoleCommon)






4.83/5 (36投票s)
.NET 库,用于自动验证和转换控制台输入参数
引言
ConsoleCommon 是一个 .NET 库,提供了一组旨在与控制台应用程序一起使用的辅助工具。这些工具侧重于自动化参数类型转换和验证,以及生成帮助文本。
可在 Nuget 上获取:搜索 ConsoleCommon
背景
为了更好地理解这些工具的作用,请考虑以下场景
程序员创建一个控制台应用程序,用于搜索数据库中的特定客户。该控制台应用程序需要以下参数:名字、姓氏和出生日期。为了调用控制台应用程序,用户将在命令行中输入类似以下内容:
 CustomerFinder.exe Yisrael Lax 11-28-1987
在代码中,应用程序首先必须解析用户的输入参数。在健壮的设计中,应用程序随后会创建一个具有名字、姓氏和出生日期属性的 customer 对象。然后,它会将每个属性设置为从用户那里传递过来的值。立即出现几个问题。第一个问题是,应用程序将不得不要求用户按照预设的顺序(在我们的示例中,先是名字,然后是姓氏,最后是出生日期)传递每个参数。接下来,为了健壮性,应用程序将在设置新 customer 对象上的字段之前进行一些类型验证,以避免类型不匹配错误。例如,如果用户输入了无效的日期,应用程序应该在尝试设置 customer 对象上的日期字段之前就知道这一点,并抛出适当的错误,并附带描述性消息,以提醒用户它遇到的问题。接下来,应用程序将执行其他验证检查。例如,假设应用程序认为输入一个设定的未来日期是无效的。这样做应该会引发错误。
ConsoleCommon 工具集通过自动类型转换和错误验证解决了所有这些问题。此外,ConsoleCommon 还提供了一些自动帮助文本生成功能。一个好的控制台应用程序应该始终附带良好的帮助文本,通常在用户输入类似以下内容时显示:
CustomerFinder.exe /?
创建、正确格式化和维护这些帮助文本通常很麻烦。ConsoleCommon 使此操作变得容易,如下文所示。
基本实现示例
ConsoleCommon 通过实现 abstract ParamsObject 类来工作。此类将包含代表应用程序输入参数的强类型属性。这些属性是即时创建的,并且为了指示 ConsoleCommon 库这些是参数,它们必须用 Switch 属性进行装饰。对于 CustomerFinder.exe 应用程序,我们将实现一个 CustomerParamsObject。首先,引用 ConsoleCommon .dll 并为 ConsoleCommon 创建一个 using 语句。然后,实现该类并创建一些基本开关属性:
    using System;
    using ConsoleCommon;
    namespace ConsoleCommonExplanation
    {
        public class CustomerParamsObject : ParamsObject
        {
            public CustomerParamsObject(string[] args)
                : base(args)
            {    
          
            }
            [Switch("F")]
            public string firstName { get; set; }
            [Switch("L")]
            public string lastName { get; set; }
            [Switch("DOB")]
            public DateTime DOB { get; set; }
        }
    }
Switch 属性中的 string 值("F"、"L" 和 "DOB")指定了应用程序要求用户如何传递参数。ConsoleCommon 不要求用户按特定顺序传递参数,而是让用户使用开关(以任何顺序)传递参数,例如在以下示例中:
    CustomerFinder.exe /F:Yisrael /DOB:11-28-1987 /L:Lax
每个“switch”是参数和开关名称的组合。用户使用 /[SwitchName]:[Argument] 格式指定开关。在上面的示例中,"/F:Yisrael" 是一个单独的开关。ConsoleCommon 然后会搜索输入参数中与 CustomerParamsObject 上定义的开关属性匹配的开关值,然后进行类型检查,执行一些自动验证,如果一切都通过,则将 CustomerParamsObject 上的开关属性设置为输入参数。为了使用 CustomerParamsObject,我们的客户端将如下所示:
    using System;
    using ConsoleCommon;
    namespace ConsoleCommonExplanation
    {
        class Program
        {
            static void Main(string[] args)
            {
                try
                {
                     //This step will automatically cast the string args to a strongly typed object:
                    CustomerParamsObject _customer = new CustomerParamsObject(args);
                    //This step will do type checking and validation
                    _customer.CheckParams();
                    string _fname = _customer.firstName;
                    string _lname = _customer.lastName;
                    DateTime _dob = _customer.DOB;
                }
                catch(Exception ex)
                {
                    Console.WriteLine(ex.Message);
                }
            }
        }
    }
开关属性也可以使用它们的属性名称来指定,如下所示:
    CustomerFinder.exe /FirstName:Yisrael /DOB:11-28-1987 /LastName:Lax
从 v4.0 开始,在 SwitchAttribute 中指定开关名称不再是必需的。如果未使用,调用者需要使用属性名称来指定开关。
基本验证
所有验证在调用 CheckParams() 时执行。始终将此方法调用包装在 try… catch 中,因为验证错误会导致抛出描述性异常。实例化 ParamsObject 时,由类型不匹配(例如,用户为 "DOB" 开关输入了无效日期)和无效开关引起的错误将被排队。然后,当调用 CheckParams() 时,会抛出这些错误。
可选验证
除了类型检查和开关验证之外,ParamsObject 还提供了额外的“可选”验证。与基本验证(在实例化时进行验证检查,但仅在调用 CheckParams() 时执行)不同,可选验证在调用 CheckParams() 时会同时进行检查和执行。
必需
可以通过向属性的 SwitchAttribute 构造函数传递附加参数来将 switch 属性标记为“必需”或“可选”。默认情况下,开关不是必需的。在以下示例中,"firstName" 和 "lastName" 是必需的,而 "DOB" 是可选的:
    using System;
    using ConsoleCommon;
    
    namespace ConsoleCommonExplanation
    {
        public class CustomerParamsObject : ParamsObject
        {
            public CustomerParamsObject(string[] args)
                : base(args)
            {
    
            }
            [Switch("F", true)]
            public string firstName { get; set; }
            [Switch("L", true)]
            public string lastName { get; set; }
            [Switch("DOB", false)]
            public DateTime DOB { get; set; }
        }
    }
用户现在可以选择是否输入出生日期,但至少必须输入名字和姓氏。为了让 ConsoleCommon 检查必需字段和其他下文介绍的验证类型,请使用以下命令:
    _customer.CheckParams();
始终将 CheckParams() 调用包装在 try… catch 中,因为任何验证错误都会导致抛出描述性异常。
受限值集
要限制单个参数可以设置的值集,请将 SwitchAttribute 中的 "switchValues" 构造函数参数设置为值列表。例如,假设您只想允许用户搜索姓氏为 "Smith"、"Johnson" 或 "Nixon" 的人。修改 CustomerParamsObject 如下:
    using System;
    using ConsoleCommon;
    
    namespace ConsoleCommonExplanation
    {
        public class CustomerParamsObject : ParamsObject
        {
            public CustomerParamsObject(string[] args)
                : base(args)
            {
    
            }
            [Switch("F", true)]
            public string firstName { get; set; }
            [Switch("L", true , -1,  "Smith", "Johnson", "Nixon")]
            public string lastName { get; set; }
            [Switch("DOB", false)]
            public DateTime DOB { get; set; }
        }
    }
请注意 lastName 属性上修改的 SwitchAttribute。如果客户输入的姓氏不是上面定义的受限列表中的一个,ConsoleCommon 将在调用 CheckParams() 时立即抛出错误。'-1' 值用于将默认序数值设置为其默认值,因此 ConsoleCommon 将忽略它。稍后我们将解释默认序数。
建立受限列表的另一种方法是使用 enum 类型作为您的开关属性。ConsoleCommon 将自动将 enum 属性的值集限制为 enum 值的名称。请注意,在下面的示例中,我们创建了一个新的 enum LastNameEnum,并将 lastName 属性的类型从 string 更改为 LastNameEnum:
    using System;
    using ConsoleCommon;
    
    namespace ConsoleCommonExplanation
    {
        public enum LastNameEnum
        {
            Smith,
            Johnson,
            Nixon
        }
        public class CustomerParamsObject : ParamsObject
        {
            public CustomerParamsObject(string[] args)
                : base(args)
            {
    
            }
            [Switch("F", true)]
            public string firstName { get; set; }
            [Switch("L", true)]
            public LastNameEnum lastName { get; set; }
            [Switch("DOB", false)]
            public DateTime DOB { get; set; }
        }
    }
自定义验证
如果除了内置验证之外还需要其他验证,您可以实现 ParamsObject 实现上的 GetParamExceptionDictionary() 方法。这允许您添加在调用 CheckParams() 时调用的附加验证检查。此方法需要返回一个 Dictionary。此 Dictionary 中的每个条目包含一个 Func<bool>,其中包含一个验证检查,以及一个 string,其中包含在验证检查失败时返回的异常消息。Func 用于其延迟处理功能。当调用 CheckParams() 时,ConsoleCommon 会遍历从 GetParamExceptionDictionary() 方法返回的 Dictionary,处理每个 Func<bool>,如果任何一个为 false,则会抛出带有 Func 配对的 string 消息作为异常消息的错误。在下面的示例中,我们添加了一个异常检查,以确保用户不会为出生日期字段输入未来日期:
    public override Dictionary<Func<bool>, string> GetParamExceptionDictionary()
    {
        Dictionary<Func<bool>, string> _exceptionChecks = new Dictionary<Func<bool>, string>();
    
        Func<bool> _isDateInFuture = new Func<bool>( () => DateTime.Now <= this.DOB );
    
        _exceptionChecks.Add(_isDateInFuture,
                             "Please choose a date of birth that is not in the future!");
        return _exceptionChecks;
    }
如果用户确实这样做,当调用 CheckParams() 时,将向调用者返回一条消息为“Please choose a date of birth that is not in the future!”(请选择一个不是未来的出生日期!)的异常。
自动生成帮助文本
用户通过将 "/?"、"/help" 或 "help" 作为第一个参数传递给应用程序来触发帮助文本的打印。

实现帮助文本生成的最基本方法是重写 ParamsObject 实现上的 GetHelp() 方法。在本例中,那就是 CustomerParamsObject。
    public class CustomerParamsObject : ParamsObject
    {
        public CustomerParamsObject(string[] args)
            : base(args)
        {
        }
        #region Switch Properties
        [Switch("F", true)]
        public string firstName { get; set; }
        [Switch("L", true)]
        public LastNameEnum lastName { get; set; }
        [Switch("DOB", false)]
        public DateTime DOB { get; set; }
        #endregion
        public override Dictionary<Func<bool>, string> GetParamExceptionDictionary()
        {
            Dictionary<Func<bool>, string> _exceptionChecks = new Dictionary<Func<bool>, string>();
            Func<bool> _isDateInFuture = new Func<bool>( () => DateTime.Now <= this.DOB );
            _exceptionChecks.Add(_isDateInFuture,
                                 "Please choose a date of birth that is not in the future!");
            return _exceptionChecks;
        }
        public override string GetHelp()
        {
            return 
             "\n-----------This is help-----------\n\nBe smart!\n\n----------------------------------";
        }
    }
}
然后,客户端将这样调用 GetHelp() 方法:
    //CustomerParamsObject _customer = new CustomerParamsObject(args)
	String _helptext = _customer.GetHelp();
	Console.Writeline(_helptext);
但是,这种方法没有利用 ConsoleCommon 在帮助文本生成方面更自动化的功能。
帮助文本属性
ConsoleCommon 提供了一些自动格式化功能来帮助创建帮助文本。要利用这些功能,请不要重写 GetHelp() 方法。而是,在 ParamsObject 实现上创建 string 属性,并用 HelpTextAttribute 装饰它们。这些属性返回一个帮助文本组件。请注意下面新的 Description 和 ExampleText 帮助文本属性:
    using System;
    using ConsoleCommon;
    using System.Collections.Generic;
    
    namespace CustomerFinder
    {
        public enum LastNameEnum
        {
            Smith,
            Johnson,
            Nixon
        }
        public class CustomerParamsObject : ParamsObject
        {
            public CustomerParamsObject(string[] args)
                : base(args)
            {
    
            }
    
            #region Switch Properties
            [Switch("F", true)]
            public string firstName { get; set; }
            [Switch("L", true)]
            public LastNameEnum lastName { get; set; }
            [Switch("DOB", false)]
            public DateTime DOB { get; set; }
            #endregion
    
            public override Dictionary<Func<bool>, string> GetParamExceptionDictionary()
            {
                Dictionary<Func<bool>, string> _exceptionChecks = new Dictionary<Func<bool>, string>();
    
                Func<bool> _isDateInFuture = new Func<bool>( () => DateTime.Now <= this.DOB );
    
                _exceptionChecks.Add(_isDateInFuture,
                                     "Please choose a date of birth that is not in the future!");
                return _exceptionChecks;
            }
    
            [HelpText(0)]
            public string Description
            {
                get { return "Finds a customer in the database."; }
            }
            [HelpText(1, "Example")]
            public string ExampleText
            {
                get { return "This is an example: CustomerFinder.exe Yisrael Lax 11-28-1987"; }
            }
        }
    }
HelpTextAttribute 的构造函数必须指定一个序数值,该序数值决定了当用户请求帮助时,该帮助文本组件出现的位置。默认情况下,帮助文本组件的前面是属性名称和一个冒号。但是,可以通过在 HelpTextAttribute 构造函数中指定名称来覆盖帮助文本组件的名称。上面的 Description 属性使用了属性名称,而 ExampleText 属性覆盖了此默认行为,而是使用了名称 "Example"。结果如下所示:

包含的帮助文本属性
有几个包含的帮助文本属性供您选择使用。这些是 Usage 属性和 SwitchHelp 属性。要使用其中任何一个,请重写它们,并像使用任何其他帮助文本属性一样用 HelpTextAttribute 装饰它们,但返回基类的实现而不是自定义实现。
        [HelpText(2)]
        public override string Usage
        {
            get { return base.Usage; }
        }
Usage 属性
此帮助文本属性将打印出应用程序的调用示例。
USAGE:                 CustomerFinder.exe /F:"firstName" /L:"lastName" /DOB:"DOB"
SwitchHelp 属性
此帮助文本属性为每个开关属性/参数打印帮助文本,并且需要额外的工作才能打印有意义的内容。要实现此目的,请用 SwitchHelpTextAttribute 装饰每个开关属性,并将一些基本帮助文本传递给该属性的构造函数。在以下示例中,请注意装饰开关属性的新属性以及新的 Description 和 Usage 属性:
    using System;
    using ConsoleCommon;
    using System.Collections.Generic;
    
    namespace CustomerFinder
    {
        public enum LastNameEnum
        {
            Smith,
            Johnson,
            Nixon
        }
        public class CustomerParamsObject : ParamsObject
        {
            public CustomerParamsObject(string[] args)
                : base(args)
            {
    
            }
    
            #region Switch Properties
            [Switch("F", true)]
            [SwitchHelpText("First name of customer.")]
            public string firstName { get; set; }
            [Switch("L", true)]
            [SwitchHelpText("Last name of customer.")]
            public LastNameEnum lastName { get; set; }
            [Switch("DOB", false)]
            [SwitchHelpText("The date of birth of customer")]
            public DateTime DOB { get; set; }
            #endregion
    
            public override Dictionary<Func<bool>, string> GetParamExceptionDictionary()
            {
                Dictionary<Func<bool>, string> _exceptionChecks = new Dictionary<Func<bool>, string>();
    
                Func<bool> _isDateInFuture = new Func<bool>( () => DateTime.Now <= this.DOB );
    
                _exceptionChecks.Add(_isDateInFuture,
                                     "Please choose a date of birth that is not in the future!");
                return _exceptionChecks;
            }
    
            [HelpText(0)]
            public string Description
            {
                get { return "Finds a customer in the database."; }
            }
            [HelpText(1, "Example")]
            public string ExampleText
            {
                get { return "This is an example: CustomerFinder.exe Yisrael Lax 11-28-1987"; }
            }
            [HelpText(2)]
            public override string Usage
            {
                get { return base.Usage; }
            }
            [HelpText(3,"Parameters")]
            public override string SwitchHelp
            {
                get { return base.SwitchHelp; }
            }
        }
    }
输出是:

如您所见,SwitchHelp 帮助文本属性提供了相当多的您没有明确指定的信息,包括该参数是否是必需的、是否有默认序数以及该属性允许的值列表。
从 v4.0 开始,您现在可以直接在 SwitchAttribute 中添加帮助文本,而不是向属性添加额外的 SwitchHelpTextAttribute。例如:
 public class CustomerParamsObject : ParamsObject 
 { 
      public CustomerParamsObject(string[] args) : base(args) { } 
      //...
      #region Switch Properties 
      [Switch("F", true, helptext: "First name of customer.")]
      public string firstName { get; set; } 
      
      //...
      [HelpText(3,"Parameters")] 
      public override string SwitchHelp => base.SwitchHelp;
}
需要时获取帮助
在大多数情况下,您只希望在用户请求时才向控制台打印帮助。要做到这一点,请使用 GetHelpIfNeeded() 方法,该方法如果用户传递了一个帮助开关("/?"、"/help" 或 "help")作为第一个参数,则返回一个包含帮助文本的 string,否则返回 string.Empty。一个简单的实现如下所示:
    using System;
    
    namespace CustomerFinder
    {
        class Program
        {
            static void Main(string[] args)
            {
                try
                {
                    //This step will do type validation
                    //and automatically cast the string args to a strongly typed object:
                    CustomerParamsObject _customer = new CustomerParamsObject(args);
                    //This step does additional validation
                    _customer.CheckParams();
                    //Get help if user requested it
                    string _helptext = _customer.GetHelpIfNeeded();
                    //Print help to console if requested
                    if(!string.IsNullOrEmpty(_helptext))
                    {
                        Console.WriteLine(_helptext);
                        Environment.Exit(0);
                    }
                    string _fname = _customer.firstName;
                    string _lname = _customer.lastName.ToString();
                    string _dob = _customer.DOB.ToString();
                }
                catch(Exception ex)
                {
                    Console.WriteLine(ex.Message);
                }
            }
        }
    }
默认序数
默认序数是为 switch 属性指定的参数,这些参数允许您的控制台应用程序使用有序参数而不是开关来调用。此功能为 ConsoleCommon 用于已经构建的控制台应用程序,并且有外部批处理脚本和应用程序当前使用非开关有序参数(例如)调用它时提供了向后兼容性。
    CustomerFinder.exe Yisrael Lax 11-28-1987
为了实现此功能,请在每个开关属性的 SwitchAttribute 中添加一个序数值:
        #region Switch Properties
        [Switch("F", true,1)]
        [SwitchHelpText("First name of customer")]
        public string firstName { get; set; }
        [Switch("L", true,2)]
        [SwitchHelpText("Last name of customer")]
        public LastNameEnum lastName { get; set; }
        [SwitchHelpText("The date of birth of customer")]
        [Switch("DOB", false,3)]
        public DateTime DOB { get; set; }
        #endregion
您会注意到帮助文本也发生了变化。它现在按默认序数的顺序列出了 switch 属性,并附带一些关于默认序数的附加消息。
默认序数可能会变得复杂,因为允许使用默认序数的参数和使用开关的参数混合调用应用程序。使事情更加复杂的是,完全允许具有默认序数的 switch 属性和不具有默认序数的属性。存在一些复杂的逻辑来确定在进行任何混合搭配时的特定要求,我们在此不详细介绍。但是,其中很多是直观的,玩弄混合搭配可以帮助您自己确定此逻辑。
使用 Type Type 开关属性
这不是拼写错误。Switch 属性可以设为任何类型。但是,特别支持的类型包括所有原始类型、DateTime、enum、Type、System.Security.SecureStrings、KeyValuePairs 以及实现 IConvertible 的任何类型。所有其他类型将使用类型的默认 ToString() 方法来尝试将参数匹配到属性的值。这可能会导致数据错误或意外的异常。
Type 类型有特定的实现。使用 Type 类型的原因是,用户可以通过名称或在装饰该类型的特定类型属性中指定的描述符来指定应用程序关联的任何程序集中的任何类型。例如,假设我们有两个类:
    class PizzaShopCustomer{ }
    class BodegaCustomer { }
现在,在我们的 ParamsObject 实现上,我们添加了一个新属性 "CustomerType":
        #region Switch Properties
        [Switch("F", true,1)]
        [SwitchHelpText("First name of customer")]
        public string firstName { get; set; }
        [Switch("L", true,2)]
        [SwitchHelpText("Last name of customer")]
        public LastNameEnum lastName { get; set; }
        [SwitchHelpText("The date of birth of customer")]
        [Switch("DOB", false,3)]
        public DateTime DOB { get; set; }
        [Switch("T",false)]
        public Type CustomerType { get; set; }
        #endregion
用户现在可以通过 /T 开关指定 Customer 类的类型:

(请注意,我们在上面的示例中混合使用了开关和默认序数。我们还将 "Lax" 添加到了 LastNameEnum 中)。此实现有一个恼人的方面:在我们的应用程序示例中,用户必须输入类名,这并不一定对用户友好(例如,用户可能无法弄清楚为什么他们必须在客户类型末尾键入 "customer" 这个词)。为了解决这个问题,类可以被装饰一个 ConsoleCommon.TypeParamAttribute,该属性指定一个友好的名称:
    [TypeParam("pizza shop")]
    public class PizzaShopCustomer { }
    [TypeParam("bodega")]
    public class BodegaCustomer { }
现在,用户在调用应用程序时可以使用上面指定的友好名称:

Type 类型开关属性最适合与受限值功能结合使用。当这样做时,此属性与动态工厂类一起使用会非常有用。
使用数组
只要其底层类型受支持,数组就可以用作开关属性。要传递带开关的数组值集,请将整个值集括在双引号中,并用逗号分隔每个值:
public class CustomerParamsObject : ParamsObject
{
	//...
	[Switch("NN")]
	public string[] Nicknames { get; set; }
}
然后,从控制台,我们可以这样传递:
CustomerFinder.exe Yisrael Lax /NN:"Codemaster,Devguru"
使用枚举
用 FlagsAttribute 装饰的 Enum 可以用逗号分隔的列表指定其值。这将导致这些值被按位 OR 运算,因此请确保没有两个 enum 值可以重叠。这是通过在指定 enum 值时使用 2 的幂来完成的。请参阅以下示例:
[Flags]
public enum CustomerInterests
{
	Pizza = 1,
	Crabcakes = 2,
	Parachuting = 4,
	Biking = 8
}
public class CustomerParamsObject : ParamsObject
{
	//...
	[Switch("Int")]
	public CustomerInterests Interests { get; set; }
}
And, from the console, we can pass in something like this:
CustomerFinder.exe Yisrael Lax /Int:"Crabcakes,Biking"
使用 KeyValuePairs
可以通过指定由冒号分隔的键值对(例如 "Key:Value")来使用 KeyValuePair。在需要填充任意变量的情况下,使用 KeyValuePair 数组可能很有用,例如在下面的示例中:
public class CustomerParamsObject : ParamsObject
{
	//...
	[Switch("Pets")]
	public KeyValuePair<string,int>[] PetCount { get; set; }
}
然后,从控制台,我们可以这样传递:
CustomerFinder.exe Yisrael Lax /Pets:"dog:5,cat:3,bird:1"
其他选项
ConsoleCommon 是相当可定制的。可以实现自定义类型解析、自定义帮助处理和自定义开关解析。但是,对于那些只想通过默认解析和处理来定制某些选项的实现者来说,存在一些功能。
可以通过以下方式自定义开关起始字符、开关结束字符和合法的开关名称正则表达式:
public class CustomerParamsObject : ParamsObject
{
	//...
	public override SwitchOptions Options
	{
		get
		{
			return new SwitchOptions(switchStartChars: new List<char> { '-','/'}, 
			switchEndChars: new List<char> { ':', '-'}, 
                                        switchNameRegex: "[_A-Za-z]+[_A-Za-z0-9]*");
		}
	}
}
在上面的示例中,所有以下方法都可以用来调用程序:
CustomerFinder.exe /F:Yisrael /L:Lax
CustomerFinder.exe -F:Yisrael -L:Lax
CustomerFinder.Exe -F-Yisrael -L-Lax
可以通过以下方式自定义请求帮助的命令:
public class CustomerParamsObject : ParamsObject
{
	//...
	public override List<string> HelpCommands
	{
		get
		{
			return new List<string> { "/?", "help", "/help"};
		}
	}
}
自定义解析
ConsoleCommon 附带了一套默认类型解析器,可以处理所有原始类型、DateTime、Type、Enum、Array、SecureString、Nullable、Bool(允许使用附加值来表示“true”和“false”)。从 3.0 版开始,实现者可以覆盖任何一个打包的类型解析器的解析,或者添加一个非包含类型的解析,而无需覆盖所有默认解析。以下是如何覆盖 DateTime 类型处理的示例:
public class DayFirstDashOnlyDateTimeParser : TypeParserBase<DateTime>
{
     public override object Parse(string toParse, Type typeToParse, ITypeParserContainer parserContainer)
     {
          CultureInfo _culture = CultureInfo.CreateSpecificCulture("");
          return DateTime.ParseExact(toParse, "dd-MM-yyyy", _culture);
     }
}
public class CustomerParamsObject : ParamsObject
{
    //...
    [Switch("Bday")]
    public DateTime Birthday { get; set; }
    
    protected override ITypeParserContainer TypeParser => new TypeParserContainer(overwriteDupes: true, parserContainer: new DefaultTypeContainer(), typeParsers: new DateTimeParser());
}
在上面的示例中,TypeParser 属性被一个名为 TypeParserContainer 的新类覆盖。使用了 TypeParserContainer 的构造函数重载,该重载指定 DefaultTypeContainer 中的所有类型解析器都将被添加到新的 TypeParserContainer 中。overwriteDupes 参数指定了新 ITypeParser 中处理已被 ITypeParser 在基本容器中处理的类型的任何类型都将覆盖基本容器中类似的 ITypeParser。
当 ParamsObject 需要解析开关属性时,它会向 TypeParserContainer 发送请求,以返回处理该属性类型的解析器。TypeParserContainer 将返回一个处理最派生类型的解析器,该类型与属性的类型匹配。例如,如果 TypeParserContainer 同时具有 DateTimeParser 和 ObjectParser,而 ParamsObject 想解析 DateTime 属性,则将返回 DateTimeParser。如果只有一个 ObjectParser,则会返回它。
非参数解析
从 v4.0 开始,ConsoleCommon 能够解析包含开关语句的字符串,而不是数组。要使用此功能,请使用接受字符串的构造函数重载:
 class Program 
 { 
     static void Main(string[] args) 
     { 
        try { 
            string _argText = "/F:Yisrael /L:Lax /Bday:11/28/1987";
            CustomerParamsObject _customer = new CustomerParamsObject(_argText); 
          } catch Exception ex { Console.WriteLine(ex.Message); }
     }
 }
ConsoleCommon 首先将字符串拆分成参数,然后按正常方式进行。但是要小心。ConsoleCommon 不会像 Windows Shell 或 Command Prompt 那样将字符串拆分成参数。双引号和大多数空格被视为命令文本的一部分,并且不会影响参数的分隔方式。在开关键之后直到下一个开关键或命令文本结束的所有字符都将被视为开关值的一部分,但运行的和尾随的空格除外。使用此重载有助于处理预计包含大量空格的开关值的情况。它使开发人员无需将开关参数(键和值)括在引号中。
ConsoleCommon 还有一个可以从环境中读取命令文本的重载。但是请注意,此重载期望环境的当前目录与应用程序的调用目录相同。另外,显然,此重载仅在 ConsoleCommon 用于解析程序启动时传递的参数时才有用。
完整示例代码
using System;
using ConsoleCommon;
using System.Collections.Generic;
namespace CustomerFinder
{
    class Program
    {
        static void Main(string[] args)
        {
            try
            {
                <code>args</code> = new string[] { "Yisrael", "Lax", 
                "/Int:Pizza,Parachuting", "/Pets:dog:5,cat:3,bird:1" };//"/T:pizza shop", 
                                 // "/DOB:11-28-1987", "/Case", "/Regions:Northeast,Central" };
                //This step will do type validation
                //and automatically cast the string args to a strongly typed object:
                CustomerParamsObject _customer = new CustomerParamsObject(args);
                //This step does additional validation
                _customer.CheckParams();
                //Get help if user requested it
                string _helptext = _customer.GetHelpIfNeeded();
                //Print help to console if requested
                if (!string.IsNullOrEmpty(_helptext))
                {
                    Console.WriteLine(_helptext);
                    Environment.Exit(0);
                }
                string _fname = _customer.firstName;
                string _lname = _customer.lastName.ToString();
                string _dob = _customer.DOB.ToString("MM-dd-yyyy");
                string _ctype = _customer.CustomerType == null ? "None" : _customer.CustomerType.Name;
                string _caseSensitive = _customer.CaseSensitiveSearch ? "Yes" : "No";
                string _regions = _customer.CustRegions == null ? "None" : 
                string.Concat(_customer.CustRegions.Select(r => "," + r.ToString())).Substring(1);
                string _interests = _customer.Interests.ToString();
                string _petCount = _customer.PetCount == null || _customer.PetCount.Length == 0 ? 
                "None" : string.Concat(_customer.PetCount.Select
                             (pc => ", " + pc.Key + ": " + pc.Value)).Substring(2);
                Console.WriteLine();
                Console.WriteLine("First Name: {0}", _fname);
                Console.WriteLine("Last Name: {0}", _lname);
                Console.WriteLine("DOB: {0}", _dob);
                Console.WriteLine("Customer Type: {0}", _ctype);
                Console.WriteLine("Case sensitive: {0}", _caseSensitive);
                Console.WriteLine("Regions: {0}", _regions);
                Console.WriteLine("Interests: {0}", _interests);
                Console.WriteLine("Pet count: {0}", _petCount);
            }
            catch (Exception ex)
            {
                Console.WriteLine(ex.Message);
            }
        }
    }
    [TypeParam("pizza shop")]
    public class PizzaShopCustomer { }
    [TypeParam("bodega")]
    public class BodegaCustomer { }
    public enum LastNameEnum
    {
        Smith,
        Johnson,
        Nixon,
        Lax
    }
    public class CustomerParamsObject : ParamsObject
    {
        public CustomerParamsObject(string[] args)
            : base(args)
        {
        }
        #region Switch Properties
        [Switch("F", true,1)]
        [SwitchHelpText("First name of customer")]
        public string firstName { get; set; }
        [Switch("L", true,2)]
        [SwitchHelpText("Last name of customer")]
        public LastNameEnum lastName { get; set; }
        [SwitchHelpText("The date of birth of customer")]
        [Switch("DOB", false,3)]
        public DateTime DOB { get; set; }
        [Switch("T",false,4, "bodega", "pizza shop")]
        public Type CustomerType { get; set; }
        #endregion
        public override Dictionary<Func<bool>, string> GetParamExceptionDictionary()
        {
            Dictionary<Func<bool>, string> _exceptionChecks = new Dictionary<Func<bool>, string>();
            Func<bool> _isDateInFuture = new Func<bool>( () => DateTime.Now <= this.DOB );
            _exceptionChecks.Add(_isDateInFuture,
                                 "Please choose a date of birth that is not in the future!");
            return _exceptionChecks;
        }
        [HelpText(0)]
        public string Description
        {
            get { return "Finds a customer in the database."; }
        }
        [HelpText(1, "Example")]
        public string ExampleText
        {
            get { return "This is an example: CustomerFinder.exe Yisrael Lax 11-28-1987"; }
        }
        [HelpText(2)]
        public override string Usage
        {
            get { return base.Usage; }
        }
        [HelpText(3,"Parameters")]
        public override string SwitchHelp
        {
            get { return base.SwitchHelp; }
        }
    }
}
结论
我通过将我曾在不同地方使用的代码片段组合在一起,然后将其泛化以便广泛使用,当然还添加了一些额外的功能来创建这个库。正如我们编码人员所知,有用的工具通常从一个非常有机、类似的过程发展而来,有时它们几乎是偶然发生的。话虽如此,这个库确实是即时构建并组合了各种代码片段,所以肯定还有改进的空间。请随时扩展、修改和改进它,以便您也能为改进我们整体的工具集做出贡献。祝您编码愉快!
历史
- 版本 1,发布于 2017 年 3 月 31 日
- 版本 5,发布于 2018 年 4 月 12 日
- 版本 6,发布于 2018 年 10 月 30 日。修复了 bug:参数被小写化。新增:数组元素值可以包含空格;数组现在只能用逗号分隔。
- 版本 7,发布于 2018 年 11 月。新增:无需指定参数即可实例化 ParamsObject;参数动态派生。新增:无需指定 str 数组即可使用指定字符串实例化 ParamsObject。
- 版本 8,发布于 2018 年 12 月 6 日。修复了 bug:GetHelpIfNeeded() 与 cstor ParamsObject(CommandText)。


