diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 00000000..c6020604 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,31 @@ +--- +name: Bug report +about: Create a report to help us improve +title: '' +labels: '' +assignees: '' + +--- + +**Describe the bug** +A clear and concise description of what the bug is. + +**To Reproduce** +Either fork from this fiddle and paste link here: https://dotnetfiddle.net/mh9CjX + +or + +Steps to reproduce the behavior: +1. Go to '...' +2. Click on '....' +3. Scroll down to '....' +4. See error + +**Expected behavior** +A clear and concise description of what you expected to happen. + +**Screenshots** +If applicable, add screenshots to help explain your problem. + +**Additional context** +Add any other context about the problem here. diff --git a/CHANGELOG.md b/CHANGELOG.md index eb001542..eea8ad12 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,15 @@ All notable changes to this project will be documented in this file. CommandLineParser project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [2.9.0-preview2] + +### Added +- Properly assign arguments after a double dash to values, fix #605 by [@robnasby, PR# 610](https://github.com/commandlineparser/commandline/pull/610). + +### Changed +- Drop "Add multi-instance option support". + + ## [2.9.0-preview1] - 2020-7-24 ### Added diff --git a/README.md b/README.md index 68b10659..79a16fa7 100644 --- a/README.md +++ b/README.md @@ -40,7 +40,7 @@ __This library provides _hassle free_ command line parsing with a constantly upd - Support Mutable and Immutable types. - Support HelpText localization. - Support ordering of options in HelpText. -- Support [Mutually Exclusive Options](https://github.com/commandlineparser/commandline/wiki/Mutually-Exclusive-Options) and Options groups. +- Support [Mutually Exclusive Options](https://github.com/commandlineparser/commandline/wiki/Mutually-Exclusive-Options) and [Option groups](https://github.com/commandlineparser/commandline/wiki/Option-Groups). - Support named and value options. - Support Asynchronous programming with async and await. - Unparsing support: `CommandLine.Parser.Default.FormatCommandLine(T options)`. diff --git a/appveyor.yml b/appveyor.yml index 91a36832..d11d6abf 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -1,6 +1,6 @@ #version should be only changed with RELEASE eminent, see RELEASE.md -version: 2.9.0-ci-{build} +version: 2.9.1-ci-{build} image: Visual Studio 2019 @@ -44,18 +44,9 @@ on_failure: appveyor PushArtifact .\files.lst -DeploymentName "Failed Build File Listing" deploy: -- provider: GitHub - auth_token: - secure: hVyVwHl0JiVq0VxXB4VMRWbUtrGclIzadfnWFcWCQBLvbgMLahLBnWlwGglT63pZ - artifact: /.*(\.|\.s)nupkg/ - prerelease: false - force_update: true #fsharp package runs as separate build job, so have to force_update to add fsharp.nuget added - on: - APPVEYOR_REPO_TAG: true - - provider: NuGet api_key: - secure: e2gJJ3r6Uls5trJwryaudAZd49QniNfIjax/A+tfywlchSnIQVOzOQCO9tTSNccI + secure: llMIgYMuLHh9thyKMEAmkWraTaA9Zvcm1F8/yRwm0HCiPIt/ehR/GI4kJKyMTPyf artifact: /.*(\.|\.s)nupkg/ on: APPVEYOR_REPO_TAG: true diff --git a/demo/ReadText.LocalizedDemo/Properties/Resources.Designer.cs b/demo/ReadText.LocalizedDemo/Properties/Resources.Designer.cs index cc414359..f2ca4b31 100644 --- a/demo/ReadText.LocalizedDemo/Properties/Resources.Designer.cs +++ b/demo/ReadText.LocalizedDemo/Properties/Resources.Designer.cs @@ -331,7 +331,7 @@ public static string SentenceSequenceOutOfRangeErrorOption { } /// - /// Looks up a localized string similar to A sequence value not bound to option name is defined with few items than required.. + /// Looks up a localized string similar to A sequence value not bound to option name is defined with fewer items than required.. /// public static string SentenceSequenceOutOfRangeErrorValue { get { diff --git a/demo/ReadText.LocalizedDemo/Properties/Resources.resx b/demo/ReadText.LocalizedDemo/Properties/Resources.resx index afdea3d0..b002fc43 100644 --- a/demo/ReadText.LocalizedDemo/Properties/Resources.resx +++ b/demo/ReadText.LocalizedDemo/Properties/Resources.resx @@ -208,7 +208,7 @@ A sequence option '{0}' is defined with fewer or more items than required. - A sequence value not bound to option name is defined with few items than required. + A sequence value not bound to option name is defined with fewer items than required. Error setting value to option '{0}': {1} diff --git a/src/CommandLine/CastExtensions.cs b/src/CommandLine/CastExtensions.cs new file mode 100644 index 00000000..fa34928c --- /dev/null +++ b/src/CommandLine/CastExtensions.cs @@ -0,0 +1,100 @@ +using System; +using System.Linq; +using System.Reflection; + +namespace CommandLine +{ + internal static class CastExtensions + { + private const string ImplicitCastMethodName = "op_Implicit"; + private const string ExplicitCastMethodName = "op_Explicit"; + + public static bool CanCast(this Type baseType) + { + return baseType.CanImplicitCast() || baseType.CanExplicitCast(); + } + + public static bool CanCast(this object obj) + { + var objType = obj.GetType(); + return objType.CanCast(); + } + + public static T Cast(this object obj) + { + try + { + return (T) obj; + } + catch (InvalidCastException) + { + if (obj.CanImplicitCast()) + return obj.ImplicitCast(); + if (obj.CanExplicitCast()) + return obj.ExplicitCast(); + else + throw; + } + } + + private static bool CanImplicitCast(this Type baseType) + { + return baseType.CanCast(ImplicitCastMethodName); + } + + private static bool CanImplicitCast(this object obj) + { + var baseType = obj.GetType(); + return baseType.CanImplicitCast(); + } + + private static bool CanExplicitCast(this Type baseType) + { + return baseType.CanCast(ExplicitCastMethodName); + } + + private static bool CanExplicitCast(this object obj) + { + var baseType = obj.GetType(); + return baseType.CanExplicitCast(); + } + + private static bool CanCast(this Type baseType, string castMethodName) + { + var targetType = typeof(T); + return baseType.GetMethods(BindingFlags.Public | BindingFlags.Static) + .Where(mi => mi.Name == castMethodName && mi.ReturnType == targetType) + .Any(mi => + { + ParameterInfo pi = mi.GetParameters().FirstOrDefault(); + return pi != null && pi.ParameterType == baseType; + }); + } + + private static T ImplicitCast(this object obj) + { + return obj.Cast(ImplicitCastMethodName); + } + + private static T ExplicitCast(this object obj) + { + return obj.Cast(ExplicitCastMethodName); + } + + private static T Cast(this object obj, string castMethodName) + { + var objType = obj.GetType(); + MethodInfo conversionMethod = objType.GetMethods(BindingFlags.Public | BindingFlags.Static) + .Where(mi => mi.Name == castMethodName && mi.ReturnType == typeof(T)) + .SingleOrDefault(mi => + { + ParameterInfo pi = mi.GetParameters().FirstOrDefault(); + return pi != null && pi.ParameterType == objType; + }); + if (conversionMethod != null) + return (T) conversionMethod.Invoke(null, new[] {obj}); + else + throw new InvalidCastException($"No method to cast {objType.FullName} to {typeof(T).FullName}"); + } + } +} diff --git a/src/CommandLine/Core/SpecificationGuards.cs b/src/CommandLine/Core/SpecificationGuards.cs index b6d7d122..985dbca9 100644 --- a/src/CommandLine/Core/SpecificationGuards.cs +++ b/src/CommandLine/Core/SpecificationGuards.cs @@ -13,7 +13,8 @@ static class SpecificationGuards Tuple.Create(GuardAgainstScalarWithRange(), "Scalar option specifications do not support range specification."), Tuple.Create(GuardAgainstSequenceWithWrongRange(), "Bad range in sequence option specifications."), Tuple.Create(GuardAgainstSequenceWithZeroRange(), "Zero is not allowed in range of sequence option specifications."), - Tuple.Create(GuardAgainstOneCharLongName(), "Long name should be longer than one character.") + Tuple.Create(GuardAgainstOneCharLongName(), "Long name should be longer than one character."), + Tuple.Create(GaurdAgainstUnsupportedSequenceTypes(), "Unsupported sequence type specification.") }; private static Func GuardAgainstScalarWithRange() @@ -39,5 +40,10 @@ private static Func GuardAgainstSequenceWithZeroRange() && (spec.HavingMin(min => min == 0) || spec.HavingMax(max => max == 0)); } + + private static Func GaurdAgainstUnsupportedSequenceTypes() + { + return spec => spec.TargetType == TargetType.Sequence && spec.ConversionType.GetGenericArguments().Length != 1; + } } } diff --git a/src/CommandLine/Infrastructure/LocalizableAttributeProperty.cs b/src/CommandLine/Infrastructure/LocalizableAttributeProperty.cs index 33db2640..b8bd1398 100644 --- a/src/CommandLine/Infrastructure/LocalizableAttributeProperty.cs +++ b/src/CommandLine/Infrastructure/LocalizableAttributeProperty.cs @@ -1,8 +1,5 @@ using System; -using System.Collections.Generic; -using System.Linq; using System.Reflection; -using System.Text; namespace CommandLine.Infrastructure { @@ -43,15 +40,16 @@ private string GetLocalizedValue() return _value; if (_localizationPropertyInfo == null) { - // Static class IsAbstract + // Static class IsAbstract if (!_type.IsVisible) throw new ArgumentException($"Invalid resource type '{_type.FullName}'! {_type.Name} is not visible for the parser! Change resources 'Access Modifier' to 'Public'", _propertyName); PropertyInfo propertyInfo = _type.GetProperty(_value, BindingFlags.Public | BindingFlags.GetProperty | BindingFlags.Static); - if (propertyInfo == null || !propertyInfo.CanRead || propertyInfo.PropertyType != typeof(string)) + if (propertyInfo == null || !propertyInfo.CanRead || (propertyInfo.PropertyType != typeof(string) && !propertyInfo.PropertyType.CanCast())) throw new ArgumentException($"Invalid resource property name! Localized value: {_value}", _propertyName); _localizationPropertyInfo = propertyInfo; } - return (string)_localizationPropertyInfo.GetValue(null, null); + + return _localizationPropertyInfo.GetValue(null, null).Cast(); } } diff --git a/src/CommandLine/Text/SentenceBuilder.cs b/src/CommandLine/Text/SentenceBuilder.cs index c8537542..842ae675 100644 --- a/src/CommandLine/Text/SentenceBuilder.cs +++ b/src/CommandLine/Text/SentenceBuilder.cs @@ -138,7 +138,7 @@ public override Func FormatError case ErrorType.SequenceOutOfRangeError: var seqOutRange = ((SequenceOutOfRangeError)error); return seqOutRange.NameInfo.Equals(NameInfo.EmptyName) - ? "A sequence value not bound to option name is defined with few items than required." + ? "A sequence value not bound to option name is defined with fewer items than required." : "A sequence option '".JoinTo(seqOutRange.NameInfo.NameText, "' is defined with fewer or more items than required."); case ErrorType.BadVerbSelectedError: diff --git a/tests/CommandLine.Tests/Fakes/Options_With_Dictionary_Specification.cs b/tests/CommandLine.Tests/Fakes/Options_With_Dictionary_Specification.cs new file mode 100644 index 00000000..0b5382a2 --- /dev/null +++ b/tests/CommandLine.Tests/Fakes/Options_With_Dictionary_Specification.cs @@ -0,0 +1,14 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace CommandLine.Tests.Fakes +{ + class Options_With_Dictionary_Specification + { + [Option('d', "dict")] + public Dictionary KeyValuePairs { get; set; } + } +} diff --git a/tests/CommandLine.Tests/Fakes/ResourceFakes.cs b/tests/CommandLine.Tests/Fakes/ResourceFakes.cs index f7b46bac..1b18da6e 100644 --- a/tests/CommandLine.Tests/Fakes/ResourceFakes.cs +++ b/tests/CommandLine.Tests/Fakes/ResourceFakes.cs @@ -3,6 +3,10 @@ public static class StaticResource { public static string HelpText { get { return "Localized HelpText"; } } + public static TypeWithImplicitCast ImplicitCastHelpText => new TypeWithImplicitCast("Localized HelpText"); + public static TypeWithExplicitCast ExplicitCastHelpText => new TypeWithExplicitCast("Localized HelpText"); + public static TypeWithWrongImplicitCast WrongImplicitCastHelpText => new TypeWithWrongImplicitCast(); + public static TypeWithWrongExplicitCast WrongExplicitCastHelpText => new TypeWithWrongExplicitCast(); } public class NonStaticResource @@ -10,6 +14,10 @@ public class NonStaticResource public static string HelpText { get { return "Localized HelpText"; } } public static string WriteOnlyText { set { value?.ToString(); } } private static string PrivateHelpText { get { return "Localized HelpText"; } } + public static TypeWithImplicitCast ImplicitCastHelpText => new TypeWithImplicitCast("Localized HelpText"); + public static TypeWithExplicitCast ExplicitCastHelpText => new TypeWithExplicitCast("Localized HelpText"); + public static TypeWithWrongImplicitCast WrongImplicitCastHelpText => new TypeWithWrongImplicitCast(); + public static TypeWithWrongExplicitCast WrongExplicitCastHelpText => new TypeWithWrongExplicitCast(); } public class NonStaticResource_WithNonStaticProperty @@ -22,4 +30,59 @@ internal class InternalResource public static string HelpText { get { return "Localized HelpText"; } } } + public class TypeWithImplicitCast + { + private string value; + + public TypeWithImplicitCast(string value) + { + this.value = value; + } + + public static implicit operator string(TypeWithImplicitCast obj) + { + return obj.value; + } + + public static implicit operator int(TypeWithImplicitCast obj) + { + return 0; + } + } + + public class TypeWithWrongImplicitCast + { + public static implicit operator int(TypeWithWrongImplicitCast obj) + { + return 0; + } + } + + public class TypeWithExplicitCast + { + private string value; + + public TypeWithExplicitCast(string value) + { + this.value = value; + } + + public static explicit operator string(TypeWithExplicitCast obj) + { + return obj.value; + } + + public static explicit operator int(TypeWithExplicitCast obj) + { + return 0; + } + } + + public class TypeWithWrongExplicitCast + { + public static explicit operator int(TypeWithWrongExplicitCast obj) + { + return 0; + } + } } diff --git a/tests/CommandLine.Tests/Unit/BaseAttributeTests.cs b/tests/CommandLine.Tests/Unit/BaseAttributeTests.cs index 3c2bfbbd..ab566255 100644 --- a/tests/CommandLine.Tests/Unit/BaseAttributeTests.cs +++ b/tests/CommandLine.Tests/Unit/BaseAttributeTests.cs @@ -21,12 +21,16 @@ public static void Default(object defaultValue) [InlineData("Help text", null, "Help text")] [InlineData("HelpText", typeof(Fakes.StaticResource), "Localized HelpText")] [InlineData("HelpText", typeof(Fakes.NonStaticResource), "Localized HelpText")] + [InlineData("ImplicitCastHelpText", typeof(Fakes.StaticResource), "Localized HelpText")] + [InlineData("ImplicitCastHelpText", typeof(Fakes.NonStaticResource), "Localized HelpText")] + [InlineData("ExplicitCastHelpText", typeof(Fakes.StaticResource), "Localized HelpText")] + [InlineData("ExplicitCastHelpText", typeof(Fakes.NonStaticResource), "Localized HelpText")] public static void HelpText(string helpText, Type resourceType, string expected) { TestBaseAttribute baseAttribute = new TestBaseAttribute(); baseAttribute.HelpText = helpText; baseAttribute.ResourceType = resourceType; - + Assert.Equal(expected, baseAttribute.HelpText); } @@ -35,6 +39,10 @@ public static void HelpText(string helpText, Type resourceType, string expected) [InlineData("WriteOnlyText", typeof(Fakes.NonStaticResource))] [InlineData("PrivateOnlyText", typeof(Fakes.NonStaticResource))] [InlineData("HelpText", typeof(Fakes.InternalResource))] + [InlineData("WrongImplicitCastHelpText", typeof(Fakes.StaticResource))] + [InlineData("WrongExplicitCastHelpText", typeof(Fakes.StaticResource))] + [InlineData("WrongImplicitCastHelpText", typeof(Fakes.NonStaticResource))] + [InlineData("WrongExplicitCastHelpText", typeof(Fakes.NonStaticResource))] public void ThrowsHelpText(string helpText, Type resourceType) { TestBaseAttribute baseAttribute = new TestBaseAttribute(); diff --git a/tests/CommandLine.Tests/Unit/Core/InstanceBuilderTests.cs b/tests/CommandLine.Tests/Unit/Core/InstanceBuilderTests.cs index 2f8d02b7..f3e888ea 100644 --- a/tests/CommandLine.Tests/Unit/Core/InstanceBuilderTests.cs +++ b/tests/CommandLine.Tests/Unit/Core/InstanceBuilderTests.cs @@ -685,6 +685,17 @@ public void Parse_fsharp_option_int(string[] arguments, int expectedValue, bool #endif + [Fact] + public void Dictionary_type_throws_exception() + { + // Exercize system + Action test = () => InvokeBuild( + new string[] { }); + + // Verify outcome + Assert.Throws(test); + } + [Fact] public void Min_constraint_set_to_zero_throws_exception() {