From d443a51aeb3a418425e970542b3b96e9da5f62e2 Mon Sep 17 00:00:00 2001 From: moh-hassan Date: Thu, 6 Aug 2020 12:59:37 +0300 Subject: [PATCH 01/11] Update changelog --- CHANGELOG.md | 9 +++++++++ 1 file changed, 9 insertions(+) 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 From 2e1fff3657ef52a1633d9586edd866fca07e0538 Mon Sep 17 00:00:00 2001 From: Misha Gorshenin Date: Sat, 22 May 2021 22:23:01 +0500 Subject: [PATCH 02/11] Add cast support for resource types --- .gitignore | 1 + src/CommandLine/CastExtensions.cs | 100 ++++++++++++++++++ src/CommandLine/CommandLine.csproj | 1 + .../LocalizableAttributeProperty.cs | 12 +-- .../CommandLine.Tests/Fakes/ResourceFakes.cs | 63 +++++++++++ .../Unit/BaseAttributeTests.cs | 10 +- 6 files changed, 179 insertions(+), 8 deletions(-) create mode 100644 src/CommandLine/CastExtensions.cs diff --git a/.gitignore b/.gitignore index 55f900a1..7cccb220 100644 --- a/.gitignore +++ b/.gitignore @@ -37,5 +37,6 @@ artifacts/* *.DotSettings.user # Visual Studio 2015 cache/options directory .vs/ +.idea/ [R|r]elease/** diff --git a/src/CommandLine/CastExtensions.cs b/src/CommandLine/CastExtensions.cs new file mode 100644 index 00000000..828a18e4 --- /dev/null +++ b/src/CommandLine/CastExtensions.cs @@ -0,0 +1,100 @@ +using System; +using System.Linq; +using System.Reflection; + +namespace CommandLine +{ + public 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/CommandLine.csproj b/src/CommandLine/CommandLine.csproj index 04496eb8..b2e65413 100644 --- a/src/CommandLine/CommandLine.csproj +++ b/src/CommandLine/CommandLine.csproj @@ -27,6 +27,7 @@ 8.0 true snupkg + 5.0.0 diff --git a/src/CommandLine/Infrastructure/LocalizableAttributeProperty.cs b/src/CommandLine/Infrastructure/LocalizableAttributeProperty.cs index 9edd621b..26714cca 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)) + PropertyInfo propertyInfo = _type.GetProperty(_value, BindingFlags.Public | BindingFlags.Static); + 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/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(); From 5b693d5386de984c855586b0a1b608275314dbd2 Mon Sep 17 00:00:00 2001 From: Misha Gorshenin Date: Sat, 22 May 2021 22:35:45 +0500 Subject: [PATCH 03/11] Reset nuget version --- src/CommandLine/CommandLine.csproj | 1 - 1 file changed, 1 deletion(-) diff --git a/src/CommandLine/CommandLine.csproj b/src/CommandLine/CommandLine.csproj index b2e65413..04496eb8 100644 --- a/src/CommandLine/CommandLine.csproj +++ b/src/CommandLine/CommandLine.csproj @@ -27,7 +27,6 @@ 8.0 true snupkg - 5.0.0 From 705b421b6f0bc93a1de72af0c1266e2a51593925 Mon Sep 17 00:00:00 2001 From: Mike Perrin Date: Mon, 8 Nov 2021 12:33:21 +0000 Subject: [PATCH 04/11] added link to Option groups --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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)`. From dffb7517a7b7e0a46a5b6cc17259a0220fc4fe28 Mon Sep 17 00:00:00 2001 From: Eric Newton Date: Thu, 12 May 2022 16:26:27 -0400 Subject: [PATCH 05/11] update nuget api key --- appveyor.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/appveyor.yml b/appveyor.yml index 91a36832..30fc9fe9 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -55,7 +55,7 @@ deploy: - provider: NuGet api_key: - secure: e2gJJ3r6Uls5trJwryaudAZd49QniNfIjax/A+tfywlchSnIQVOzOQCO9tTSNccI + secure: llMIgYMuLHh9thyKMEAmkWraTaA9Zvcm1F8/yRwm0HCiPIt/ehR/GI4kJKyMTPyf artifact: /.*(\.|\.s)nupkg/ on: APPVEYOR_REPO_TAG: true From b0b0ec9f5470bacc0823c013a599ba3b4e115900 Mon Sep 17 00:00:00 2001 From: Eric Newton Date: Thu, 12 May 2022 17:07:06 -0400 Subject: [PATCH 06/11] update appveyor, remove github deployment for now, bump version to 2.9.1 --- appveyor.yml | 11 +---------- 1 file changed, 1 insertion(+), 10 deletions(-) diff --git a/appveyor.yml b/appveyor.yml index 30fc9fe9..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,15 +44,6 @@ 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: llMIgYMuLHh9thyKMEAmkWraTaA9Zvcm1F8/yRwm0HCiPIt/ehR/GI4kJKyMTPyf From 5ece267c5bf988a1135be49dac76c2c81c73bff4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=93=D0=BE=D1=80=D1=88=D0=B5=D0=BD=D0=B8=D0=BD=20=D0=9C?= =?UTF-8?q?=D0=B8=D1=85=D0=B0=D0=B8=D0=BB=20=28Gorshenin=5FMV=29?= Date: Tue, 7 Jun 2022 18:29:11 +0500 Subject: [PATCH 07/11] mark CastExtensions as internal class --- src/CommandLine/CastExtensions.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/CommandLine/CastExtensions.cs b/src/CommandLine/CastExtensions.cs index 828a18e4..fa34928c 100644 --- a/src/CommandLine/CastExtensions.cs +++ b/src/CommandLine/CastExtensions.cs @@ -4,7 +4,7 @@ namespace CommandLine { - public static class CastExtensions + internal static class CastExtensions { private const string ImplicitCastMethodName = "op_Implicit"; private const string ExplicitCastMethodName = "op_Explicit"; From 72677c5858f7b41117f31f366e8cdb03ccafd940 Mon Sep 17 00:00:00 2001 From: Bjarte Aarmo Lund Date: Tue, 20 Dec 2022 15:33:20 +0100 Subject: [PATCH 08/11] A minor spelling mistake (with few items => with fewer items) --- demo/ReadText.LocalizedDemo/Properties/Resources.Designer.cs | 2 +- demo/ReadText.LocalizedDemo/Properties/Resources.resx | 2 +- src/CommandLine/Text/SentenceBuilder.cs | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) 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/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: From 6514645610d6f56ebb143e69516a986ec2aefe78 Mon Sep 17 00:00:00 2001 From: Eric Newton Date: Tue, 7 Mar 2023 13:41:45 -0500 Subject: [PATCH 09/11] Update issue templates --- .github/ISSUE_TEMPLATE/bug_report.md | 29 ++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) create mode 100644 .github/ISSUE_TEMPLATE/bug_report.md diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 00000000..29c526ef --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,29 @@ +--- +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** +Fork from this fiddle and paste link here: https://dotnetfiddle.net/mh9CjX + +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. From 1e3607b97af6141743edb3c434c06d5b492f6fb3 Mon Sep 17 00:00:00 2001 From: Eric Newton Date: Tue, 7 Mar 2023 13:42:40 -0500 Subject: [PATCH 10/11] Update bug_report.md --- .github/ISSUE_TEMPLATE/bug_report.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index 29c526ef..c6020604 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -11,7 +11,9 @@ assignees: '' A clear and concise description of what the bug is. **To Reproduce** -Fork from this fiddle and paste link here: https://dotnetfiddle.net/mh9CjX +Either fork from this fiddle and paste link here: https://dotnetfiddle.net/mh9CjX + +or Steps to reproduce the behavior: 1. Go to '...' From 0efa3fcbb7b6fc0d086fc3dd25834daacf09f1a9 Mon Sep 17 00:00:00 2001 From: Ignacio Calvo Date: Thu, 20 Apr 2023 19:21:09 +0200 Subject: [PATCH 11/11] First implementation --- src/CommandLine/Core/InstanceBuilder.cs | 2 +- src/CommandLine/Core/NameExtensions.cs | 7 ++- src/CommandLine/Core/NameLookup.cs | 4 +- src/CommandLine/Core/OptionMapper.cs | 2 +- src/CommandLine/Core/OptionSpecification.cs | 30 +++++++-- src/CommandLine/Core/Specification.cs | 2 +- .../Core/SpecificationExtensions.cs | 4 +- src/CommandLine/Core/SpecificationGuards.cs | 7 ++- .../Core/SpecificationPropertyRules.cs | 15 +++-- src/CommandLine/Core/TypeLookup.cs | 4 +- src/CommandLine/NameInfo.cs | 48 +++++++++++---- src/CommandLine/OptionAttribute.cs | 38 +++++++++--- src/CommandLine/Text/HelpText.cs | 61 +++++++++++++------ src/CommandLine/UnParserExtensions.cs | 4 +- tests/CommandLine.Tests/Fakes/Verb_Fakes.cs | 14 ++++- .../Unit/Text/HelpTextTests.cs | 30 +++++++++ .../Unit/UnParserExtensionsTests.cs | 51 ++++++++++++++++ 17 files changed, 255 insertions(+), 68 deletions(-) diff --git a/src/CommandLine/Core/InstanceBuilder.cs b/src/CommandLine/Core/InstanceBuilder.cs index f48127b1..2dcc35a8 100644 --- a/src/CommandLine/Core/InstanceBuilder.cs +++ b/src/CommandLine/Core/InstanceBuilder.cs @@ -100,7 +100,7 @@ public static ParserResult Build( var missingValueErrors = from token in errorsPartition select new MissingValueOptionError( - optionSpecs.Single(o => token.Text.MatchName(o.ShortName, o.LongName, nameComparer)) + optionSpecs.Single(o => token.Text.MatchName(o.ShortName, o.LongNames, nameComparer)) .FromOptionSpecification()); var specPropsWithValue = diff --git a/src/CommandLine/Core/NameExtensions.cs b/src/CommandLine/Core/NameExtensions.cs index 405e44f5..3165a3ca 100644 --- a/src/CommandLine/Core/NameExtensions.cs +++ b/src/CommandLine/Core/NameExtensions.cs @@ -1,23 +1,24 @@ // Copyright 2005-2015 Giacomo Stelluti Scala & Contributors. All rights reserved. See License.md in the project root for license information. using System; +using System.Linq; namespace CommandLine.Core { static class NameExtensions { - public static bool MatchName(this string value, string shortName, string longName, StringComparer comparer) + public static bool MatchName(this string value, string shortName, string[] longNames, StringComparer comparer) { return value.Length == 1 ? comparer.Equals(value, shortName) - : comparer.Equals(value, longName); + : longNames.Any(longName => comparer.Equals(value, longName)); } public static NameInfo FromOptionSpecification(this OptionSpecification specification) { return new NameInfo( specification.ShortName, - specification.LongName); + specification.LongNames); } public static NameInfo FromSpecification(this Specification specification) diff --git a/src/CommandLine/Core/NameLookup.cs b/src/CommandLine/Core/NameLookup.cs index ccb24ea5..aa5c8839 100644 --- a/src/CommandLine/Core/NameLookup.cs +++ b/src/CommandLine/Core/NameLookup.cs @@ -18,7 +18,7 @@ static class NameLookup { public static NameLookupResult Contains(string name, IEnumerable specifications, StringComparer comparer) { - var option = specifications.FirstOrDefault(a => name.MatchName(a.ShortName, a.LongName, comparer)); + var option = specifications.FirstOrDefault(a => name.MatchName(a.ShortName, a.LongNames, comparer)); if (option == null) return NameLookupResult.NoOptionFound; return option.ConversionType == typeof(bool) || (option.ConversionType == typeof(int) && option.FlagCounter) ? NameLookupResult.BooleanOptionFound @@ -29,7 +29,7 @@ public static Maybe HavingSeparator(string name, IEnumerable name.MatchName(a.ShortName, a.LongName, comparer) && a.Separator != '\0') + a => name.MatchName(a.ShortName, a.LongNames, comparer) && a.Separator != '\0') .ToMaybe() .MapValueOrDefault(spec => Maybe.Just(spec.Separator), Maybe.Nothing()); } diff --git a/src/CommandLine/Core/OptionMapper.cs b/src/CommandLine/Core/OptionMapper.cs index ded42c4f..0eb984d7 100644 --- a/src/CommandLine/Core/OptionMapper.cs +++ b/src/CommandLine/Core/OptionMapper.cs @@ -23,7 +23,7 @@ public static Result< pt => { var matched = options.Where(s => - s.Key.MatchName(((OptionSpecification)pt.Specification).ShortName, ((OptionSpecification)pt.Specification).LongName, comparer)).ToMaybe(); + s.Key.MatchName(((OptionSpecification)pt.Specification).ShortName, ((OptionSpecification)pt.Specification).LongNames, comparer)).ToMaybe(); if (matched.IsJust()) { var matches = matched.GetValueOrDefault(Enumerable.Empty>>()); diff --git a/src/CommandLine/Core/OptionSpecification.cs b/src/CommandLine/Core/OptionSpecification.cs index 1c2e4f88..87e1199a 100644 --- a/src/CommandLine/Core/OptionSpecification.cs +++ b/src/CommandLine/Core/OptionSpecification.cs @@ -10,7 +10,7 @@ namespace CommandLine.Core sealed class OptionSpecification : Specification { private readonly string shortName; - private readonly string longName; + private readonly string[] longNames; private readonly char separator; private readonly string setName; private readonly string group; @@ -23,7 +23,21 @@ public OptionSpecification(string shortName, string longName, bool required, str required, min, max, defaultValue, helpText, metaValue, enumValues, conversionType, conversionType == typeof(int) && flagCounter ? TargetType.Switch : targetType, hidden) { this.shortName = shortName; - this.longName = longName; + this.longNames = new [] { longName }; + this.separator = separator; + this.setName = setName; + this.group = group; + this.flagCounter = flagCounter; + } + + public OptionSpecification(string shortName, string[] longNames, bool required, string setName, Maybe min, Maybe max, + char separator, Maybe defaultValue, string helpText, string metaValue, IEnumerable enumValues, + Type conversionType, TargetType targetType, string group, bool flagCounter = false, bool hidden = false) + : base(SpecificationType.Option, + required, min, max, defaultValue, helpText, metaValue, enumValues, conversionType, conversionType == typeof(int) && flagCounter ? TargetType.Switch : targetType, hidden) + { + this.shortName = shortName; + this.longNames = longNames; this.separator = separator; this.setName = setName; this.group = group; @@ -34,7 +48,7 @@ public static OptionSpecification FromAttribute(OptionAttribute attribute, Type { return new OptionSpecification( attribute.ShortName, - attribute.LongName, + attribute.LongNames, attribute.Required, attribute.SetName, attribute.Min == -1 ? Maybe.Nothing() : Maybe.Just(attribute.Min), @@ -57,14 +71,20 @@ public static OptionSpecification NewSwitch(string shortName, string longName, b '\0', Maybe.Nothing(), helpText, metaValue, Enumerable.Empty(), typeof(bool), TargetType.Switch, string.Empty, false, hidden); } + public static OptionSpecification NewSwitch(string shortName, string[] longNames, bool required, string helpText, string metaValue, bool hidden = false) + { + return new OptionSpecification(shortName, longNames, required, string.Empty, Maybe.Nothing(), Maybe.Nothing(), + '\0', Maybe.Nothing(), helpText, metaValue, Enumerable.Empty(), typeof(bool), TargetType.Switch, string.Empty, false, hidden); + } + public string ShortName { get { return shortName; } } - public string LongName + public string[] LongNames { - get { return longName; } + get { return longNames; } } public char Separator diff --git a/src/CommandLine/Core/Specification.cs b/src/CommandLine/Core/Specification.cs index b95b998c..3fc12ca7 100644 --- a/src/CommandLine/Core/Specification.cs +++ b/src/CommandLine/Core/Specification.cs @@ -118,7 +118,7 @@ public static Specification FromProperty(PropertyInfo property) var spec = OptionSpecification.FromAttribute(oa.Single(), property.PropertyType, ReflectionHelper.GetNamesOfEnum(property.PropertyType)); - if (spec.ShortName.Length == 0 && spec.LongName.Length == 0) + if (spec.ShortName.Length == 0 && spec.LongNames.Length == 0) { return spec.WithLongName(property.Name.ToLowerInvariant()); } diff --git a/src/CommandLine/Core/SpecificationExtensions.cs b/src/CommandLine/Core/SpecificationExtensions.cs index c080e983..e7c796d8 100644 --- a/src/CommandLine/Core/SpecificationExtensions.cs +++ b/src/CommandLine/Core/SpecificationExtensions.cs @@ -22,7 +22,7 @@ public static OptionSpecification WithLongName(this OptionSpecification specific { return new OptionSpecification( specification.ShortName, - newLongName, + new [] { newLongName }, specification.Required, specification.SetName, specification.Min, @@ -41,7 +41,7 @@ public static OptionSpecification WithLongName(this OptionSpecification specific public static string UniqueName(this OptionSpecification specification) { - return specification.ShortName.Length > 0 ? specification.ShortName : specification.LongName; + return specification.ShortName.Length > 0 ? specification.ShortName : specification.LongNames[0]; } public static IEnumerable ThrowingValidate(this IEnumerable specifications, IEnumerable, string>> guardsLookup) diff --git a/src/CommandLine/Core/SpecificationGuards.cs b/src/CommandLine/Core/SpecificationGuards.cs index b6d7d122..f1fbd1f0 100644 --- a/src/CommandLine/Core/SpecificationGuards.cs +++ b/src/CommandLine/Core/SpecificationGuards.cs @@ -2,6 +2,7 @@ using System; using System.Collections.Generic; +using System.Linq; using CSharpx; namespace CommandLine.Core @@ -30,7 +31,11 @@ private static Func GuardAgainstSequenceWithWrongRange() private static Func GuardAgainstOneCharLongName() { - return spec => spec.IsOption() && ((OptionSpecification)spec).LongName.Length == 1; + return spec => + { + var optionSpecification = spec as OptionSpecification; + return spec.IsOption() && (optionSpecification?.LongNames.Any(x => x.Length == 1) ?? false); + }; } private static Func GuardAgainstSequenceWithZeroRange() diff --git a/src/CommandLine/Core/SpecificationPropertyRules.cs b/src/CommandLine/Core/SpecificationPropertyRules.cs index 4f8b78a9..fcf268bd 100644 --- a/src/CommandLine/Core/SpecificationPropertyRules.cs +++ b/src/CommandLine/Core/SpecificationPropertyRules.cs @@ -48,7 +48,7 @@ where o.Group.Length > 0 if (options.Any()) { return from o in options - select new GroupOptionAmbiguityError(new NameInfo(o.ShortName, o.LongName)); + select new GroupOptionAmbiguityError(new NameInfo(o.ShortName, o.LongNames)); } return Enumerable.Empty(); @@ -79,7 +79,7 @@ group o by o.Option.Group into g if (errorGroups.Any()) { - return errorGroups.Select(gr => new MissingGroupOptionError(gr.Key, gr.Select(g => new NameInfo(g.Option.ShortName, g.Option.LongName)))); + return errorGroups.Select(gr => new MissingGroupOptionError(gr.Key, gr.Select(g => new NameInfo(g.Option.ShortName, g.Option.LongNames)))); } return Enumerable.Empty(); @@ -199,20 +199,19 @@ where t.IsName() join o in specs on t.Text equals o.ShortName into to from o in to.DefaultIfEmpty() where o != null - select new { o.ShortName, o.LongName }; + select new { o.ShortName, o.LongNames }; var longOptions = from t in tokens where t.IsName() - join o in specs on t.Text equals o.LongName into to - from o in to.DefaultIfEmpty() - where o != null - select new { o.ShortName, o.LongName }; + from o in specs + where o.LongNames.Contains(t.Text) + select new { o.ShortName, o.LongNames }; var groups = from x in shortOptions.Concat(longOptions) group x by x into g let count = g.Count() select new { Value = g.Key, Count = count }; var errors = from y in groups where y.Count > 1 - select new RepeatedOptionError(new NameInfo(y.Value.ShortName, y.Value.LongName)); + select new RepeatedOptionError(new NameInfo(y.Value.ShortName, y.Value.LongNames)); return errors; }; } diff --git a/src/CommandLine/Core/TypeLookup.cs b/src/CommandLine/Core/TypeLookup.cs index 24515a4f..d78bdc48 100644 --- a/src/CommandLine/Core/TypeLookup.cs +++ b/src/CommandLine/Core/TypeLookup.cs @@ -15,7 +15,7 @@ public static Maybe FindTypeDescriptorAndSibling( StringComparer comparer) { var info = - specifications.SingleOrDefault(a => name.MatchName(a.ShortName, a.LongName, comparer)) + specifications.SingleOrDefault(a => name.MatchName(a.ShortName, a.LongNames, comparer)) .ToMaybe() .Map( first => @@ -31,4 +31,4 @@ public static Maybe FindTypeDescriptorAndSibling( } } -} \ No newline at end of file +} diff --git a/src/CommandLine/NameInfo.cs b/src/CommandLine/NameInfo.cs index baf259ef..9725406d 100644 --- a/src/CommandLine/NameInfo.cs +++ b/src/CommandLine/NameInfo.cs @@ -1,7 +1,7 @@ // Copyright 2005-2015 Giacomo Stelluti Scala & Contributors. All rights reserved. See License.md in the project root for license information. using System; -using CommandLine.Core; +using System.Linq; namespace CommandLine { @@ -14,16 +14,40 @@ public sealed class NameInfo : IEquatable /// Represents an empty name information. Used when are tied to values, /// rather than options. /// - public static readonly NameInfo EmptyName = new NameInfo(string.Empty, string.Empty); - private readonly string longName; + public static readonly NameInfo EmptyName = new NameInfo(string.Empty, new string[0]); + private readonly string[] longNames; private readonly string shortName; + internal NameInfo(string shortName) + { + if (shortName == null) throw new ArgumentNullException("shortName"); + + this.longNames = new string[0]; + this.shortName = shortName; + } + internal NameInfo(string shortName, string longName) { if (shortName == null) throw new ArgumentNullException("shortName"); if (longName == null) throw new ArgumentNullException("longName"); + if (longName == string.Empty) + { + this.longNames = new string[0]; + } + else + { + this.longNames = new [] { longName }; + } - this.longName = longName; + this.shortName = shortName; + } + + internal NameInfo(string shortName, string[] longNames) + { + if (shortName == null) throw new ArgumentNullException("shortName"); + if (longNames == null) throw new ArgumentNullException("longNames"); + if (longNames.Any(x => x == null)) throw new ArgumentNullException("longNames"); + this.longNames = longNames; this.shortName = shortName; } @@ -38,9 +62,9 @@ public string ShortName /// /// Gets the long name of the name information. /// - public string LongName + public string[] LongNames { - get { return longName; } + get { return longNames; } } /// @@ -50,11 +74,11 @@ public string NameText { get { - return ShortName.Length > 0 && LongName.Length > 0 - ? ShortName + ", " + LongName + return ShortName.Length > 0 && LongNames.Length > 0 + ? ShortName + ", " + string.Join(", ", LongNames) : ShortName.Length > 0 ? ShortName - : LongName; + : string.Join(", ", LongNames); } } @@ -80,7 +104,7 @@ public override bool Equals(object obj) /// A hash code for the current . public override int GetHashCode() { - return new { ShortName, LongName }.GetHashCode(); + return CSharpx.EnumerableExtensions.Prepend(LongNames, ShortName).ToArray().GetHashCode(); } /// @@ -95,7 +119,7 @@ public bool Equals(NameInfo other) return false; } - return ShortName.Equals(other.ShortName) && LongName.Equals(other.LongName); + return ShortName.Equals(other.ShortName) && LongNames.SequenceEqual(other.LongNames); } } -} \ No newline at end of file +} diff --git a/src/CommandLine/OptionAttribute.cs b/src/CommandLine/OptionAttribute.cs index 6ae51dac..b7f6d22d 100644 --- a/src/CommandLine/OptionAttribute.cs +++ b/src/CommandLine/OptionAttribute.cs @@ -12,20 +12,20 @@ namespace CommandLine [AttributeUsage(AttributeTargets.Property, AllowMultiple = false, Inherited = true)] public sealed class OptionAttribute : BaseAttribute { - private readonly string longName; + private readonly string[] longNames; private readonly string shortName; private string setName; private bool flagCounter; private char separator; private string group=string.Empty; - private OptionAttribute(string shortName, string longName) : base() + private OptionAttribute(string shortName, string[] longNames) : base() { if (shortName == null) throw new ArgumentNullException("shortName"); - if (longName == null) throw new ArgumentNullException("longName"); + if (longNames == null) throw new ArgumentNullException("longNames"); this.shortName = shortName; - this.longName = longName; + this.longNames = longNames; setName = string.Empty; separator = '\0'; } @@ -35,7 +35,7 @@ private OptionAttribute(string shortName, string longName) : base() /// The default long name will be inferred from target property. /// public OptionAttribute() - : this(string.Empty, string.Empty) + : this(string.Empty, new string[0]) { } @@ -44,17 +44,35 @@ public OptionAttribute() /// /// The long name of the option. public OptionAttribute(string longName) - : this(string.Empty, longName) + : this(string.Empty, new []{ longName }) { } + /// + /// Initializes a new instance of the class. + /// + /// The long name of the option. + public OptionAttribute(string[] longNames) + : this(string.Empty, longNames) + { + } /// /// Initializes a new instance of the class. /// /// The short name of the option. /// The long name of the option or null if not used. public OptionAttribute(char shortName, string longName) - : this(shortName.ToOneCharString(), longName) + : this(shortName.ToOneCharString(), new []{ longName }) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The short name of the option. + /// The long name of the option or null if not used. + public OptionAttribute(char shortName, string[] longNames) + : this(shortName.ToOneCharString(), longNames) { } @@ -63,16 +81,16 @@ public OptionAttribute(char shortName, string longName) /// /// The short name of the option.. public OptionAttribute(char shortName) - : this(shortName.ToOneCharString(), string.Empty) + : this(shortName.ToOneCharString(), new string[0]) { } /// /// Gets long name of this command line option. This name is usually a single english word. /// - public string LongName + public string[] LongNames { - get { return longName; } + get { return longNames; } } /// diff --git a/src/CommandLine/Text/HelpText.cs b/src/CommandLine/Text/HelpText.cs index f5e9a7b9..18c5c102 100644 --- a/src/CommandLine/Text/HelpText.cs +++ b/src/CommandLine/Text/HelpText.cs @@ -27,7 +27,7 @@ public struct ComparableOption public bool Required; public bool IsOption; public bool IsValue; - public string LongName; + public string[] LongNames; public string ShortName; public int Index; } @@ -48,7 +48,7 @@ ComparableOption ToComparableOption(Specification spec, int index) Required = required, IsOption = option != null, IsValue = value != null, - LongName = option?.LongName ?? value?.MetaName, + LongNames = option?.LongNames ?? new [] { value?.MetaName }, ShortName = option?.ShortName, Index = index }; @@ -70,8 +70,18 @@ ComparableOption ToComparableOption(Specification spec, int index) return 1; } - return String.Compare(attr1.LongName, attr2.LongName, StringComparison.Ordinal); + if (attr1.LongNames.Length != attr2.LongNames.Length) + { + return attr1.LongNames.Length.CompareTo(attr2.LongNames.Length); + } + for (int i = 0; i < attr1.LongNames.Length; i++) + { + var cmp = String.Compare(attr1.LongNames[i], attr2.LongNames[i], StringComparison.Ordinal); + if (cmp != 0) return cmp; + } + + return 0; } else if (attr1.IsOption && attr2.IsValue) { @@ -95,6 +105,8 @@ ComparableOption ToComparableOption(Specification spec, int index) /// The width of the option prefix (either "--" or " " /// private const int OptionPrefixWidth = 2; + + private const string CommaSeparation = ", "; /// /// The total amount of extra space that needs to accounted for when indenting Option help text /// @@ -852,7 +864,7 @@ private IEnumerable AdaptVerbsToSpecifications(IEnumerable select OptionSpecification.NewSwitch( string.Empty, - verbTuple.Item1.Name.Concat(verbTuple.Item1.Aliases).ToDelimitedString(", "), + verbTuple.Item1.Name.Concat(verbTuple.Item1.Aliases).ToDelimitedString(CommaSeparation), false, verbTuple.Item1.IsDefault ? "(Default Verb) " + verbTuple.Item1.HelpText : verbTuple.Item1.HelpText, //Default verb string.Empty, @@ -910,7 +922,7 @@ private OptionSpecification MakeHelpEntry() { return OptionSpecification.NewSwitch( string.Empty, - "help", + new [] { "help" }, false, sentenceBuilder.HelpCommandText(AddDashesToOption), string.Empty, @@ -921,7 +933,7 @@ private OptionSpecification MakeVersionEntry() { return OptionSpecification.NewSwitch( string.Empty, - "version", + new [] { "version" }, false, sentenceBuilder.VersionCommandText(AddDashesToOption), string.Empty, @@ -969,7 +981,7 @@ specification is OptionSpecification optionSpecification && var optionHelpText = specification.HelpText; if (addEnumValuesToHelpText && specification.EnumValues.Any()) - optionHelpText += " Valid values: " + string.Join(", ", specification.EnumValues); + optionHelpText += " Valid values: " + string.Join(CommaSeparation, specification.EnumValues); specification.DefaultValue.Do( defaultValue => optionHelpText = "(Default: {0}) ".FormatInvariant(FormatDefaultValue(defaultValue)) + optionHelpText); @@ -1006,13 +1018,25 @@ private string AddOptionName(int maxLength, OptionSpecification specification) .AppendWhen(addDashesToOption, '-') .AppendFormat("{0}", specification.ShortName) .AppendFormatWhen(specification.MetaValue.Length > 0, " {0}", specification.MetaValue) - .AppendWhen(specification.LongName.Length > 0, ", ")) + .AppendWhen(specification.LongNames.Length > 0, CommaSeparation)) .MapIf( - specification.LongName.Length > 0, - it => it + specification.LongNames.Length > 0, + it => + { + it .AppendWhen(addDashesToOption, "--") - .AppendFormat("{0}", specification.LongName) - .AppendFormatWhen(specification.MetaValue.Length > 0, "={0}", specification.MetaValue)) + .Append(specification.LongNames[0]) + .AppendFormatWhen(specification.MetaValue.Length > 0, "={0}", specification.MetaValue); + foreach (var longName in specification.LongNames.Skip(1)) + { + it + .Append(CommaSeparation) + .AppendWhen(addDashesToOption, "--") + .AppendFormat("{0}", longName) + .AppendFormatWhen(specification.MetaValue.Length > 0, "={0}", specification.MetaValue); + } + return it; + }) .ToString(); } @@ -1056,7 +1080,7 @@ private int GetMaxOptionLength(OptionSpecification spec) var specLength = 0; var hasShort = spec.ShortName.Length > 0; - var hasLong = spec.LongName.Length > 0; + var hasLong = spec.LongNames.Length > 0; var metaLength = 0; if (spec.MetaValue.Length > 0) @@ -1073,15 +1097,18 @@ private int GetMaxOptionLength(OptionSpecification spec) if (hasLong) { - specLength += spec.LongName.Length; + specLength += spec.LongNames.Sum(x => x.Length); if (AddDashesToOption) - specLength += OptionPrefixWidth; + specLength += OptionPrefixWidth * spec.LongNames.Length; - specLength += metaLength; + if (spec.LongNames.Length > 1) + specLength += CommaSeparation.Length * (spec.LongNames.Length - 1); + + specLength += metaLength * spec.LongNames.Length; } if (hasShort && hasLong) - specLength += OptionPrefixWidth; + specLength += CommaSeparation.Length; return specLength; } diff --git a/src/CommandLine/UnParserExtensions.cs b/src/CommandLine/UnParserExtensions.cs index e823a7fa..3dbe008e 100644 --- a/src/CommandLine/UnParserExtensions.cs +++ b/src/CommandLine/UnParserExtensions.cs @@ -266,12 +266,12 @@ private static string FormatName(this OptionSpecification optionSpec, object val { // Have a long name and short name not preferred? Go with long! // No short name? Has to be long! - var longName = (optionSpec.LongName.Length > 0 && !settings.PreferShortName) + var longName = (optionSpec.LongNames.Length > 0 && !settings.PreferShortName) || optionSpec.ShortName.Length == 0; var formattedName = new StringBuilder(longName - ? "--".JoinTo(optionSpec.LongName) + ? "--".JoinTo(optionSpec.LongNames[0]) : "-".JoinTo(optionSpec.ShortName)) .AppendWhen(optionSpec.TargetType != TargetType.Switch, longName && settings.UseEqualToken ? "=" : " ") .ToString(); diff --git a/tests/CommandLine.Tests/Fakes/Verb_Fakes.cs b/tests/CommandLine.Tests/Fakes/Verb_Fakes.cs index 9710d0de..e824745b 100644 --- a/tests/CommandLine.Tests/Fakes/Verb_Fakes.cs +++ b/tests/CommandLine.Tests/Fakes/Verb_Fakes.cs @@ -35,7 +35,7 @@ public class Add_Verb_As_Default [Verb("commit", HelpText = "Record changes to the repository.")] public class Commit_Verb { - [Option('p', "patch", + [Option('p', new[] { "patch" }, HelpText = "Use the interactive patch selection interface to chose which changes to commit.")] public bool Patch { get; set; } @@ -119,4 +119,16 @@ class Default_Verb_With_Empty_Name [Option('t', "test")] public bool TestValue { get; set; } } + [Verb("multilong")] + public class Verb_With_Option_With_Several_Long_Names + { + [Option('d', new [] { "downloadfiles", "dlf", "df" }, HelpText = "Downloads files.")] + public bool DownloadFiles { get; set; } + + [Option(new [] { "rooturl", "ru" }, HelpText = "Root URL.")] + public string RootUrl { get; set; } + + [Option(new [] { "wm", "withmeta" }, HelpText = "With Meta.", MetaValue = "NUM")] + public string WithMeta { get; set; } + } } diff --git a/tests/CommandLine.Tests/Unit/Text/HelpTextTests.cs b/tests/CommandLine.Tests/Unit/Text/HelpTextTests.cs index 9811f7be..abf80836 100644 --- a/tests/CommandLine.Tests/Unit/Text/HelpTextTests.cs +++ b/tests/CommandLine.Tests/Unit/Text/HelpTextTests.cs @@ -911,6 +911,36 @@ public void Options_Should_Render_Multiple_OptionGroups_When_Available() lines[7].Should().BeEquivalentTo("--version Display version information."); } + [Fact] + public void Invoke_AutoBuild_for_Verbs_with_multiple_long_names() + { + // Fixture setup + var fakeResult = new NotParsed( + TypeInfo.Create(typeof(NullInstance)), + new Error[] + { + new HelpVerbRequestedError("multilong", typeof(Verb_With_Option_With_Several_Long_Names), true) + }); + + // Exercize system + var helpText = HelpText.AutoBuild(fakeResult); + + // Verify outcome + var lines = helpText.ToString().ToLines().TrimStringArray(); + + lines[0].Should().Be(HeadingInfo.Default.ToString()); + lines[1].Should().Be(CopyrightInfo.Default.ToString()); + lines[2].Should().BeEmpty(); + lines[3].Should().BeEquivalentTo("-d, --downloadfiles, --dlf, --df Downloads files."); + lines[4].Should().BeEmpty(); + lines[5].Should().BeEquivalentTo("--rooturl, --ru Root URL."); + lines[6].Should().BeEmpty(); + lines[7].Should().BeEquivalentTo("--withmeta=NUM, --wm=NUM With Meta."); + lines[8].Should().BeEmpty(); + lines[9].Should().BeEquivalentTo("--help Display this help screen."); + // Teardown + } + #region Custom Help [Fact] diff --git a/tests/CommandLine.Tests/Unit/UnParserExtensionsTests.cs b/tests/CommandLine.Tests/Unit/UnParserExtensionsTests.cs index 7e878f32..69c59b8d 100644 --- a/tests/CommandLine.Tests/Unit/UnParserExtensionsTests.cs +++ b/tests/CommandLine.Tests/Unit/UnParserExtensionsTests.cs @@ -364,6 +364,57 @@ class Options_DateTimeOffset public DateTimeOffset Start { get; set; } } #endregion + + + #region Issue 885 + [Theory] + [MemberData(nameof(UnParseMultiLongArgsVerbs))] + public static void UnParsing_instance_with_multiple_long_args_returns_with_first_long_arg(Verb_With_Option_With_Several_Long_Names verb, string result) + { + new Parser() + .FormatCommandLine(verb) + .Should().BeEquivalentTo(result); + } + + [Theory] + [MemberData(nameof(UnParseMultiLongArgsVerbsPreferShort))] + public static void UnParsing_instance_with_multiple_long_args_returns_with_first_long_arg_prefer_short_name( + Verb_With_Option_With_Several_Long_Names verb, string result) + { + new Parser() + .FormatCommandLine(verb, settings => settings.PreferShortName = true) + .Should().BeEquivalentTo(result); + } + + public static IEnumerable UnParseMultiLongArgsVerbs + { + get + { + yield return new object[] { new Verb_With_Option_With_Several_Long_Names(), "multilong" }; + yield return new object[] { new Verb_With_Option_With_Several_Long_Names { DownloadFiles = true }, "multilong --downloadfiles" }; + yield return new object[] + { + new Verb_With_Option_With_Several_Long_Names { DownloadFiles = true, WithMeta = "qwert" }, + "multilong --downloadfiles --wm qwert" + }; + } + } + + public static IEnumerable UnParseMultiLongArgsVerbsPreferShort + { + get + { + yield return new object[] { new Verb_With_Option_With_Several_Long_Names(), "multilong" }; + yield return new object[] { new Verb_With_Option_With_Several_Long_Names { DownloadFiles = true }, "multilong -d" }; + yield return new object[] + { + new Verb_With_Option_With_Several_Long_Names { DownloadFiles = true, WithMeta = "qwert" }, + "multilong -d --wm qwert" + }; + } + } + + #endregion public static IEnumerable UnParseData { get