From 690136eaed700cb873d1f750076da0360e11dd1e Mon Sep 17 00:00:00 2001 From: Robin Munn Date: Tue, 18 Aug 2020 04:23:20 +0700 Subject: [PATCH] Add multi-instance option support (#678) * Add multi-instance option support Fixes #357 * Fix Appveyor build It seems Appveyor's C# compiler is old enough that it doesn't know how to do tuple deconstruction, so we'll avoid doing that. It makes TokenPartitioner uglier, but no getting around that if we're dealing with an old C# compiler. --- src/CommandLine/Core/InstanceBuilder.cs | 26 +++- src/CommandLine/Core/InstanceChooser.cs | 31 +++- src/CommandLine/Core/OptionMapper.cs | 33 ++-- src/CommandLine/Core/PartitionExtensions.cs | 25 +++ src/CommandLine/Core/Scalar.cs | 27 ---- src/CommandLine/Core/Sequence.cs | 43 ------ .../Core/SpecificationPropertyRules.cs | 17 +- src/CommandLine/Core/Switch.cs | 21 --- src/CommandLine/Core/TokenPartitioner.cs | 145 ++++++++++++++++-- src/CommandLine/Core/TypeConverter.cs | 2 +- src/CommandLine/Parser.cs | 5 +- src/CommandLine/ParserSettings.cs | 10 ++ ...s_With_Value_Sequence_And_Normal_Option.cs | 28 ++++ .../Unit/Core/InstanceBuilderTests.cs | 14 +- .../Unit/Core/InstanceChooserTests.cs | 17 +- .../Unit/Core/OptionMapperTests.cs | 62 ++++++++ .../Unit/Core/ScalarTests.cs | 6 +- .../Unit/Core/SequenceTests.cs | 76 ++++++++- .../Core/SpecificationPropertyRulesTests.cs | 58 +++++++ .../Unit/Core/SwitchTests.cs | 6 +- .../Unit/Core/TypeConverterTests.cs | 10 ++ tests/CommandLine.Tests/Unit/ParserTests.cs | 68 ++++++++ 22 files changed, 598 insertions(+), 132 deletions(-) create mode 100644 src/CommandLine/Core/PartitionExtensions.cs delete mode 100644 src/CommandLine/Core/Scalar.cs delete mode 100644 src/CommandLine/Core/Sequence.cs delete mode 100644 src/CommandLine/Core/Switch.cs create mode 100644 tests/CommandLine.Tests/Fakes/Options_With_Value_Sequence_And_Normal_Option.cs create mode 100644 tests/CommandLine.Tests/Unit/Core/SpecificationPropertyRulesTests.cs diff --git a/src/CommandLine/Core/InstanceBuilder.cs b/src/CommandLine/Core/InstanceBuilder.cs index dce377f1..ffd6250b 100644 --- a/src/CommandLine/Core/InstanceBuilder.cs +++ b/src/CommandLine/Core/InstanceBuilder.cs @@ -24,6 +24,30 @@ public static ParserResult Build( bool autoVersion, IEnumerable nonFatalErrors) { + return Build( + factory, + tokenizer, + arguments, + nameComparer, + ignoreValueCase, + parsingCulture, + autoHelp, + autoVersion, + false, + nonFatalErrors); + } + + public static ParserResult Build( + Maybe> factory, + Func, IEnumerable, Result, Error>> tokenizer, + IEnumerable arguments, + StringComparer nameComparer, + bool ignoreValueCase, + CultureInfo parsingCulture, + bool autoHelp, + bool autoVersion, + bool allowMultiInstance, + IEnumerable nonFatalErrors) { var typeInfo = factory.MapValueOrDefault(f => f().GetType(), typeof(T)); var specProps = typeInfo.GetSpecifications(pi => SpecificationProperty.Create( @@ -95,7 +119,7 @@ public static ParserResult Build( instance = BuildImmutable(typeInfo, factory, specProps, specPropsWithValue, setPropertyErrors); } - var validationErrors = specPropsWithValue.Validate(SpecificationPropertyRules.Lookup(tokens)); + var validationErrors = specPropsWithValue.Validate(SpecificationPropertyRules.Lookup(tokens, allowMultiInstance)); var allErrors = tokenizerResult.SuccessMessages() diff --git a/src/CommandLine/Core/InstanceChooser.cs b/src/CommandLine/Core/InstanceChooser.cs index 0593a2b2..72307bf2 100644 --- a/src/CommandLine/Core/InstanceChooser.cs +++ b/src/CommandLine/Core/InstanceChooser.cs @@ -22,6 +22,31 @@ public static ParserResult Choose( bool autoHelp, bool autoVersion, IEnumerable nonFatalErrors) + { + return Choose( + tokenizer, + types, + arguments, + nameComparer, + ignoreValueCase, + parsingCulture, + autoHelp, + autoVersion, + false, + nonFatalErrors); + } + + public static ParserResult Choose( + Func, IEnumerable, Result, Error>> tokenizer, + IEnumerable types, + IEnumerable arguments, + StringComparer nameComparer, + bool ignoreValueCase, + CultureInfo parsingCulture, + bool autoHelp, + bool autoVersion, + bool allowMultiInstance, + IEnumerable nonFatalErrors) { var verbs = Verb.SelectFromTypes(types); var defaultVerbs = verbs.Where(t => t.Item1.IsDefault); @@ -46,7 +71,7 @@ bool preprocCompare(string command) => arguments.Skip(1).FirstOrDefault() ?? string.Empty, nameComparer)) : (autoVersion && preprocCompare("version")) ? MakeNotParsed(types, new VersionRequestedError()) - : MatchVerb(tokenizer, verbs, defaultVerb, arguments, nameComparer, ignoreValueCase, parsingCulture, autoHelp, autoVersion, nonFatalErrors); + : MatchVerb(tokenizer, verbs, defaultVerb, arguments, nameComparer, ignoreValueCase, parsingCulture, autoHelp, autoVersion, allowMultiInstance, nonFatalErrors); } return arguments.Any() @@ -92,6 +117,7 @@ private static ParserResult MatchVerb( CultureInfo parsingCulture, bool autoHelp, bool autoVersion, + bool allowMultiInstance, IEnumerable nonFatalErrors) { string firstArg = arguments.First(); @@ -114,7 +140,8 @@ private static ParserResult MatchVerb( ignoreValueCase, parsingCulture, autoHelp, - autoVersion, + autoVersion, + allowMultiInstance, nonFatalErrors); } diff --git a/src/CommandLine/Core/OptionMapper.cs b/src/CommandLine/Core/OptionMapper.cs index 18349b40..d222c100 100644 --- a/src/CommandLine/Core/OptionMapper.cs +++ b/src/CommandLine/Core/OptionMapper.cs @@ -22,26 +22,31 @@ public static Result< .Select( pt => { - var matched = options.FirstOrDefault(s => + var matched = options.Where(s => s.Key.MatchName(((OptionSpecification)pt.Specification).ShortName, ((OptionSpecification)pt.Specification).LongName, comparer)).ToMaybe(); - return matched.IsJust() - ? ( - from sequence in matched - from converted in - converter( - sequence.Value, - pt.Property.PropertyType, - pt.Specification.TargetType != TargetType.Sequence) - select Tuple.Create( - pt.WithValue(Maybe.Just(converted)), Maybe.Nothing()) - ) + if (matched.IsJust()) + { + var matches = matched.GetValueOrDefault(Enumerable.Empty>>()); + var values = new HashSet(); + foreach (var kvp in matches) + { + foreach (var value in kvp.Value) + { + values.Add(value); + } + } + + return converter(values, pt.Property.PropertyType, pt.Specification.TargetType != TargetType.Sequence) + .Select(value => Tuple.Create(pt.WithValue(Maybe.Just(value)), Maybe.Nothing())) .GetValueOrDefault( Tuple.Create>( pt, Maybe.Just( new BadFormatConversionError( - ((OptionSpecification)pt.Specification).FromOptionSpecification())))) - : Tuple.Create(pt, Maybe.Nothing()); + ((OptionSpecification)pt.Specification).FromOptionSpecification())))); + } + + return Tuple.Create(pt, Maybe.Nothing()); } ).Memoize(); return Result.Succeed( diff --git a/src/CommandLine/Core/PartitionExtensions.cs b/src/CommandLine/Core/PartitionExtensions.cs new file mode 100644 index 00000000..47cc397e --- /dev/null +++ b/src/CommandLine/Core/PartitionExtensions.cs @@ -0,0 +1,25 @@ +// Copyright 2005-2015 Giacomo Stelluti Scala & Contributors. All rights reserved. See License.md in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Linq; +using CSharpx; + +namespace CommandLine.Core +{ + static class PartitionExtensions + { + public static Tuple,IEnumerable> PartitionByPredicate( + this IEnumerable items, + Func pred) + { + List yes = new List(); + List no = new List(); + foreach (T item in items) { + List list = pred(item) ? yes : no; + list.Add(item); + } + return Tuple.Create,IEnumerable>(yes, no); + } + } +} diff --git a/src/CommandLine/Core/Scalar.cs b/src/CommandLine/Core/Scalar.cs deleted file mode 100644 index 215ca2d2..00000000 --- a/src/CommandLine/Core/Scalar.cs +++ /dev/null @@ -1,27 +0,0 @@ -// Copyright 2005-2015 Giacomo Stelluti Scala & Contributors. All rights reserved. See License.md in the project root for license information. - -using System; -using System.Collections.Generic; -using System.Linq; -using CommandLine.Infrastructure; -using CSharpx; - -namespace CommandLine.Core -{ - static class Scalar - { - public static IEnumerable Partition( - IEnumerable tokens, - Func> typeLookup) - { - return from tseq in tokens.Pairwise( - (f, s) => - f.IsName() && s.IsValue() - ? typeLookup(f.Text).MapValueOrDefault(info => - info.TargetType == TargetType.Scalar ? new[] { f, s } : new Token[] { }, new Token[] { }) - : new Token[] { }) - from t in tseq - select t; - } - } -} diff --git a/src/CommandLine/Core/Sequence.cs b/src/CommandLine/Core/Sequence.cs deleted file mode 100644 index 3a6147b2..00000000 --- a/src/CommandLine/Core/Sequence.cs +++ /dev/null @@ -1,43 +0,0 @@ -// Copyright 2005-2015 Giacomo Stelluti Scala & Contributors. All rights reserved. See License.md in the project root for license information. - -using System; -using System.Collections.Generic; -using System.Linq; -using CommandLine.Infrastructure; -using CSharpx; - -namespace CommandLine.Core -{ - static class Sequence - { - public static IEnumerable Partition( - IEnumerable tokens, - Func> typeLookup) - { - return from tseq in tokens.Pairwise( - (f, s) => - f.IsName() && s.IsValue() - ? typeLookup(f.Text).MapValueOrDefault(info => - info.TargetType == TargetType.Sequence - ? new[] { f }.Concat(tokens.OfSequence(f, info)) - : new Token[] { }, new Token[] { }) - : new Token[] { }) - from t in tseq - select t; - } - - private static IEnumerable OfSequence(this IEnumerable tokens, Token nameToken, TypeDescriptor info) - { - var nameIndex = tokens.IndexOf(t => t.Equals(nameToken)); - if (nameIndex >= 0) - { - return info.NextValue.MapValueOrDefault( - _ => info.MaxItems.MapValueOrDefault( - n => tokens.Skip(nameIndex + 1).Take(n), - tokens.Skip(nameIndex + 1).TakeWhile(v => v.IsValue() && !v.IsValueForced())), - tokens.Skip(nameIndex + 1).TakeWhile(v => v.IsValue() && !v.IsValueForced())); - } - return new Token[] { }; - } - } -} diff --git a/src/CommandLine/Core/SpecificationPropertyRules.cs b/src/CommandLine/Core/SpecificationPropertyRules.cs index 5dc1a406..4f8b78a9 100644 --- a/src/CommandLine/Core/SpecificationPropertyRules.cs +++ b/src/CommandLine/Core/SpecificationPropertyRules.cs @@ -13,6 +13,14 @@ static class SpecificationPropertyRules public static IEnumerable, IEnumerable>> Lookup( IEnumerable tokens) + { + return Lookup(tokens, false); + } + + public static IEnumerable, IEnumerable>> + Lookup( + IEnumerable tokens, + bool allowMultiInstance) { return new List, IEnumerable>> { @@ -21,7 +29,7 @@ public static IEnumerable, IEnumerable, IEnumerable> EnforceSingle(IEnumerable tokens) + private static Func, IEnumerable> EnforceSingle(IEnumerable tokens, bool allowMultiInstance) { return specProps => { + if (allowMultiInstance) + { + return Enumerable.Empty(); + } + var specs = from sp in specProps where sp.Specification.IsOption() where sp.Value.IsJust() diff --git a/src/CommandLine/Core/Switch.cs b/src/CommandLine/Core/Switch.cs deleted file mode 100644 index 96e62443..00000000 --- a/src/CommandLine/Core/Switch.cs +++ /dev/null @@ -1,21 +0,0 @@ -// Copyright 2005-2015 Giacomo Stelluti Scala & Contributors. All rights reserved. See License.md in the project root for license information. - -using System; -using System.Collections.Generic; -using System.Linq; -using CSharpx; - -namespace CommandLine.Core -{ - static class Switch - { - public static IEnumerable Partition( - IEnumerable tokens, - Func> typeLookup) - { - return from t in tokens - where typeLookup(t.Text).MapValueOrDefault(info => t.IsName() && info.TargetType == TargetType.Switch, false) - select t; - } - } -} diff --git a/src/CommandLine/Core/TokenPartitioner.cs b/src/CommandLine/Core/TokenPartitioner.cs index be38a6d0..a735a762 100644 --- a/src/CommandLine/Core/TokenPartitioner.cs +++ b/src/CommandLine/Core/TokenPartitioner.cs @@ -18,15 +18,14 @@ Tuple>>, IEnumerable tokenComparer = ReferenceEqualityComparer.Default; var tokenList = tokens.Memoize(); - var switches = new HashSet(Switch.Partition(tokenList, typeLookup), tokenComparer); - var scalars = new HashSet(Scalar.Partition(tokenList, typeLookup), tokenComparer); - var sequences = new HashSet(Sequence.Partition(tokenList, typeLookup), tokenComparer); - var nonOptions = tokenList - .Where(t => !switches.Contains(t)) - .Where(t => !scalars.Contains(t)) - .Where(t => !sequences.Contains(t)).Memoize(); - var values = nonOptions.Where(v => v.IsValue()).Memoize(); - var errors = nonOptions.Except(values, (IEqualityComparer)ReferenceEqualityComparer.Default).Memoize(); + var partitioned = PartitionTokensByType(tokenList, typeLookup); + var switches = partitioned.Item1; + var scalars = partitioned.Item2; + var sequences = partitioned.Item3; + var nonOptions = partitioned.Item4; + var valuesAndErrors = nonOptions.PartitionByPredicate(v => v.IsValue()); + var values = valuesAndErrors.Item1; + var errors = valuesAndErrors.Item2; return Tuple.Create( KeyValuePairHelper.ForSwitch(switches) @@ -35,5 +34,131 @@ Tuple>>, IEnumerable t.Text), errors); } + + public static Tuple, IEnumerable, IEnumerable, IEnumerable> PartitionTokensByType( + IEnumerable tokens, + Func> typeLookup) + { + var switchTokens = new List(); + var scalarTokens = new List(); + var sequenceTokens = new List(); + var nonOptionTokens = new List(); + var sequences = new Dictionary>(); + var count = new Dictionary(); + var max = new Dictionary>(); + var state = SequenceState.TokenSearch; + Token nameToken = null; + foreach (var token in tokens) + { + if (token.IsValueForced()) + { + nonOptionTokens.Add(token); + } + else if (token.IsName()) + { + if (typeLookup(token.Text).MatchJust(out var info)) + { + switch (info.TargetType) + { + case TargetType.Switch: + nameToken = null; + switchTokens.Add(token); + state = SequenceState.TokenSearch; + break; + case TargetType.Scalar: + nameToken = token; + scalarTokens.Add(nameToken); + state = SequenceState.ScalarTokenFound; + break; + case TargetType.Sequence: + nameToken = token; + if (! sequences.ContainsKey(nameToken)) + { + sequences[nameToken] = new List(); + count[nameToken] = 0; + max[nameToken] = info.MaxItems; + } + state = SequenceState.SequenceTokenFound; + break; + } + } + else + { + nameToken = null; + nonOptionTokens.Add(token); + state = SequenceState.TokenSearch; + } + } + else + { + switch (state) + { + case SequenceState.TokenSearch: + case SequenceState.ScalarTokenFound when nameToken == null: + case SequenceState.SequenceTokenFound when nameToken == null: + // if (nameToken == null) Console.WriteLine($" (because there was no nameToken)"); + nameToken = null; + nonOptionTokens.Add(token); + state = SequenceState.TokenSearch; + break; + + case SequenceState.ScalarTokenFound: + nameToken = null; + scalarTokens.Add(token); + state = SequenceState.TokenSearch; + break; + + case SequenceState.SequenceTokenFound: + if (sequences.TryGetValue(nameToken, out var sequence)) { + // if (max[nameToken].MatchJust(out int m) && count[nameToken] >= m) + // { + // // This sequence is completed, so this and any further values are non-option values + // nameToken = null; + // nonOptionTokens.Add(token); + // state = SequenceState.TokenSearch; + // } + // else + { + sequence.Add(token); + count[nameToken]++; + } + } + else + { + Console.WriteLine("***BUG!!!***"); + throw new NullReferenceException($"Sequence for name {nameToken} doesn't exist, and it should"); + // sequences[nameToken] = new List(new[] { token }); + } + break; + } + } + } + + foreach (var kvp in sequences) + { + if (kvp.Value.Empty()) { + nonOptionTokens.Add(kvp.Key); + } + else + { + sequenceTokens.Add(kvp.Key); + sequenceTokens.AddRange(kvp.Value); + } + } + return Tuple.Create( + (IEnumerable)switchTokens, + (IEnumerable)scalarTokens, + (IEnumerable)sequenceTokens, + (IEnumerable)nonOptionTokens + ); + } + + private enum SequenceState + { + TokenSearch, + SequenceTokenFound, + ScalarTokenFound, + } + } -} \ No newline at end of file +} diff --git a/src/CommandLine/Core/TypeConverter.cs b/src/CommandLine/Core/TypeConverter.cs index 8f193c46..ee69373e 100644 --- a/src/CommandLine/Core/TypeConverter.cs +++ b/src/CommandLine/Core/TypeConverter.cs @@ -16,7 +16,7 @@ static class TypeConverter public static Maybe ChangeType(IEnumerable values, Type conversionType, bool scalar, CultureInfo conversionCulture, bool ignoreValueCase) { return scalar - ? ChangeTypeScalar(values.Single(), conversionType, conversionCulture, ignoreValueCase) + ? ChangeTypeScalar(values.Last(), conversionType, conversionCulture, ignoreValueCase) : ChangeTypeSequence(values, conversionType, conversionCulture, ignoreValueCase); } diff --git a/src/CommandLine/Parser.cs b/src/CommandLine/Parser.cs index f801c0f7..10c9b4e1 100644 --- a/src/CommandLine/Parser.cs +++ b/src/CommandLine/Parser.cs @@ -101,6 +101,7 @@ public ParserResult ParseArguments(IEnumerable args) settings.ParsingCulture, settings.AutoHelp, settings.AutoVersion, + settings.AllowMultiInstance, HandleUnknownArguments(settings.IgnoreUnknownArguments)), settings); } @@ -131,6 +132,7 @@ public ParserResult ParseArguments(Func factory, IEnumerable ar settings.ParsingCulture, settings.AutoHelp, settings.AutoVersion, + settings.AllowMultiInstance, HandleUnknownArguments(settings.IgnoreUnknownArguments)), settings); } @@ -163,6 +165,7 @@ public ParserResult ParseArguments(IEnumerable args, params Type settings.ParsingCulture, settings.AutoHelp, settings.AutoVersion, + settings.AllowMultiInstance, HandleUnknownArguments(settings.IgnoreUnknownArguments)), settings); } @@ -228,4 +231,4 @@ private void Dispose(bool disposing) } } } -} \ No newline at end of file +} diff --git a/src/CommandLine/ParserSettings.cs b/src/CommandLine/ParserSettings.cs index 07c10c4c..95a4cd81 100644 --- a/src/CommandLine/ParserSettings.cs +++ b/src/CommandLine/ParserSettings.cs @@ -25,6 +25,7 @@ public class ParserSettings : IDisposable private CultureInfo parsingCulture; private bool enableDashDash; private int maximumDisplayWidth; + private bool allowMultiInstance; /// /// Initializes a new instance of the class. @@ -174,6 +175,15 @@ public int MaximumDisplayWidth set { maximumDisplayWidth = value; } } + /// + /// Gets or sets a value indicating whether options are allowed to be specified multiple times. + /// + public bool AllowMultiInstance + { + get => allowMultiInstance; + set => PopsicleSetter.Set(Consumed, ref allowMultiInstance, value); + } + internal StringComparer NameComparer { get diff --git a/tests/CommandLine.Tests/Fakes/Options_With_Value_Sequence_And_Normal_Option.cs b/tests/CommandLine.Tests/Fakes/Options_With_Value_Sequence_And_Normal_Option.cs new file mode 100644 index 00000000..e8e7bf47 --- /dev/null +++ b/tests/CommandLine.Tests/Fakes/Options_With_Value_Sequence_And_Normal_Option.cs @@ -0,0 +1,28 @@ +// Copyright 2005-2015 Giacomo Stelluti Scala & Contributors. All rights reserved. See License.md in the project root for license information. +using System.Collections.Generic; + +namespace CommandLine.Tests.Fakes +{ + public class Options_With_Value_Sequence_And_Normal_Option + { + [Option('c', "compress", + HelpText = "Compress Match Pattern, Pipe Separated (|) ", + Separator = '|', + Default = new[] + { + "*.txt", "*.log", "*.ini" + })] + public IEnumerable Compress { get; set; } + + [Value(0, + HelpText = "Input Directories.", + Required = true)] + public IEnumerable InputDirs { get; set; } + + + [Option('n', "name", + HelpText = "Metadata Name.", + Default = "WILDCARD")] + public string Name { get; set; } + } +} diff --git a/tests/CommandLine.Tests/Unit/Core/InstanceBuilderTests.cs b/tests/CommandLine.Tests/Unit/Core/InstanceBuilderTests.cs index 3ef33261..be09f375 100644 --- a/tests/CommandLine.Tests/Unit/Core/InstanceBuilderTests.cs +++ b/tests/CommandLine.Tests/Unit/Core/InstanceBuilderTests.cs @@ -19,7 +19,7 @@ namespace CommandLine.Tests.Unit.Core { public class InstanceBuilderTests { - private static ParserResult InvokeBuild(string[] arguments, bool autoHelp = true, bool autoVersion = true) + private static ParserResult InvokeBuild(string[] arguments, bool autoHelp = true, bool autoVersion = true, bool multiInstance = false) where T : new() { return InstanceBuilder.Build( @@ -31,6 +31,7 @@ private static ParserResult InvokeBuild(string[] arguments, bool autoHelp CultureInfo.InvariantCulture, autoHelp, autoVersion, + multiInstance, Enumerable.Empty()); } @@ -1251,6 +1252,17 @@ public void Options_In_Group_Do_Not_Allow_Mutually_Exclusive_Set() errors.Should().BeEquivalentTo(expectedResult); } + [Fact] + public void Parse_int_sequence_with_multi_instance() + { + var expected = new[] { 1, 2, 3 }; + var result = InvokeBuild( + new[] { "--int-seq", "1", "2", "--int-seq", "3" }, + multiInstance: true); + + ((Parsed)result).Value.IntSequence.Should().BeEquivalentTo(expected); + } + #region custom types diff --git a/tests/CommandLine.Tests/Unit/Core/InstanceChooserTests.cs b/tests/CommandLine.Tests/Unit/Core/InstanceChooserTests.cs index c9dae5fb..d5cb9a21 100644 --- a/tests/CommandLine.Tests/Unit/Core/InstanceChooserTests.cs +++ b/tests/CommandLine.Tests/Unit/Core/InstanceChooserTests.cs @@ -15,7 +15,8 @@ public class InstanceChooserTests { private static ParserResult InvokeChoose( IEnumerable types, - IEnumerable arguments) + IEnumerable arguments, + bool multiInstance = false) { return InstanceChooser.Choose( (args, optionSpecs) => Tokenizer.ConfigureTokenizer(StringComparer.Ordinal, false, false)(args, optionSpecs), @@ -26,6 +27,7 @@ private static ParserResult InvokeChoose( CultureInfo.InvariantCulture, true, true, + multiInstance, Enumerable.Empty()); } @@ -168,5 +170,18 @@ public void Parse_sequence_verb_with_separator_returns_verb_instance(string[] ar expected.Should().BeEquivalentTo(((Parsed)result).Value); // Teardown } + + [Fact] + public void Parse_sequence_verb_with_multi_instance_returns_verb_instance() + { + var expected = new SequenceOptions { LongSequence = new long[] { }, StringSequence = new[] { "s1", "s2" } }; + var result = InvokeChoose( + new[] { typeof(Add_Verb), typeof(Commit_Verb), typeof(Clone_Verb), typeof(SequenceOptions) }, + new[] { "sequence", "-s", "s1", "-s", "s2" }, + true); + + Assert.IsType(((Parsed)result).Value); + expected.Should().BeEquivalentTo(((Parsed)result).Value); + } } } diff --git a/tests/CommandLine.Tests/Unit/Core/OptionMapperTests.cs b/tests/CommandLine.Tests/Unit/Core/OptionMapperTests.cs index b2219683..63bf22f3 100644 --- a/tests/CommandLine.Tests/Unit/Core/OptionMapperTests.cs +++ b/tests/CommandLine.Tests/Unit/Core/OptionMapperTests.cs @@ -49,5 +49,67 @@ public void Map_boolean_switch_creates_boolean_value() // Teardown } + + [Fact] + public void Map_with_multi_instance_scalar() + { + var tokenPartitions = new[] + { + new KeyValuePair>("s", new[] { "string1" }), + new KeyValuePair>("shortandlong", new[] { "string2" }), + new KeyValuePair>("shortandlong", new[] { "string3" }), + new KeyValuePair>("s", new[] { "string4" }), + }; + + var specProps = new[] + { + SpecificationProperty.Create( + new OptionSpecification("s", "shortandlong", false, string.Empty, Maybe.Nothing(), Maybe.Nothing(), '\0', Maybe.Nothing(), string.Empty, string.Empty, new List(), typeof(string), TargetType.Scalar, string.Empty), + typeof(Simple_Options).GetProperties().Single(p => p.Name.Equals(nameof(Simple_Options.ShortAndLong), StringComparison.Ordinal)), + Maybe.Nothing()), + }; + + var result = OptionMapper.MapValues( + specProps.Where(pt => pt.Specification.IsOption()), + tokenPartitions, + (vals, type, isScalar) => TypeConverter.ChangeType(vals, type, isScalar, CultureInfo.InvariantCulture, false), + StringComparer.Ordinal); + + var property = result.SucceededWith().Single(); + Assert.True(property.Specification.IsOption()); + Assert.True(property.Value.MatchJust(out var stringVal)); + Assert.Equal(tokenPartitions.Last().Value.Last(), stringVal); + } + + [Fact] + public void Map_with_multi_instance_sequence() + { + var tokenPartitions = new[] + { + new KeyValuePair>("i", new [] { "1", "2" }), + new KeyValuePair>("i", new [] { "3" }), + new KeyValuePair>("i", new [] { "4", "5" }), + }; + var specProps = new[] + { + SpecificationProperty.Create( + new OptionSpecification("i", string.Empty, false, string.Empty, Maybe.Nothing(), Maybe.Nothing(), '\0', Maybe.Nothing(), string.Empty, string.Empty, new List(), typeof(IEnumerable), TargetType.Sequence, string.Empty), + typeof(Simple_Options).GetProperties().Single(p => p.Name.Equals(nameof(Simple_Options.IntSequence), StringComparison.Ordinal)), + Maybe.Nothing()) + }; + + var result = OptionMapper.MapValues( + specProps.Where(pt => pt.Specification.IsOption()), + tokenPartitions, + (vals, type, isScalar) => TypeConverter.ChangeType(vals, type, isScalar, CultureInfo.InvariantCulture, false), + StringComparer.Ordinal); + + var property = result.SucceededWith().Single(); + Assert.True(property.Specification.IsOption()); + Assert.True(property.Value.MatchJust(out var sequence)); + + var expected = tokenPartitions.Aggregate(Enumerable.Empty(), (prev, part) => prev.Concat(part.Value.Select(i => int.Parse(i)))); + Assert.Equal(expected, sequence); + } } } diff --git a/tests/CommandLine.Tests/Unit/Core/ScalarTests.cs b/tests/CommandLine.Tests/Unit/Core/ScalarTests.cs index 9b1028f0..2d08bbcb 100644 --- a/tests/CommandLine.Tests/Unit/Core/ScalarTests.cs +++ b/tests/CommandLine.Tests/Unit/Core/ScalarTests.cs @@ -15,12 +15,13 @@ public void Partition_scalar_values_from_empty_token_sequence() { var expected = new Token[] { }; - var result = Scalar.Partition( + var tokens = TokenPartitioner.PartitionTokensByType( new Token[] { }, name => new[] { "str", "int" }.Contains(name) ? Maybe.Just(TypeDescriptor.Create(TargetType.Scalar, Maybe.Nothing())) : Maybe.Nothing()); + var result = tokens.Item2; // Switch, *Scalar*, Sequence, NonOption expected.Should().BeEquivalentTo(result); } @@ -30,7 +31,7 @@ public void Partition_scalar_values() { var expected = new [] { Token.Name("str"), Token.Value("strvalue") }; - var result = Scalar.Partition( + var tokens = TokenPartitioner.PartitionTokensByType( new [] { Token.Name("str"), Token.Value("strvalue"), Token.Value("freevalue"), @@ -40,6 +41,7 @@ public void Partition_scalar_values() new[] { "str", "int" }.Contains(name) ? Maybe.Just(TypeDescriptor.Create(TargetType.Scalar, Maybe.Nothing())) : Maybe.Nothing()); + var result = tokens.Item2; // Switch, *Scalar*, Sequence, NonOption expected.Should().BeEquivalentTo(result); } diff --git a/tests/CommandLine.Tests/Unit/Core/SequenceTests.cs b/tests/CommandLine.Tests/Unit/Core/SequenceTests.cs index b26575b8..cd17f592 100644 --- a/tests/CommandLine.Tests/Unit/Core/SequenceTests.cs +++ b/tests/CommandLine.Tests/Unit/Core/SequenceTests.cs @@ -15,12 +15,13 @@ public void Partition_sequence_values_from_empty_token_sequence() { var expected = new Token[] { }; - var result = Sequence.Partition( + var tokens = TokenPartitioner.PartitionTokensByType( new Token[] { }, name => new[] { "seq" }.Contains(name) ? Maybe.Just(TypeDescriptor.Create(TargetType.Sequence, Maybe.Nothing())) : Maybe.Nothing()); + var result = tokens.Item3; // Switch, Scalar, *Sequence*, NonOption expected.Should().AllBeEquivalentTo(result); } @@ -33,7 +34,7 @@ public void Partition_sequence_values() Token.Name("seq"), Token.Value("seqval0"), Token.Value("seqval1") }; - var result = Sequence.Partition( + var tokens = TokenPartitioner.PartitionTokensByType( new[] { Token.Name("str"), Token.Value("strvalue"), Token.Value("freevalue"), @@ -44,6 +45,7 @@ public void Partition_sequence_values() new[] { "seq" }.Contains(name) ? Maybe.Just(TypeDescriptor.Create(TargetType.Sequence, Maybe.Nothing())) : Maybe.Nothing()); + var result = tokens.Item3; // Switch, Scalar, *Sequence*, NonOption expected.Should().BeEquivalentTo(result); } @@ -57,7 +59,7 @@ public void Partition_sequence_values_from_two_sequneces() Token.Name("seqb"), Token.Value("seqbval0") }; - var result = Sequence.Partition( + var tokens = TokenPartitioner.PartitionTokensByType( new[] { Token.Name("str"), Token.Value("strvalue"), Token.Value("freevalue"), @@ -69,6 +71,7 @@ public void Partition_sequence_values_from_two_sequneces() new[] { "seq", "seqb" }.Contains(name) ? Maybe.Just(TypeDescriptor.Create(TargetType.Sequence, Maybe.Nothing())) : Maybe.Nothing()); + var result = tokens.Item3; // Switch, Scalar, *Sequence*, NonOption expected.Should().BeEquivalentTo(result); } @@ -81,7 +84,7 @@ public void Partition_sequence_values_only() Token.Name("seq"), Token.Value("seqval0"), Token.Value("seqval1") }; - var result = Sequence.Partition( + var tokens = TokenPartitioner.PartitionTokensByType( new[] { Token.Name("seq"), Token.Value("seqval0"), Token.Value("seqval1") @@ -90,8 +93,73 @@ public void Partition_sequence_values_only() new[] { "seq" }.Contains(name) ? Maybe.Just(TypeDescriptor.Create(TargetType.Sequence, Maybe.Nothing())) : Maybe.Nothing()); + var result = tokens.Item3; // Switch, Scalar, *Sequence*, NonOption expected.Should().BeEquivalentTo(result); } + + [Fact] + public void Partition_sequence_multi_instance() + { + var expected = new[] + { + Token.Name("seq"), + Token.Value("seqval0"), + Token.Value("seqval1"), + Token.Value("seqval2"), + Token.Value("seqval3"), + Token.Value("seqval4"), + }; + + var tokens = TokenPartitioner.PartitionTokensByType( + new[] + { + Token.Name("str"), Token.Value("strvalue"), Token.Value("freevalue"), + Token.Name("seq"), Token.Value("seqval0"), Token.Value("seqval1"), + Token.Name("x"), Token.Value("freevalue2"), + Token.Name("seq"), Token.Value("seqval2"), Token.Value("seqval3"), + Token.Name("seq"), Token.Value("seqval4") + }, + name => + new[] { "seq" }.Contains(name) + ? Maybe.Just(TypeDescriptor.Create(TargetType.Sequence, Maybe.Nothing())) + : Maybe.Nothing()); + var result = tokens.Item3; // Switch, Scalar, *Sequence*, NonOption + + var actual = result.ToArray(); + Assert.Equal(expected, actual); + } + + [Fact] + public void Partition_sequence_multi_instance_with_max() + { + var expected = new[] + { + Token.Name("seq"), + Token.Value("seqval0"), + Token.Value("seqval1"), + Token.Value("seqval2"), + Token.Value("seqval3"), + Token.Value("seqval4"), + Token.Value("seqval5"), + }; + + var tokens = TokenPartitioner.PartitionTokensByType( + new[] + { + Token.Name("str"), Token.Value("strvalue"), Token.Value("freevalue"), + Token.Name("seq"), Token.Value("seqval0"), Token.Value("seqval1"), + Token.Name("x"), Token.Value("freevalue2"), + Token.Name("seq"), Token.Value("seqval2"), Token.Value("seqval3"), + Token.Name("seq"), Token.Value("seqval4"), Token.Value("seqval5"), + }, + name => + new[] { "seq" }.Contains(name) + ? Maybe.Just(TypeDescriptor.Create(TargetType.Sequence, Maybe.Just(3))) + : Maybe.Nothing()); + var result = tokens.Item3; // Switch, Scalar, *Sequence*, NonOption + + Assert.Equal(expected, result); + } } } diff --git a/tests/CommandLine.Tests/Unit/Core/SpecificationPropertyRulesTests.cs b/tests/CommandLine.Tests/Unit/Core/SpecificationPropertyRulesTests.cs new file mode 100644 index 00000000..6e055c55 --- /dev/null +++ b/tests/CommandLine.Tests/Unit/Core/SpecificationPropertyRulesTests.cs @@ -0,0 +1,58 @@ +using CommandLine.Core; +using CommandLine.Tests.Fakes; +using CSharpx; +using System.Collections.Generic; +using Xunit; + +namespace CommandLine.Tests.Unit.Core +{ + + public class SpecificationPropertyRulesTests + { + [Fact] + public void Lookup_allows_multi_instance() + { + var tokens = new[] + { + Token.Name("name"), + Token.Value("value"), + Token.Name("name"), + Token.Value("value2"), + }; + + var specProps = new[] + { + SpecificationProperty.Create( + new OptionSpecification(string.Empty, "name", false, string.Empty, Maybe.Nothing(), Maybe.Nothing(), '\0', Maybe.Nothing(), string.Empty, string.Empty, new List(), typeof(IEnumerable), TargetType.Sequence, string.Empty), + typeof(SequenceOptions).GetProperty(nameof(SequenceOptions.StringSequence)), + Maybe.Just(new object())), + }; + + var results = specProps.Validate(SpecificationPropertyRules.Lookup(tokens, true)); + Assert.Empty(results); + } + + [Fact] + public void Lookup_fails_with_repeated_options_false_multi_instance() + { + var tokens = new[] + { + Token.Name("name"), + Token.Value("value"), + Token.Name("name"), + Token.Value("value2"), + }; + + var specProps = new[] + { + SpecificationProperty.Create( + new OptionSpecification(string.Empty, "name", false, string.Empty, Maybe.Nothing(), Maybe.Nothing(), '\0', Maybe.Nothing(), string.Empty, string.Empty, new List(), typeof(IEnumerable), TargetType.Sequence, string.Empty), + typeof(SequenceOptions).GetProperty(nameof(SequenceOptions.StringSequence)), + Maybe.Just(new object())), + }; + + var results = specProps.Validate(SpecificationPropertyRules.Lookup(tokens, false)); + Assert.Contains(results, r => r.GetType() == typeof(RepeatedOptionError)); + } + } +} diff --git a/tests/CommandLine.Tests/Unit/Core/SwitchTests.cs b/tests/CommandLine.Tests/Unit/Core/SwitchTests.cs index 82edb635..0fc6db70 100644 --- a/tests/CommandLine.Tests/Unit/Core/SwitchTests.cs +++ b/tests/CommandLine.Tests/Unit/Core/SwitchTests.cs @@ -15,12 +15,13 @@ public void Partition_switch_values_from_empty_token_sequence() { var expected = new Token[] { }; - var result = Switch.Partition( + var tokens = TokenPartitioner.PartitionTokensByType( new Token[] { }, name => new[] { "x", "switch" }.Contains(name) ? Maybe.Just(TypeDescriptor.Create(TargetType.Switch, Maybe.Nothing())) : Maybe.Nothing()); + var result = tokens.Item1; // *Switch*, Scalar, Sequence, NonOption expected.Should().BeEquivalentTo(result); } @@ -30,7 +31,7 @@ public void Partition_switch_values() { var expected = new [] { Token.Name("x") }; - var result = Switch.Partition( + var tokens = TokenPartitioner.PartitionTokensByType( new [] { Token.Name("str"), Token.Value("strvalue"), Token.Value("freevalue"), @@ -40,6 +41,7 @@ public void Partition_switch_values() new[] { "x", "switch" }.Contains(name) ? Maybe.Just(TypeDescriptor.Create(TargetType.Switch, Maybe.Nothing())) : Maybe.Nothing()); + var result = tokens.Item1; // *Switch*, Scalar, Sequence, NonOption expected.Should().BeEquivalentTo(result); } diff --git a/tests/CommandLine.Tests/Unit/Core/TypeConverterTests.cs b/tests/CommandLine.Tests/Unit/Core/TypeConverterTests.cs index c62a7836..dc16dfd0 100644 --- a/tests/CommandLine.Tests/Unit/Core/TypeConverterTests.cs +++ b/tests/CommandLine.Tests/Unit/Core/TypeConverterTests.cs @@ -120,5 +120,15 @@ public static IEnumerable ChangeType_scalars_source }; } } + + [Fact] + public void ChangeType_Scalar_LastOneWins() + { + var values = new[] { "100", "200", "300", "400", "500" }; + var result = TypeConverter.ChangeType(values, typeof(int), true, CultureInfo.InvariantCulture, true); + result.MatchJust(out var matchedValue).Should().BeTrue("should parse successfully"); + Assert.Equal(500, matchedValue); + + } } } diff --git a/tests/CommandLine.Tests/Unit/ParserTests.cs b/tests/CommandLine.Tests/Unit/ParserTests.cs index bc6d77a8..58ce7d3f 100644 --- a/tests/CommandLine.Tests/Unit/ParserTests.cs +++ b/tests/CommandLine.Tests/Unit/ParserTests.cs @@ -132,6 +132,21 @@ public void Parse_options_with_double_dash() // Teardown } + [Fact] + public void Parse_options_with_repeated_value_in_values_sequence_and_option() + { + var text = "x1 x2 x3 -c x1"; // x1 is the same in -c option and first value + var args = text.Split(); + var parser = new Parser(with => + { + with.HelpWriter = Console.Out; + }); + var result = parser.ParseArguments(args); + var options= (result as Parsed).Value; + options.Compress.Should().BeEquivalentTo(new[] { "x1" }); + options.InputDirs.Should().BeEquivalentTo(new[] { "x1","x2","x3" }); + } + [Fact] public void Parse_options_with_double_dash_and_option_sequence() { @@ -901,6 +916,59 @@ public void Parse_multiple_default_verbs() .WithParsed(args => throw new InvalidOperationException("Should not be parsed.")); } + [Fact] + public void Parse_repeated_options_in_verbs_scenario_with_multi_instance() + { + using (var sut = new Parser(settings => settings.AllowMultiInstance = true)) + { + var longVal1 = 100; + var longVal2 = 200; + var longVal3 = 300; + var stringVal = "shortSeq1"; + + var result = sut.ParseArguments( + new[] { "sequence", "--long-seq", $"{longVal1}", "-s", stringVal, "--long-seq", $"{longVal2};{longVal3}" }, + typeof(Add_Verb), typeof(Commit_Verb), typeof(SequenceOptions)); + + Assert.IsType>(result); + Assert.IsType(((Parsed)result).Value); + result.WithParsed(verb => + { + Assert.Equal(new long[] { longVal1, longVal2, longVal3 }, verb.LongSequence); + Assert.Equal(new[] { stringVal }, verb.StringSequence); + }); + } + } + + [Fact] + public void Parse_repeated_options_in_verbs_scenario_without_multi_instance() + { + using (var sut = new Parser(settings => settings.AllowMultiInstance = false)) + { + var longVal1 = 100; + var longVal2 = 200; + var longVal3 = 300; + var stringVal = "shortSeq1"; + + var result = sut.ParseArguments( + new[] { "sequence", "--long-seq", $"{longVal1}", "-s", stringVal, "--long-seq", $"{longVal2};{longVal3}" }, + typeof(Add_Verb), typeof(Commit_Verb), typeof(SequenceOptions)); + + Assert.IsType>(result); + result.WithNotParsed(errors => Assert.All(errors, e => + { + if (e is RepeatedOptionError) + { + // expected + } + else + { + throw new Exception($"{nameof(RepeatedOptionError)} expected"); + } + })); + } + } + [Fact] public void Parse_default_verb_with_empty_name() {