From 7efd9c2faea80ecaf639f931cae02dea8b2183ef Mon Sep 17 00:00:00 2001 From: Scott Xu Date: Fri, 28 Aug 2020 22:22:55 +0800 Subject: [PATCH 1/9] default value for option (and show it in the help) --- src/CommandLineUtils/CommandOption.cs | 5 +++++ src/CommandLineUtils/CommandOption{T}.cs | 16 +++++++++++++++ .../OptionAttributeConventionBase.cs | 6 ++++++ .../HelpText/DefaultHelpTextGenerator.cs | 5 +++++ src/CommandLineUtils/Internal/Delegates.cs | 2 ++ .../Internal/ReflectionHelper.cs | 20 +++++++++++++++++++ src/CommandLineUtils/PublicAPI.Shipped.txt | 4 ++++ .../DefaultHelpTextGeneratorTests.cs | 12 +++++++++++ 8 files changed, 70 insertions(+) diff --git a/src/CommandLineUtils/CommandOption.cs b/src/CommandLineUtils/CommandOption.cs index f47c894c..b08b7c62 100644 --- a/src/CommandLineUtils/CommandOption.cs +++ b/src/CommandLineUtils/CommandOption.cs @@ -101,6 +101,11 @@ internal CommandOption(CommandOptionType type) /// public List Values { get; } = new List(); + /// + /// The default value of the option. + /// + public string? DefaultValue { get; set; } + /// /// Defines the type of the option. /// diff --git a/src/CommandLineUtils/CommandOption{T}.cs b/src/CommandLineUtils/CommandOption{T}.cs index 5fe98052..7ed5e0d6 100644 --- a/src/CommandLineUtils/CommandOption{T}.cs +++ b/src/CommandLineUtils/CommandOption{T}.cs @@ -19,6 +19,7 @@ public class CommandOption : CommandOption, IInternalCommandParamOfT { private readonly List _parsedValues = new List(); private readonly IValueParser _valueParser; + private T _defaultValue; /// /// Intializes a new instance of @@ -31,6 +32,7 @@ public CommandOption(IValueParser valueParser, string template, CommandOption { _valueParser = valueParser ?? throw new ArgumentNullException(nameof(valueParser)); UnderlyingType = typeof(T); + base.DefaultValue = default(T)?.ToString(); } /// @@ -43,6 +45,20 @@ public CommandOption(IValueParser valueParser, string template, CommandOption /// public IReadOnlyList ParsedValues => _parsedValues; + /// + /// The default value of the option + /// + + public new T DefaultValue + { + get => _defaultValue; + set + { + _defaultValue = value; + base.DefaultValue = value?.ToString(); + } + } + void IInternalCommandParamOfT.Parse(CultureInfo culture) { _parsedValues.Clear(); diff --git a/src/CommandLineUtils/Conventions/OptionAttributeConventionBase.cs b/src/CommandLineUtils/Conventions/OptionAttributeConventionBase.cs index db72a6f0..8c5f0e74 100644 --- a/src/CommandLineUtils/Conventions/OptionAttributeConventionBase.cs +++ b/src/CommandLineUtils/Conventions/OptionAttributeConventionBase.cs @@ -57,6 +57,12 @@ private protected void AddOption(ConventionContext context, CommandOption option context.Application._longOptions.Add(option.LongName, prop); } + var getter = ReflectionHelper.GetPropertyGetter(prop); + + var value = getter.Invoke(modelAccessor.GetModel()); + + option.DefaultValue = getter.Invoke(modelAccessor.GetModel())?.ToString(); + var setter = ReflectionHelper.GetPropertySetter(prop); switch (option.OptionType) diff --git a/src/CommandLineUtils/HelpText/DefaultHelpTextGenerator.cs b/src/CommandLineUtils/HelpText/DefaultHelpTextGenerator.cs index 6c5665bc..3d2b8bda 100644 --- a/src/CommandLineUtils/HelpText/DefaultHelpTextGenerator.cs +++ b/src/CommandLineUtils/HelpText/DefaultHelpTextGenerator.cs @@ -250,6 +250,11 @@ protected virtual void GenerateOptions( } } + if (!string.IsNullOrEmpty(opt.DefaultValue)) + { + description += $"\nDefault value is: {opt.DefaultValue}."; + } + var wrappedDescription = IndentWriter?.Write(description); var message = string.Format(outputFormat, Format(opt), wrappedDescription); diff --git a/src/CommandLineUtils/Internal/Delegates.cs b/src/CommandLineUtils/Internal/Delegates.cs index c9d9de13..e9471010 100644 --- a/src/CommandLineUtils/Internal/Delegates.cs +++ b/src/CommandLineUtils/Internal/Delegates.cs @@ -4,4 +4,6 @@ namespace McMaster.Extensions.CommandLineUtils { internal delegate void SetPropertyDelegate(object obj, object? value); + + internal delegate object GetPropertyDelegate(object obj); } diff --git a/src/CommandLineUtils/Internal/ReflectionHelper.cs b/src/CommandLineUtils/Internal/ReflectionHelper.cs index a3785611..5f977a7b 100644 --- a/src/CommandLineUtils/Internal/ReflectionHelper.cs +++ b/src/CommandLineUtils/Internal/ReflectionHelper.cs @@ -35,6 +35,26 @@ public static SetPropertyDelegate GetPropertySetter(PropertyInfo prop) } } + public static GetPropertyDelegate GetPropertyGetter(PropertyInfo prop) + { + var setter = prop.GetSetMethod(nonPublic: true); + if (setter != null) + { + return obj => setter.Invoke(obj, new object[] { }); + } + else + { + var backingField = prop.DeclaringType.GetField($"<{prop.Name}>k__BackingField", DeclaredOnlyLookup); + if (backingField == null) + { + throw new InvalidOperationException( + $"Could not find a way to get {prop.DeclaringType.FullName}.{prop.Name}. Try adding a getter."); + } + + return obj => backingField.GetValue(obj); + } + } + public static MethodInfo[] GetPropertyOrMethod(Type type, string name) { var members = GetAllMembers(type).ToList(); diff --git a/src/CommandLineUtils/PublicAPI.Shipped.txt b/src/CommandLineUtils/PublicAPI.Shipped.txt index 56994b24..551cac6f 100644 --- a/src/CommandLineUtils/PublicAPI.Shipped.txt +++ b/src/CommandLineUtils/PublicAPI.Shipped.txt @@ -194,6 +194,8 @@ McMaster.Extensions.CommandLineUtils.CommandLineApplication.ModelFactory McMaster.Extensions.CommandLineUtils.CommandLineApplicationExtensions McMaster.Extensions.CommandLineUtils.CommandOption McMaster.Extensions.CommandLineUtils.CommandOption.CommandOption(string! template, McMaster.Extensions.CommandLineUtils.CommandOptionType optionType) -> void +McMaster.Extensions.CommandLineUtils.CommandOption.DefaultValue.get -> string? +McMaster.Extensions.CommandLineUtils.CommandOption.DefaultValue.set -> void McMaster.Extensions.CommandLineUtils.CommandOption.Description.get -> string? McMaster.Extensions.CommandLineUtils.CommandOption.Description.set -> void McMaster.Extensions.CommandLineUtils.CommandOption.HasValue() -> bool @@ -216,6 +218,8 @@ McMaster.Extensions.CommandLineUtils.CommandOption.ValueName.set -> void McMaster.Extensions.CommandLineUtils.CommandOption.Values.get -> System.Collections.Generic.List! McMaster.Extensions.CommandLineUtils.CommandOption McMaster.Extensions.CommandLineUtils.CommandOption.CommandOption(McMaster.Extensions.CommandLineUtils.Abstractions.IValueParser! valueParser, string! template, McMaster.Extensions.CommandLineUtils.CommandOptionType optionType) -> void +McMaster.Extensions.CommandLineUtils.CommandOption.DefaultValue.get -> T +McMaster.Extensions.CommandLineUtils.CommandOption.DefaultValue.set -> void McMaster.Extensions.CommandLineUtils.CommandOption.ParsedValue.get -> T McMaster.Extensions.CommandLineUtils.CommandOption.ParsedValues.get -> System.Collections.Generic.IReadOnlyList! McMaster.Extensions.CommandLineUtils.CommandOptionType diff --git a/test/CommandLineUtils.Tests/DefaultHelpTextGeneratorTests.cs b/test/CommandLineUtils.Tests/DefaultHelpTextGeneratorTests.cs index 4e03dc16..3bf8ace6 100644 --- a/test/CommandLineUtils.Tests/DefaultHelpTextGeneratorTests.cs +++ b/test/CommandLineUtils.Tests/DefaultHelpTextGeneratorTests.cs @@ -110,6 +110,7 @@ public void ShowHelp() app.HelpOption(); app.Option("--strOpt ", "str option desc.", CommandOptionType.SingleValue); app.Option("--rStrOpt ", "restricted str option desc.", CommandOptionType.SingleValue, o => o.IsRequired().Accepts().Values("Foo", "Bar")); + app.Option("--dStrOpt ", "str option with default value desc.", CommandOptionType.SingleValue, o => o.DefaultValue = "Foo"); app.Option("--intOpt ", "int option desc.", CommandOptionType.SingleValue); app.Option("--enumOpt ", "enum option desc.", CommandOptionType.SingleValue); app.Argument("SomeStringArgument", "string arg desc."); @@ -131,9 +132,13 @@ SomeEnumArgument enum arg desc. --strOpt str option desc. --rStrOpt restricted str option desc. Allowed values are: Foo, Bar. + --dStrOpt str option with default value desc. + Default value is: Foo. --intOpt int option desc. + Default value is: 0. --enumOpt enum option desc. Allowed values are: None, Normal, Extreme. + Default value is: None. ", helpText, @@ -161,9 +166,13 @@ SomeEnumArgument enum arg desc. -strOpt|--str-opt str option desc. -rStrOpt|--r-str-opt restricted str option desc. Allowed values are: Foo, Bar. + -dStrOpt|--d-str-opt str option with default value desc. + Default value is: Foo. -intOpt|--int-opt int option desc. + Default value is: 0. -enumOpt|--verbosity enum option desc. Allowed values are: None, Normal, Extreme. + Default value is: None. -?|-h|--help Show help information. ", @@ -181,6 +190,9 @@ public class MyApp [AllowedValues("Foo", "Bar")] public string rStrOpt { get; set; } + [Option(ShortName = "dStrOpt", Description = "str option with default value desc.")] + public string dStrOpt { get; set; } = "Foo"; + [Option(ShortName = "intOpt", Description = "int option desc.")] public int intOpt { get; set; } From 27690f984113166c91a310734cff42ea647a4e5a Mon Sep 17 00:00:00 2001 From: Scott Xu Date: Fri, 28 Aug 2020 22:58:37 +0800 Subject: [PATCH 2/9] Fix UT Note: `UseConstructorInjection` must call prior to `UseDefaultConventions` since we now need to access model at early stage --- src/CommandLineUtils/CommandLineApplication.cs | 5 +++++ src/CommandLineUtils/CommandLineApplication{T}.cs | 8 ++------ .../Conventions/OptionAttributeConventionBase.cs | 2 -- src/CommandLineUtils/Internal/ReflectionHelper.cs | 6 +++--- .../ConstructorInjectionConventionTests.cs | 2 +- 5 files changed, 11 insertions(+), 12 deletions(-) diff --git a/src/CommandLineUtils/CommandLineApplication.cs b/src/CommandLineUtils/CommandLineApplication.cs index 7af05ed4..441f7c51 100644 --- a/src/CommandLineUtils/CommandLineApplication.cs +++ b/src/CommandLineUtils/CommandLineApplication.cs @@ -128,6 +128,11 @@ internal CommandLineApplication( _conventionContext = CreateConventionContext(); + this.Initialize(); + } + + internal virtual void Initialize() + { if (Parent != null) { foreach (var convention in Parent._conventions) diff --git a/src/CommandLineUtils/CommandLineApplication{T}.cs b/src/CommandLineUtils/CommandLineApplication{T}.cs index 58258987..f6501a0e 100644 --- a/src/CommandLineUtils/CommandLineApplication{T}.cs +++ b/src/CommandLineUtils/CommandLineApplication{T}.cs @@ -25,7 +25,6 @@ public class CommandLineApplication : CommandLineApplication, IModelAcce public CommandLineApplication() : base() { - Initialize(); } /// @@ -35,7 +34,6 @@ public CommandLineApplication() public CommandLineApplication(IConsole console) : base(console) { - Initialize(); } /// @@ -46,7 +44,6 @@ public CommandLineApplication(IConsole console) public CommandLineApplication(IConsole console, string workingDirectory) : base(console, workingDirectory) { - Initialize(); } /// @@ -64,18 +61,17 @@ public CommandLineApplication(IConsole console, string workingDirectory) public CommandLineApplication(IHelpTextGenerator helpTextGenerator, IConsole console, string workingDirectory) : base(helpTextGenerator, console, workingDirectory) { - Initialize(); } internal CommandLineApplication(CommandLineApplication parent, string name) : base(parent, name) { - Initialize(); } - private void Initialize() + internal override void Initialize() { _lazy = new Lazy(CreateModel); + base.Initialize(); } private static TModel DefaultModelFactory() diff --git a/src/CommandLineUtils/Conventions/OptionAttributeConventionBase.cs b/src/CommandLineUtils/Conventions/OptionAttributeConventionBase.cs index 8c5f0e74..17b15642 100644 --- a/src/CommandLineUtils/Conventions/OptionAttributeConventionBase.cs +++ b/src/CommandLineUtils/Conventions/OptionAttributeConventionBase.cs @@ -59,8 +59,6 @@ private protected void AddOption(ConventionContext context, CommandOption option var getter = ReflectionHelper.GetPropertyGetter(prop); - var value = getter.Invoke(modelAccessor.GetModel()); - option.DefaultValue = getter.Invoke(modelAccessor.GetModel())?.ToString(); var setter = ReflectionHelper.GetPropertySetter(prop); diff --git a/src/CommandLineUtils/Internal/ReflectionHelper.cs b/src/CommandLineUtils/Internal/ReflectionHelper.cs index 5f977a7b..e17f6747 100644 --- a/src/CommandLineUtils/Internal/ReflectionHelper.cs +++ b/src/CommandLineUtils/Internal/ReflectionHelper.cs @@ -37,10 +37,10 @@ public static SetPropertyDelegate GetPropertySetter(PropertyInfo prop) public static GetPropertyDelegate GetPropertyGetter(PropertyInfo prop) { - var setter = prop.GetSetMethod(nonPublic: true); - if (setter != null) + var getter = prop.GetGetMethod(nonPublic: true); + if (getter != null) { - return obj => setter.Invoke(obj, new object[] { }); + return obj => getter.Invoke(obj, new object[] { }); } else { diff --git a/test/CommandLineUtils.Tests/ConstructorInjectionConventionTests.cs b/test/CommandLineUtils.Tests/ConstructorInjectionConventionTests.cs index 2948528f..ff6b7510 100644 --- a/test/CommandLineUtils.Tests/ConstructorInjectionConventionTests.cs +++ b/test/CommandLineUtils.Tests/ConstructorInjectionConventionTests.cs @@ -79,7 +79,7 @@ public void ItPrefersIEnumOfOptionsFromUs() { var app = new CommandLineApplication(); var services = new ServiceCollection().BuildServiceProvider(); - app.Conventions.UseDefaultConventions().UseConstructorInjection(services); + app.Conventions.UseConstructorInjection(services).UseDefaultConventions(); app.Parse(); Assert.Empty(services.GetServices>()); Assert.Empty(services.GetServices>()); From e4eb07ee800e89419bc8eac3fe723bb8922cda7a Mon Sep 17 00:00:00 2001 From: Scott Xu Date: Sat, 29 Aug 2020 23:29:10 +0800 Subject: [PATCH 3/9] Support DefaultValue for CommandArgument --- src/CommandLineUtils/CommandArgument.cs | 5 +++++ src/CommandLineUtils/CommandArgument{T}.cs | 15 +++++++++++++++ src/CommandLineUtils/CommandOption{T}.cs | 3 +-- .../Conventions/ArgumentAttributeConvention.cs | 4 ++++ .../HelpText/DefaultHelpTextGenerator.cs | 5 +++++ src/CommandLineUtils/PublicAPI.Shipped.txt | 4 ++++ .../DefaultHelpTextGeneratorTests.cs | 16 +++++++++++++--- 7 files changed, 47 insertions(+), 5 deletions(-) diff --git a/src/CommandLineUtils/CommandArgument.cs b/src/CommandLineUtils/CommandArgument.cs index e218de2f..7b2e117a 100644 --- a/src/CommandLineUtils/CommandArgument.cs +++ b/src/CommandLineUtils/CommandArgument.cs @@ -54,6 +54,11 @@ public CommandArgument() /// public string? Value => Values.FirstOrDefault(); + /// + /// The default value of the argument. + /// + public string? DefaultValue { get; set; } + /// /// A collection of validators that execute before invoking . /// When validation fails, is invoked. diff --git a/src/CommandLineUtils/CommandArgument{T}.cs b/src/CommandLineUtils/CommandArgument{T}.cs index d79ce050..0a60acb8 100644 --- a/src/CommandLineUtils/CommandArgument{T}.cs +++ b/src/CommandLineUtils/CommandArgument{T}.cs @@ -20,6 +20,7 @@ public class CommandArgument : CommandArgument, IInternalCommandParamOfT { private readonly List _parsedValues = new List(); private readonly IValueParser _valueParser; + private T _defaultValue; /// /// Initializes a new instance of @@ -29,6 +30,7 @@ public CommandArgument(IValueParser valueParser) { _valueParser = valueParser ?? throw new ArgumentNullException(nameof(valueParser)); UnderlyingType = typeof(T); + base.DefaultValue = default(T)?.ToString(); } /// @@ -41,6 +43,19 @@ public CommandArgument(IValueParser valueParser) /// public IReadOnlyList ParsedValues => _parsedValues; + /// + /// The default value of the argument. + /// + public new T DefaultValue + { + get => _defaultValue; + set + { + _defaultValue = value; + base.DefaultValue = value?.ToString(); + } + } + void IInternalCommandParamOfT.Parse(CultureInfo culture) { _parsedValues.Clear(); diff --git a/src/CommandLineUtils/CommandOption{T}.cs b/src/CommandLineUtils/CommandOption{T}.cs index 7ed5e0d6..7bbcf6c4 100644 --- a/src/CommandLineUtils/CommandOption{T}.cs +++ b/src/CommandLineUtils/CommandOption{T}.cs @@ -46,9 +46,8 @@ public CommandOption(IValueParser valueParser, string template, CommandOption public IReadOnlyList ParsedValues => _parsedValues; /// - /// The default value of the option + /// The default value of the option. /// - public new T DefaultValue { get => _defaultValue; diff --git a/src/CommandLineUtils/Conventions/ArgumentAttributeConvention.cs b/src/CommandLineUtils/Conventions/ArgumentAttributeConvention.cs index 132e2455..d24936c7 100644 --- a/src/CommandLineUtils/Conventions/ArgumentAttributeConvention.cs +++ b/src/CommandLineUtils/Conventions/ArgumentAttributeConvention.cs @@ -95,6 +95,10 @@ private void AddArgument(PropertyInfo prop, argPropOrder.Add(argumentAttr.Order, prop); argOrder.Add(argumentAttr.Order, argument); + var getter = ReflectionHelper.GetPropertyGetter(prop); + + argument.DefaultValue = getter.Invoke(convention.ModelAccessor.GetModel())?.ToString(); + var setter = ReflectionHelper.GetPropertySetter(prop); if (argument.MultipleValues) diff --git a/src/CommandLineUtils/HelpText/DefaultHelpTextGenerator.cs b/src/CommandLineUtils/HelpText/DefaultHelpTextGenerator.cs index 3d2b8bda..78c132b1 100644 --- a/src/CommandLineUtils/HelpText/DefaultHelpTextGenerator.cs +++ b/src/CommandLineUtils/HelpText/DefaultHelpTextGenerator.cs @@ -206,6 +206,11 @@ protected virtual void GenerateArguments( } } + if (!string.IsNullOrEmpty(arg.DefaultValue)) + { + description += $"\nDefault value is: {arg.DefaultValue}."; + } + var wrappedDescription = IndentWriter?.Write(description); var message = string.Format(outputFormat, arg.Name, wrappedDescription); diff --git a/src/CommandLineUtils/PublicAPI.Shipped.txt b/src/CommandLineUtils/PublicAPI.Shipped.txt index 551cac6f..ef6a793b 100644 --- a/src/CommandLineUtils/PublicAPI.Shipped.txt +++ b/src/CommandLineUtils/PublicAPI.Shipped.txt @@ -53,6 +53,8 @@ McMaster.Extensions.CommandLineUtils.ArgumentAttribute.ShowInHelpText.set -> voi McMaster.Extensions.CommandLineUtils.ArgumentEscaper McMaster.Extensions.CommandLineUtils.CommandArgument McMaster.Extensions.CommandLineUtils.CommandArgument.CommandArgument() -> void +McMaster.Extensions.CommandLineUtils.CommandArgument.DefaultValue.get -> string? +McMaster.Extensions.CommandLineUtils.CommandArgument.DefaultValue.set -> void McMaster.Extensions.CommandLineUtils.CommandArgument.Description.get -> string? McMaster.Extensions.CommandLineUtils.CommandArgument.Description.set -> void McMaster.Extensions.CommandLineUtils.CommandArgument.MultipleValues.get -> bool @@ -66,6 +68,8 @@ McMaster.Extensions.CommandLineUtils.CommandArgument.Value.get -> string? McMaster.Extensions.CommandLineUtils.CommandArgument.Values.get -> System.Collections.Generic.List! McMaster.Extensions.CommandLineUtils.CommandArgument McMaster.Extensions.CommandLineUtils.CommandArgument.CommandArgument(McMaster.Extensions.CommandLineUtils.Abstractions.IValueParser! valueParser) -> void +McMaster.Extensions.CommandLineUtils.CommandArgument.DefaultValue.get -> T +McMaster.Extensions.CommandLineUtils.CommandArgument.DefaultValue.set -> void McMaster.Extensions.CommandLineUtils.CommandArgument.ParsedValue.get -> T McMaster.Extensions.CommandLineUtils.CommandArgument.ParsedValues.get -> System.Collections.Generic.IReadOnlyList! McMaster.Extensions.CommandLineUtils.CommandAttribute diff --git a/test/CommandLineUtils.Tests/DefaultHelpTextGeneratorTests.cs b/test/CommandLineUtils.Tests/DefaultHelpTextGeneratorTests.cs index 3bf8ace6..66da4d3a 100644 --- a/test/CommandLineUtils.Tests/DefaultHelpTextGeneratorTests.cs +++ b/test/CommandLineUtils.Tests/DefaultHelpTextGeneratorTests.cs @@ -115,17 +115,21 @@ public void ShowHelp() app.Option("--enumOpt ", "enum option desc.", CommandOptionType.SingleValue); app.Argument("SomeStringArgument", "string arg desc."); app.Argument("RestrictedStringArgument", "restricted string arg desc.", a => a.IsRequired().Accepts().Values("Foo", "Bar")); + app.Argument("DefaultValStringArgument", "string arg with default value desc.", a => a.DefaultValue = "Foo"); app.Argument("SomeEnumArgument", "enum arg desc."); var helpText = GetHelpText(app); - Assert.Equal(@"Usage: [options] + Assert.Equal(@"Usage: [options] Arguments: SomeStringArgument string arg desc. RestrictedStringArgument restricted string arg desc. Allowed values are: Foo, Bar. + DefaultValStringArgument string arg with default value desc. + Default value is: Foo. SomeEnumArgument enum arg desc. Allowed values are: None, Normal, Extreme. + Default value is: None. Options: -?|-h|--help Show help information. @@ -153,14 +157,17 @@ public void ShowHelpFromAttributes() app.Conventions.UseDefaultConventions(); var helpText = GetHelpText(app); - Assert.Equal(@"Usage: test [options] + Assert.Equal(@"Usage: test [options] Arguments: SomeStringArgument string arg desc. RestrictedStringArgument restricted string arg desc. Allowed values are: Foo, Bar. + DefaultValStringArgument string arg with default value desc. + Default value is: Foo. SomeEnumArgument enum arg desc. Allowed values are: None, Normal, Extreme. + Default value is: None. Options: -strOpt|--str-opt str option desc. @@ -207,7 +214,10 @@ public class MyApp [AllowedValues("Foo", "Bar")] public string RestrictedStringArgument { get; set; } - [Argument(2, Description = "enum arg desc.")] + [Argument(2, Description = "string arg with default value desc.")] + public string DefaultValStringArgument { get; set; } = "Foo"; + + [Argument(3, Description = "enum arg desc.")] public SomeEnum SomeEnumArgument { get; set; } } From 5baac236e5f35b7c8e6edf72fcf4981c7920a918 Mon Sep 17 00:00:00 2001 From: Scott Xu Date: Sun, 30 Aug 2020 11:36:08 +0800 Subject: [PATCH 4/9] Do not show default value for (bool,T) --- src/CommandLineUtils/CommandArgument{T}.cs | 12 +++++++++-- src/CommandLineUtils/CommandOption{T}.cs | 13 ++++++++++-- .../ArgumentAttributeConvention.cs | 5 ++++- .../OptionAttributeConventionBase.cs | 7 ++++++- .../Internal/ReflectionHelper.cs | 20 +++++++++++++++++++ .../DefaultHelpTextGeneratorTests.cs | 16 +++++++++++++-- 6 files changed, 65 insertions(+), 8 deletions(-) diff --git a/src/CommandLineUtils/CommandArgument{T}.cs b/src/CommandLineUtils/CommandArgument{T}.cs index 0a60acb8..21fd2297 100644 --- a/src/CommandLineUtils/CommandArgument{T}.cs +++ b/src/CommandLineUtils/CommandArgument{T}.cs @@ -30,7 +30,7 @@ public CommandArgument(IValueParser valueParser) { _valueParser = valueParser ?? throw new ArgumentNullException(nameof(valueParser)); UnderlyingType = typeof(T); - base.DefaultValue = default(T)?.ToString(); + SetBaseDefaultValue(default); } /// @@ -52,7 +52,7 @@ public CommandArgument(IValueParser valueParser) set { _defaultValue = value; - base.DefaultValue = value?.ToString(); + SetBaseDefaultValue(value); } } @@ -64,5 +64,13 @@ void IInternalCommandParamOfT.Parse(CultureInfo culture) _parsedValues.Add(_valueParser.Parse(Name, Values[i], culture)); } } + + void SetBaseDefaultValue(T value) + { + if (!ReflectionHelper.IsSpecialValueTupleType(typeof(T), out _)) + { + base.DefaultValue = value?.ToString(); + } + } } } diff --git a/src/CommandLineUtils/CommandOption{T}.cs b/src/CommandLineUtils/CommandOption{T}.cs index 7bbcf6c4..f57d29de 100644 --- a/src/CommandLineUtils/CommandOption{T}.cs +++ b/src/CommandLineUtils/CommandOption{T}.cs @@ -5,6 +5,7 @@ using System.Collections.Generic; using System.Globalization; using System.Linq; +using System.Reflection; using McMaster.Extensions.CommandLineUtils.Abstractions; namespace McMaster.Extensions.CommandLineUtils @@ -32,7 +33,7 @@ public CommandOption(IValueParser valueParser, string template, CommandOption { _valueParser = valueParser ?? throw new ArgumentNullException(nameof(valueParser)); UnderlyingType = typeof(T); - base.DefaultValue = default(T)?.ToString(); + SetBaseDefaultValue(default); } /// @@ -54,7 +55,7 @@ public CommandOption(IValueParser valueParser, string template, CommandOption set { _defaultValue = value; - base.DefaultValue = value?.ToString(); + SetBaseDefaultValue(value); } } @@ -66,5 +67,13 @@ void IInternalCommandParamOfT.Parse(CultureInfo culture) _parsedValues.Add(_valueParser.Parse(LongName ?? ShortName ?? SymbolName, Values[i], culture)); } } + + void SetBaseDefaultValue(T value) + { + if (!ReflectionHelper.IsSpecialValueTupleType(typeof(T), out _)) + { + base.DefaultValue = value?.ToString(); + } + } } } diff --git a/src/CommandLineUtils/Conventions/ArgumentAttributeConvention.cs b/src/CommandLineUtils/Conventions/ArgumentAttributeConvention.cs index d24936c7..dab7d7f9 100644 --- a/src/CommandLineUtils/Conventions/ArgumentAttributeConvention.cs +++ b/src/CommandLineUtils/Conventions/ArgumentAttributeConvention.cs @@ -97,7 +97,10 @@ private void AddArgument(PropertyInfo prop, var getter = ReflectionHelper.GetPropertyGetter(prop); - argument.DefaultValue = getter.Invoke(convention.ModelAccessor.GetModel())?.ToString(); + if (!ReflectionHelper.IsSpecialValueTupleType(prop.PropertyType, out _)) + { + argument.DefaultValue = getter.Invoke(convention.ModelAccessor.GetModel())?.ToString(); + } var setter = ReflectionHelper.GetPropertySetter(prop); diff --git a/src/CommandLineUtils/Conventions/OptionAttributeConventionBase.cs b/src/CommandLineUtils/Conventions/OptionAttributeConventionBase.cs index 17b15642..ec1eae8c 100644 --- a/src/CommandLineUtils/Conventions/OptionAttributeConventionBase.cs +++ b/src/CommandLineUtils/Conventions/OptionAttributeConventionBase.cs @@ -59,7 +59,12 @@ private protected void AddOption(ConventionContext context, CommandOption option var getter = ReflectionHelper.GetPropertyGetter(prop); - option.DefaultValue = getter.Invoke(modelAccessor.GetModel())?.ToString(); + var value = getter.Invoke(modelAccessor.GetModel()); + + if (!ReflectionHelper.IsSpecialValueTupleType(prop.PropertyType, out _)) + { + option.DefaultValue = value?.ToString(); + } var setter = ReflectionHelper.GetPropertySetter(prop); diff --git a/src/CommandLineUtils/Internal/ReflectionHelper.cs b/src/CommandLineUtils/Internal/ReflectionHelper.cs index e17f6747..cb428ac1 100644 --- a/src/CommandLineUtils/Internal/ReflectionHelper.cs +++ b/src/CommandLineUtils/Internal/ReflectionHelper.cs @@ -125,6 +125,26 @@ public static bool IsNullableType(Type type, out Type? wrappedType) return result; } + public static bool IsSpecialValueTupleType(Type type, out FieldInfo fieldInfo) + { + var result = type.IsGenericType && + type.GetGenericTypeDefinition() == typeof(ValueTuple<,>) && + type.GenericTypeArguments[0] == typeof(bool); + fieldInfo = result ? type.GetFields()[1] : null; + + return result; + } + + public static bool IsSpecialTupleType(Type type, out PropertyInfo propertyInfo) + { + var result = type.IsGenericType && + type.GetGenericTypeDefinition() == typeof(Tuple<,>) && + type.GenericTypeArguments[0] == typeof(bool); + propertyInfo = result ? type.GetProperties()[1] : null; + + return result; + } + private static IEnumerable GetAllMembers(Type type) { while (type != null) diff --git a/test/CommandLineUtils.Tests/DefaultHelpTextGeneratorTests.cs b/test/CommandLineUtils.Tests/DefaultHelpTextGeneratorTests.cs index 66da4d3a..d116ed1f 100644 --- a/test/CommandLineUtils.Tests/DefaultHelpTextGeneratorTests.cs +++ b/test/CommandLineUtils.Tests/DefaultHelpTextGeneratorTests.cs @@ -113,13 +113,15 @@ public void ShowHelp() app.Option("--dStrOpt ", "str option with default value desc.", CommandOptionType.SingleValue, o => o.DefaultValue = "Foo"); app.Option("--intOpt ", "int option desc.", CommandOptionType.SingleValue); app.Option("--enumOpt ", "enum option desc.", CommandOptionType.SingleValue); + app.Option<(bool, SomeEnum)>("--enumOpt2 ", "nullable enum option desc.", CommandOptionType.SingleValue); app.Argument("SomeStringArgument", "string arg desc."); app.Argument("RestrictedStringArgument", "restricted string arg desc.", a => a.IsRequired().Accepts().Values("Foo", "Bar")); app.Argument("DefaultValStringArgument", "string arg with default value desc.", a => a.DefaultValue = "Foo"); app.Argument("SomeEnumArgument", "enum arg desc."); + app.Argument<(bool, SomeEnum)>("SomeNullableEnumArgument", "nullable enum arg desc."); var helpText = GetHelpText(app); - Assert.Equal(@"Usage: [options] + Assert.Equal(@"Usage: [options] Arguments: SomeStringArgument string arg desc. @@ -130,6 +132,7 @@ RestrictedStringArgument restricted string arg desc. SomeEnumArgument enum arg desc. Allowed values are: None, Normal, Extreme. Default value is: None. + SomeNullableEnumArgument nullable enum arg desc. Options: -?|-h|--help Show help information. @@ -143,6 +146,7 @@ SomeEnumArgument enum arg desc. --enumOpt enum option desc. Allowed values are: None, Normal, Extreme. Default value is: None. + --enumOpt2 nullable enum option desc. ", helpText, @@ -157,7 +161,7 @@ public void ShowHelpFromAttributes() app.Conventions.UseDefaultConventions(); var helpText = GetHelpText(app); - Assert.Equal(@"Usage: test [options] + Assert.Equal(@"Usage: test [options] Arguments: SomeStringArgument string arg desc. @@ -168,6 +172,7 @@ RestrictedStringArgument restricted string arg desc. SomeEnumArgument enum arg desc. Allowed values are: None, Normal, Extreme. Default value is: None. + SomeNullableEnumArgument nullable enum arg desc. Options: -strOpt|--str-opt str option desc. @@ -180,6 +185,7 @@ SomeEnumArgument enum arg desc. -enumOpt|--verbosity enum option desc. Allowed values are: None, Normal, Extreme. Default value is: None. + -enumOpt2|--verb2[:] nullable enum option desc. -?|-h|--help Show help information. ", @@ -206,6 +212,9 @@ public class MyApp [Option(ShortName = "enumOpt", Description = "enum option desc.")] public SomeEnum Verbosity { get; set; } + [Option(ShortName = "enumOpt2", Description = "nullable enum option desc.")] + public (bool HasValue, SomeEnum Value) Verb2 { get; set; } + [Argument(0, Description = "string arg desc.")] public string SomeStringArgument { get; set; } @@ -219,6 +228,9 @@ public class MyApp [Argument(3, Description = "enum arg desc.")] public SomeEnum SomeEnumArgument { get; set; } + + [Argument(4, Description = "nullable enum arg desc.")] + public (bool HasValue, SomeEnum Value) SomeNullableEnumArgument { get; set; } } [Theory] From 410fd4be299e7cad4dc2e32b7a5b2a59fbcc073b Mon Sep 17 00:00:00 2001 From: Scott Xu Date: Mon, 14 Sep 2020 22:02:34 +0800 Subject: [PATCH 5/9] Add to Values when default value is set; Reuse IsSpecialTupleType, IsSpecialValueTupeType --- .../Abstractions/ValueParserProvider.cs | 13 +++------ src/CommandLineUtils/CommandArgument.cs | 15 +++++++++- src/CommandLineUtils/CommandOption.cs | 15 +++++++++- .../OptionAttributeConventionBase.cs | 13 +++++---- .../HelpText/DefaultHelpTextGenerator.cs | 4 +-- .../Internal/CommandOptionTypeMapper.cs | 28 ++++++++----------- .../Internal/ReflectionHelper.cs | 8 +++--- 7 files changed, 58 insertions(+), 38 deletions(-) diff --git a/src/CommandLineUtils/Abstractions/ValueParserProvider.cs b/src/CommandLineUtils/Abstractions/ValueParserProvider.cs index 1f54528d..af7f8650 100644 --- a/src/CommandLineUtils/Abstractions/ValueParserProvider.cs +++ b/src/CommandLineUtils/Abstractions/ValueParserProvider.cs @@ -101,7 +101,7 @@ public IValueParser GetParser(Type type) return parser; } - if (ReflectionHelper.IsNullableType(type, out var wrappedType) && wrappedType != null) + if (ReflectionHelper.IsNullableType(type, out var wrappedType)) { if (wrappedType.IsEnum) { @@ -114,21 +114,16 @@ public IValueParser GetParser(Type type) } } - if (!type.IsGenericType) + if (ReflectionHelper.IsSpecialValueTupleType(type, out var wrappedType2)) { - return null; - } - - var typeDef = type.GetGenericTypeDefinition(); - if (typeDef == typeof(ValueTuple<,>) && type.GenericTypeArguments[0] == typeof(bool)) - { - var innerParser = GetParser(type.GenericTypeArguments[1]); + var innerParser = GetParser(wrappedType2); if (innerParser == null) { return null; } var method = typeof(ValueTupleValueParser).GetMethod(nameof(ValueTupleValueParser.Create)).MakeGenericMethod(type.GenericTypeArguments[1]); return (IValueParser)method.Invoke(null, new object[] { innerParser }); + } return null; diff --git a/src/CommandLineUtils/CommandArgument.cs b/src/CommandLineUtils/CommandArgument.cs index 7b2e117a..275d0727 100644 --- a/src/CommandLineUtils/CommandArgument.cs +++ b/src/CommandLineUtils/CommandArgument.cs @@ -16,6 +16,8 @@ namespace McMaster.Extensions.CommandLineUtils /// public class CommandArgument { + private string? _defaultValue; + /// /// Initializes a new instance of . /// @@ -57,7 +59,18 @@ public CommandArgument() /// /// The default value of the argument. /// - public string? DefaultValue { get; set; } + public string? DefaultValue + { + get => _defaultValue; + set + { + _defaultValue = value; + if (value != null) + { + Values.Add(value); + } + } + } /// /// A collection of validators that execute before invoking . diff --git a/src/CommandLineUtils/CommandOption.cs b/src/CommandLineUtils/CommandOption.cs index 5cf3ae88..a50b90ef 100644 --- a/src/CommandLineUtils/CommandOption.cs +++ b/src/CommandLineUtils/CommandOption.cs @@ -17,6 +17,8 @@ namespace McMaster.Extensions.CommandLineUtils /// public class CommandOption { + private string? _defaultValue; + /// /// Initializes a new . /// @@ -104,7 +106,18 @@ internal CommandOption(CommandOptionType type) /// /// The default value of the option. /// - public string? DefaultValue { get; set; } + public string? DefaultValue + { + get => _defaultValue; + set + { + _defaultValue = value; + if (value != null) + { + Values.Add(value); + } + } + } /// /// Defines the type of the option. diff --git a/src/CommandLineUtils/Conventions/OptionAttributeConventionBase.cs b/src/CommandLineUtils/Conventions/OptionAttributeConventionBase.cs index ec1eae8c..6570d2a6 100644 --- a/src/CommandLineUtils/Conventions/OptionAttributeConventionBase.cs +++ b/src/CommandLineUtils/Conventions/OptionAttributeConventionBase.cs @@ -57,13 +57,16 @@ private protected void AddOption(ConventionContext context, CommandOption option context.Application._longOptions.Add(option.LongName, prop); } - var getter = ReflectionHelper.GetPropertyGetter(prop); + if (option.OptionType != CommandOptionType.NoValue) + { + var getter = ReflectionHelper.GetPropertyGetter(prop); - var value = getter.Invoke(modelAccessor.GetModel()); + var value = getter.Invoke(modelAccessor.GetModel()); - if (!ReflectionHelper.IsSpecialValueTupleType(prop.PropertyType, out _)) - { - option.DefaultValue = value?.ToString(); + if (!ReflectionHelper.IsSpecialValueTupleType(prop.PropertyType, out _)) + { + option.DefaultValue = value?.ToString(); + } } var setter = ReflectionHelper.GetPropertySetter(prop); diff --git a/src/CommandLineUtils/HelpText/DefaultHelpTextGenerator.cs b/src/CommandLineUtils/HelpText/DefaultHelpTextGenerator.cs index d16484ba..d25ec4fa 100644 --- a/src/CommandLineUtils/HelpText/DefaultHelpTextGenerator.cs +++ b/src/CommandLineUtils/HelpText/DefaultHelpTextGenerator.cs @@ -422,9 +422,9 @@ private string[] ExtractNamesFromEnum(Type type) return ExtractNamesFromEnum(wrappedType); } - if (ReflectionHelper.IsSpecialValueTupleType(type, out var fieldInfo)) + if (ReflectionHelper.IsSpecialValueTupleType(type, out var wrappedType2)) { - return ExtractNamesFromEnum(fieldInfo.FieldType); + return ExtractNamesFromEnum(wrappedType2); } if (type.IsEnum) diff --git a/src/CommandLineUtils/Internal/CommandOptionTypeMapper.cs b/src/CommandLineUtils/Internal/CommandOptionTypeMapper.cs index 0f98fcd6..ecf13692 100644 --- a/src/CommandLineUtils/Internal/CommandOptionTypeMapper.cs +++ b/src/CommandLineUtils/Internal/CommandOptionTypeMapper.cs @@ -55,28 +55,24 @@ public CommandOptionType GetOptionType(Type clrType, ValueParserProvider? valueP return CommandOptionType.SingleValue; } - if (clrType.IsGenericType) + if (ReflectionHelper.IsNullableType(clrType, out var wrappedType)) { - var typeDef = clrType.GetGenericTypeDefinition(); - if (typeDef == typeof(Nullable<>)) - { - return GetOptionType(clrType.GetGenericArguments().First(), valueParsers); - } + return GetOptionType(wrappedType, valueParsers); + } - if (typeDef == typeof(Tuple<,>) && clrType.GenericTypeArguments[0] == typeof(bool)) + if (ReflectionHelper.IsSpecialValueTupleType(clrType, out var wrappedType2)) + { + if (GetOptionType(wrappedType2, valueParsers) == CommandOptionType.SingleValue) { - if (GetOptionType(clrType.GenericTypeArguments[1], valueParsers) == CommandOptionType.SingleValue) - { - return CommandOptionType.SingleOrNoValue; - } + return CommandOptionType.SingleOrNoValue; } + } - if (typeDef == typeof(ValueTuple<,>) && clrType.GenericTypeArguments[0] == typeof(bool)) + if (ReflectionHelper.IsSpecialTupleType(clrType, out var wrappedType3)) + { + if (GetOptionType(wrappedType3, valueParsers) == CommandOptionType.SingleValue) { - if (GetOptionType(clrType.GenericTypeArguments[1], valueParsers) == CommandOptionType.SingleValue) - { - return CommandOptionType.SingleOrNoValue; - } + return CommandOptionType.SingleOrNoValue; } } diff --git a/src/CommandLineUtils/Internal/ReflectionHelper.cs b/src/CommandLineUtils/Internal/ReflectionHelper.cs index a64c52ff..e22c795d 100644 --- a/src/CommandLineUtils/Internal/ReflectionHelper.cs +++ b/src/CommandLineUtils/Internal/ReflectionHelper.cs @@ -125,22 +125,22 @@ public static bool IsNullableType(Type type, out Type? wrappedType) return result; } - public static bool IsSpecialValueTupleType(Type type, out FieldInfo fieldInfo) + public static bool IsSpecialValueTupleType(Type type, out Type? wrappedType) { var result = type.IsGenericType && type.GetGenericTypeDefinition() == typeof(ValueTuple<,>) && type.GenericTypeArguments[0] == typeof(bool); - fieldInfo = result ? type.GetFields()[1] : null; + wrappedType = result ? type.GenericTypeArguments[1] : null; return result; } - public static bool IsSpecialTupleType(Type type, out PropertyInfo propertyInfo) + public static bool IsSpecialTupleType(Type type, out Type? wrappedType) { var result = type.IsGenericType && type.GetGenericTypeDefinition() == typeof(Tuple<,>) && type.GenericTypeArguments[0] == typeof(bool); - propertyInfo = result ? type.GetProperties()[1] : null; + wrappedType = result ? type.GenericTypeArguments[1] : null; return result; } From 1529642808518c3dad844fc220faca231791188b Mon Sep 17 00:00:00 2001 From: Scott Xu Date: Tue, 22 Sep 2020 20:46:51 +0800 Subject: [PATCH 6/9] lazily show help and set default value --- .../Conventions/ArgumentAttributeConvention.cs | 9 ++++++--- .../Conventions/OptionAttributeConventionBase.cs | 13 +++++++------ .../Internal/CommandLineProcessor.cs | 2 +- .../ConstructorInjectionConventionTests.cs | 2 +- .../DefaultHelpTextGeneratorTests.cs | 14 +++++++------- 5 files changed, 22 insertions(+), 18 deletions(-) diff --git a/src/CommandLineUtils/Conventions/ArgumentAttributeConvention.cs b/src/CommandLineUtils/Conventions/ArgumentAttributeConvention.cs index dab7d7f9..1e10ffee 100644 --- a/src/CommandLineUtils/Conventions/ArgumentAttributeConvention.cs +++ b/src/CommandLineUtils/Conventions/ArgumentAttributeConvention.cs @@ -95,11 +95,14 @@ private void AddArgument(PropertyInfo prop, argPropOrder.Add(argumentAttr.Order, prop); argOrder.Add(argumentAttr.Order, argument); - var getter = ReflectionHelper.GetPropertyGetter(prop); - if (!ReflectionHelper.IsSpecialValueTupleType(prop.PropertyType, out _)) { - argument.DefaultValue = getter.Invoke(convention.ModelAccessor.GetModel())?.ToString(); + convention.Application.OnParsingComplete(r => + { + var getter = ReflectionHelper.GetPropertyGetter(prop); + var value = getter.Invoke(convention.ModelAccessor.GetModel()); + argument.DefaultValue = value?.ToString(); + }); } var setter = ReflectionHelper.GetPropertySetter(prop); diff --git a/src/CommandLineUtils/Conventions/OptionAttributeConventionBase.cs b/src/CommandLineUtils/Conventions/OptionAttributeConventionBase.cs index 6570d2a6..bf6c1307 100644 --- a/src/CommandLineUtils/Conventions/OptionAttributeConventionBase.cs +++ b/src/CommandLineUtils/Conventions/OptionAttributeConventionBase.cs @@ -59,13 +59,14 @@ private protected void AddOption(ConventionContext context, CommandOption option if (option.OptionType != CommandOptionType.NoValue) { - var getter = ReflectionHelper.GetPropertyGetter(prop); - - var value = getter.Invoke(modelAccessor.GetModel()); - - if (!ReflectionHelper.IsSpecialValueTupleType(prop.PropertyType, out _)) + if (!ReflectionHelper.IsSpecialValueTupleType(prop.PropertyType, out var type)) { - option.DefaultValue = value?.ToString(); + context.Application.OnParsingComplete(_ => + { + var getter = ReflectionHelper.GetPropertyGetter(prop); + var value = getter.Invoke(modelAccessor.GetModel()); + option.DefaultValue = value?.ToString(); + }); } } diff --git a/src/CommandLineUtils/Internal/CommandLineProcessor.cs b/src/CommandLineUtils/Internal/CommandLineProcessor.cs index 7a19722f..ef4885de 100644 --- a/src/CommandLineUtils/Internal/CommandLineProcessor.cs +++ b/src/CommandLineUtils/Internal/CommandLineProcessor.cs @@ -216,7 +216,7 @@ private bool ProcessOption(OptionArgument arg) // If we find a help/version option, show information and stop parsing if (_currentCommand.OptionHelp == option) { - _currentCommand.ShowHelp(); + _currentCommand.OnParsingComplete(_ => _currentCommand.ShowHelp()); option.TryParse(null); return false; } diff --git a/test/CommandLineUtils.Tests/ConstructorInjectionConventionTests.cs b/test/CommandLineUtils.Tests/ConstructorInjectionConventionTests.cs index 8586b66f..8ada8b1a 100644 --- a/test/CommandLineUtils.Tests/ConstructorInjectionConventionTests.cs +++ b/test/CommandLineUtils.Tests/ConstructorInjectionConventionTests.cs @@ -79,7 +79,7 @@ public void ItPrefersIEnumOfOptionsFromUs() { var app = new CommandLineApplication(); var services = new ServiceCollection().BuildServiceProvider(); - app.Conventions.UseConstructorInjection(services).UseDefaultConventions(); + app.Conventions.UseDefaultConventions().UseConstructorInjection(services); app.Parse(); Assert.Empty(services.GetServices>()); Assert.Empty(services.GetServices>()); diff --git a/test/CommandLineUtils.Tests/DefaultHelpTextGeneratorTests.cs b/test/CommandLineUtils.Tests/DefaultHelpTextGeneratorTests.cs index 380963ef..ca110299 100644 --- a/test/CommandLineUtils.Tests/DefaultHelpTextGeneratorTests.cs +++ b/test/CommandLineUtils.Tests/DefaultHelpTextGeneratorTests.cs @@ -80,25 +80,25 @@ public void DoesNotOrderCommandsByName() Assert.True(indexOfA > indexOfB); } - private string GetHelpText(CommandLineApplication app, DefaultHelpTextGenerator generator) + private string GetHelpText(CommandLineApplication app, DefaultHelpTextGenerator generator, string helpOption = null) { var sb = new StringBuilder(); - generator.Generate(app, new StringWriter(sb)); + app.Out = new StringWriter(sb); + app.HelpTextGenerator = generator; + app.Parse(helpOption ?? "-h"); var helpText = sb.ToString(); - _output.WriteLine(helpText); - return helpText; } - private string GetHelpText(CommandLineApplication app) + private string GetHelpText(CommandLineApplication app, string helpOption = null) { var generator = new DefaultHelpTextGenerator { MaxLineLength = 80 }; - return GetHelpText(app, generator); + return GetHelpText(app, generator, helpOption); } public enum SomeEnum { None, Normal, Extreme } @@ -279,7 +279,7 @@ public void ShowHelpWithSubcommands(string helpOption, string expectedHintText, if (helpOption != null) app.HelpOption(helpOption); app.Command("Subcommand", _ => { }); app.Conventions.UseDefaultConventions(); - var helpText = GetHelpText(app); + var helpText = GetHelpText(app, helpOption); Assert.Equal($@"Usage: test [command] [options] From b2a442ea9ea2c9b1306301d2c831f3dde548e889 Mon Sep 17 00:00:00 2001 From: Scott Xu Date: Tue, 22 Sep 2020 20:52:50 +0800 Subject: [PATCH 7/9] rollback the lifecycle of initialization of the model class --- src/CommandLineUtils/CommandLineApplication.cs | 5 ----- src/CommandLineUtils/CommandLineApplication{T}.cs | 8 ++++++-- 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/src/CommandLineUtils/CommandLineApplication.cs b/src/CommandLineUtils/CommandLineApplication.cs index c55d1801..00c00b4c 100644 --- a/src/CommandLineUtils/CommandLineApplication.cs +++ b/src/CommandLineUtils/CommandLineApplication.cs @@ -128,11 +128,6 @@ internal CommandLineApplication( _conventionContext = CreateConventionContext(); - this.Initialize(); - } - - internal virtual void Initialize() - { if (Parent != null) { foreach (var convention in Parent._conventions) diff --git a/src/CommandLineUtils/CommandLineApplication{T}.cs b/src/CommandLineUtils/CommandLineApplication{T}.cs index f6501a0e..58258987 100644 --- a/src/CommandLineUtils/CommandLineApplication{T}.cs +++ b/src/CommandLineUtils/CommandLineApplication{T}.cs @@ -25,6 +25,7 @@ public class CommandLineApplication : CommandLineApplication, IModelAcce public CommandLineApplication() : base() { + Initialize(); } /// @@ -34,6 +35,7 @@ public CommandLineApplication() public CommandLineApplication(IConsole console) : base(console) { + Initialize(); } /// @@ -44,6 +46,7 @@ public CommandLineApplication(IConsole console) public CommandLineApplication(IConsole console, string workingDirectory) : base(console, workingDirectory) { + Initialize(); } /// @@ -61,17 +64,18 @@ public CommandLineApplication(IConsole console, string workingDirectory) public CommandLineApplication(IHelpTextGenerator helpTextGenerator, IConsole console, string workingDirectory) : base(helpTextGenerator, console, workingDirectory) { + Initialize(); } internal CommandLineApplication(CommandLineApplication parent, string name) : base(parent, name) { + Initialize(); } - internal override void Initialize() + private void Initialize() { _lazy = new Lazy(CreateModel); - base.Initialize(); } private static TModel DefaultModelFactory() From f098c042c295cf901706559fd783ed9b65b4e845 Mon Sep 17 00:00:00 2001 From: Scott Xu Date: Tue, 22 Sep 2020 21:37:30 +0800 Subject: [PATCH 8/9] UseDefaultHelpOption --- src/CommandLineUtils/Abstractions/ValueParserProvider.cs | 1 - test/CommandLineUtils.Tests/DefaultHelpTextGeneratorTests.cs | 1 + 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/src/CommandLineUtils/Abstractions/ValueParserProvider.cs b/src/CommandLineUtils/Abstractions/ValueParserProvider.cs index af7f8650..2090a924 100644 --- a/src/CommandLineUtils/Abstractions/ValueParserProvider.cs +++ b/src/CommandLineUtils/Abstractions/ValueParserProvider.cs @@ -123,7 +123,6 @@ public IValueParser GetParser(Type type) } var method = typeof(ValueTupleValueParser).GetMethod(nameof(ValueTupleValueParser.Create)).MakeGenericMethod(type.GenericTypeArguments[1]); return (IValueParser)method.Invoke(null, new object[] { innerParser }); - } return null; diff --git a/test/CommandLineUtils.Tests/DefaultHelpTextGeneratorTests.cs b/test/CommandLineUtils.Tests/DefaultHelpTextGeneratorTests.cs index ca110299..b7a4dc7d 100644 --- a/test/CommandLineUtils.Tests/DefaultHelpTextGeneratorTests.cs +++ b/test/CommandLineUtils.Tests/DefaultHelpTextGeneratorTests.cs @@ -32,6 +32,7 @@ private class EmptyShortName public void ItFormatsOptions() { var app = new CommandLineApplication(); + app.Conventions.UseDefaultHelpOption(); var option = app.Option("-a|--all ", "All", CommandOptionType.SingleValue); option.ShortName = "b"; var helpText = GetHelpText(app); From 5005b67dc684a333aac3f766df0bc2bb0cee1752 Mon Sep 17 00:00:00 2001 From: Scott Xu Date: Sat, 26 Sep 2020 16:55:11 +0800 Subject: [PATCH 9/9] Set Values/DefaultValue after parse --- src/CommandLineUtils/CommandArgument.cs | 15 +---- src/CommandLineUtils/CommandArgument{T}.cs | 9 ++- src/CommandLineUtils/CommandOption.cs | 15 +---- src/CommandLineUtils/CommandOption{T}.cs | 9 ++- .../ArgumentAttributeConvention.cs | 64 +++++++++++-------- .../OptionAttributeConventionBase.cs | 46 ++++++++----- .../DefaultHelpTextGeneratorTests.cs | 15 +++-- 7 files changed, 94 insertions(+), 79 deletions(-) diff --git a/src/CommandLineUtils/CommandArgument.cs b/src/CommandLineUtils/CommandArgument.cs index 275d0727..7b2e117a 100644 --- a/src/CommandLineUtils/CommandArgument.cs +++ b/src/CommandLineUtils/CommandArgument.cs @@ -16,8 +16,6 @@ namespace McMaster.Extensions.CommandLineUtils /// public class CommandArgument { - private string? _defaultValue; - /// /// Initializes a new instance of . /// @@ -59,18 +57,7 @@ public CommandArgument() /// /// The default value of the argument. /// - public string? DefaultValue - { - get => _defaultValue; - set - { - _defaultValue = value; - if (value != null) - { - Values.Add(value); - } - } - } + public string? DefaultValue { get; set; } /// /// A collection of validators that execute before invoking . diff --git a/src/CommandLineUtils/CommandArgument{T}.cs b/src/CommandLineUtils/CommandArgument{T}.cs index 600047bf..bc99a216 100644 --- a/src/CommandLineUtils/CommandArgument{T}.cs +++ b/src/CommandLineUtils/CommandArgument{T}.cs @@ -69,7 +69,14 @@ void SetBaseDefaultValue(T value) { if (!ReflectionHelper.IsSpecialValueTupleType(typeof(T), out _)) { - base.DefaultValue = value?.ToString(); + if (MultipleValues && value is IEnumerable enumerable) + { + base.DefaultValue = string.Join(", ", enumerable.Select(x => x?.ToString())); + } + else + { + base.DefaultValue = value?.ToString(); + } } } } diff --git a/src/CommandLineUtils/CommandOption.cs b/src/CommandLineUtils/CommandOption.cs index a50b90ef..5cf3ae88 100644 --- a/src/CommandLineUtils/CommandOption.cs +++ b/src/CommandLineUtils/CommandOption.cs @@ -17,8 +17,6 @@ namespace McMaster.Extensions.CommandLineUtils /// public class CommandOption { - private string? _defaultValue; - /// /// Initializes a new . /// @@ -106,18 +104,7 @@ internal CommandOption(CommandOptionType type) /// /// The default value of the option. /// - public string? DefaultValue - { - get => _defaultValue; - set - { - _defaultValue = value; - if (value != null) - { - Values.Add(value); - } - } - } + public string? DefaultValue { get; set; } /// /// Defines the type of the option. diff --git a/src/CommandLineUtils/CommandOption{T}.cs b/src/CommandLineUtils/CommandOption{T}.cs index e0446b02..f3dcbe7a 100644 --- a/src/CommandLineUtils/CommandOption{T}.cs +++ b/src/CommandLineUtils/CommandOption{T}.cs @@ -72,7 +72,14 @@ void SetBaseDefaultValue(T value) { if (!ReflectionHelper.IsSpecialValueTupleType(typeof(T), out _)) { - base.DefaultValue = value?.ToString(); + if (OptionType == CommandOptionType.MultipleValue && value is IEnumerable enumerable) + { + base.DefaultValue = string.Join(", ", enumerable.Select(x => x?.ToString())); + } + else + { + base.DefaultValue = value?.ToString(); + } } } } diff --git a/src/CommandLineUtils/Conventions/ArgumentAttributeConvention.cs b/src/CommandLineUtils/Conventions/ArgumentAttributeConvention.cs index 1e10ffee..85adc70d 100644 --- a/src/CommandLineUtils/Conventions/ArgumentAttributeConvention.cs +++ b/src/CommandLineUtils/Conventions/ArgumentAttributeConvention.cs @@ -95,16 +95,7 @@ private void AddArgument(PropertyInfo prop, argPropOrder.Add(argumentAttr.Order, prop); argOrder.Add(argumentAttr.Order, argument); - if (!ReflectionHelper.IsSpecialValueTupleType(prop.PropertyType, out _)) - { - convention.Application.OnParsingComplete(r => - { - var getter = ReflectionHelper.GetPropertyGetter(prop); - var value = getter.Invoke(convention.ModelAccessor.GetModel()); - argument.DefaultValue = value?.ToString(); - }); - } - + var getter = ReflectionHelper.GetPropertyGetter(prop); var setter = ReflectionHelper.GetPropertySetter(prop); if (argument.MultipleValues) @@ -119,14 +110,23 @@ private void AddArgument(PropertyInfo prop, convention.Application.OnParsingComplete(r => { - if (argument.Values.Count == 0) - { - return; - } - if (r.SelectedCommand is IModelAccessor cmd) { - setter.Invoke(cmd.GetModel(), collectionParser.Parse(argument.Name, argument.Values)); + if (argument.Values.Count == 0) + { + if (!ReflectionHelper.IsSpecialValueTupleType(prop.PropertyType, out _)) + { + if (getter.Invoke(cmd.GetModel()) is IEnumerable value) + { + argument.Values.AddRange(value.Select(x => x?.ToString())); + argument.DefaultValue = string.Join(", ", value.Select(x => x?.ToString())); + } + } + } + else + { + setter.Invoke(cmd.GetModel(), collectionParser.Parse(argument.Name, argument.Values)); + } } }); } @@ -140,19 +140,29 @@ private void AddArgument(PropertyInfo prop, convention.Application.OnParsingComplete(r => { - if (argument.Values.Count == 0) - { - return; - } - if (r.SelectedCommand is IModelAccessor cmd) { - setter.Invoke( - cmd.GetModel(), - parser.Parse( - argument.Name, - argument.Value, - convention.Application.ValueParsers.ParseCulture)); + if (argument.Values.Count == 0) + { + if (!ReflectionHelper.IsSpecialValueTupleType(prop.PropertyType, out _)) + { + var value = getter.Invoke(cmd.GetModel()); + if (value != null) + { + argument.Values.Add(value.ToString()); + argument.DefaultValue = value.ToString(); + } + } + } + else + { + setter.Invoke( + cmd.GetModel(), + parser.Parse( + argument.Name, + argument.Value, + convention.Application.ValueParsers.ParseCulture)); + } } }); } diff --git a/src/CommandLineUtils/Conventions/OptionAttributeConventionBase.cs b/src/CommandLineUtils/Conventions/OptionAttributeConventionBase.cs index bf6c1307..115b9443 100644 --- a/src/CommandLineUtils/Conventions/OptionAttributeConventionBase.cs +++ b/src/CommandLineUtils/Conventions/OptionAttributeConventionBase.cs @@ -2,6 +2,8 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; +using System.Collections; +using System.Collections.Generic; using System.ComponentModel.DataAnnotations; using System.Linq; using System.Reflection; @@ -57,19 +59,7 @@ private protected void AddOption(ConventionContext context, CommandOption option context.Application._longOptions.Add(option.LongName, prop); } - if (option.OptionType != CommandOptionType.NoValue) - { - if (!ReflectionHelper.IsSpecialValueTupleType(prop.PropertyType, out var type)) - { - context.Application.OnParsingComplete(_ => - { - var getter = ReflectionHelper.GetPropertyGetter(prop); - var value = getter.Invoke(modelAccessor.GetModel()); - option.DefaultValue = value?.ToString(); - }); - } - } - + var getter = ReflectionHelper.GetPropertyGetter(prop); var setter = ReflectionHelper.GetPropertySetter(prop); switch (option.OptionType) @@ -84,9 +74,19 @@ private protected void AddOption(ConventionContext context, CommandOption option { if (!option.HasValue()) { - return; + if (!ReflectionHelper.IsSpecialValueTupleType(prop.PropertyType, out var type)) + { + if (getter.Invoke(modelAccessor.GetModel()) is IEnumerable value) + { + option.Values.AddRange(value.Select(x => x?.ToString())); + option.DefaultValue = string.Join(", ", value.Select(x => x?.ToString())); + } + } + } + else + { + setter.Invoke(modelAccessor.GetModel(), collectionParser.Parse(option.LongName, option.Values)); } - setter.Invoke(modelAccessor.GetModel(), collectionParser.Parse(option.LongName, option.Values)); }); break; case CommandOptionType.SingleOrNoValue: @@ -100,9 +100,21 @@ private protected void AddOption(ConventionContext context, CommandOption option { if (!option.HasValue()) { - return; + if (!ReflectionHelper.IsSpecialValueTupleType(prop.PropertyType, out var type)) + { + var value = getter.Invoke(modelAccessor.GetModel()); + + if (value != null) + { + option.Values.Add(value.ToString()); + option.DefaultValue = value.ToString(); + } + } + } + else + { + setter.Invoke(modelAccessor.GetModel(), parser.Parse(option.LongName, option.Value(), context.Application.ValueParsers.ParseCulture)); } - setter.Invoke(modelAccessor.GetModel(), parser.Parse(option.LongName, option.Value(), context.Application.ValueParsers.ParseCulture)); }); break; case CommandOptionType.NoValue: diff --git a/test/CommandLineUtils.Tests/DefaultHelpTextGeneratorTests.cs b/test/CommandLineUtils.Tests/DefaultHelpTextGeneratorTests.cs index b7a4dc7d..a641c8e1 100644 --- a/test/CommandLineUtils.Tests/DefaultHelpTextGeneratorTests.cs +++ b/test/CommandLineUtils.Tests/DefaultHelpTextGeneratorTests.cs @@ -194,10 +194,12 @@ SomeNullableEnumArgument nullable enum arg desc. Options: -strOpt|--str-opt str option desc. - -rStrOpt|--r-str-opt restricted str option desc. + -rStrOpt|--r-str-opt restricted str option desc. Allowed values are: Foo, Bar. - -dStrOpt|--d-str-opt str option with default value desc. + -dStrOpt|--d-str-opt str option with default value desc. Default value is: Foo. + -dStrOpt2|--d-str-opt2 str array option with default value desc. + Default value is: Foo, Bar. -intOpt|--int-opt int option desc. Default value is: 0. -enumOpt|--verbosity enum option desc. @@ -219,17 +221,20 @@ SomeNullableEnumArgument nullable enum arg desc. public class MyApp { - [Option(ShortName = "strOpt", Description = "str option desc.")] + [Option(ShortName = "strOpt", ValueName = "STR_OPT", Description = "str option desc.")] public string strOpt { get; set; } - [Option(ShortName = "rStrOpt", Description = "restricted str option desc.")] + [Option(ShortName = "rStrOpt", ValueName = "STR_OPT", Description = "restricted str option desc.")] [Required] [AllowedValues("Foo", "Bar")] public string rStrOpt { get; set; } - [Option(ShortName = "dStrOpt", Description = "str option with default value desc.")] + [Option(ShortName = "dStrOpt", ValueName = "STR_OPT", Description = "str option with default value desc.")] public string dStrOpt { get; set; } = "Foo"; + [Option(ShortName = "dStrOpt2", ValueName = "STR_OPT", Description = "str array option with default value desc.")] + public string[] dStrOpt2 { get; set; } = new[] { "Foo", "Bar" }; + [Option(ShortName = "intOpt", Description = "int option desc.")] public int intOpt { get; set; }