Skip to content

Commit ccd02e4

Browse files
committed
Merge branch 'feature/multi-instance-args' by @tydunkel and @rmunn
2 parents 4c2a115 + 78171b0 commit ccd02e4

16 files changed

+505
-72
lines changed

src/CommandLine/Core/InstanceBuilder.cs

Lines changed: 28 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,31 @@ public static ParserResult<T> Build<T>(
2323
bool autoHelp,
2424
bool autoVersion,
2525
IEnumerable<ErrorType> nonFatalErrors)
26+
{
27+
return Build(
28+
factory,
29+
tokenizer,
30+
arguments,
31+
nameComparer,
32+
ignoreValueCase,
33+
parsingCulture,
34+
autoHelp,
35+
autoVersion,
36+
false,
37+
nonFatalErrors);
38+
}
39+
40+
public static ParserResult<T> Build<T>(
41+
Maybe<Func<T>> factory,
42+
Func<IEnumerable<string>, IEnumerable<OptionSpecification>, Result<IEnumerable<Token>, Error>> tokenizer,
43+
IEnumerable<string> arguments,
44+
StringComparer nameComparer,
45+
bool ignoreValueCase,
46+
CultureInfo parsingCulture,
47+
bool autoHelp,
48+
bool autoVersion,
49+
bool allowMultiInstance,
50+
IEnumerable<ErrorType> nonFatalErrors)
2651
{
2752
var typeInfo = factory.MapValueOrDefault(f => f().GetType(), typeof(T));
2853

@@ -70,7 +95,7 @@ public static ParserResult<T> Build<T>(
7095
var valueSpecPropsResult =
7196
ValueMapper.MapValues(
7297
(from pt in specProps where pt.Specification.IsValue() orderby ((ValueSpecification)pt.Specification).Index select pt),
73-
valuesPartition,
98+
valuesPartition,
7499
(vals, type, isScalar) => TypeConverter.ChangeType(vals, type, isScalar, parsingCulture, ignoreValueCase));
75100

76101
var missingValueErrors = from token in errorsPartition
@@ -86,7 +111,7 @@ public static ParserResult<T> Build<T>(
86111

87112
//build the instance, determining if the type is mutable or not.
88113
T instance;
89-
if(typeInfo.IsMutable() == true)
114+
if (typeInfo.IsMutable() == true)
90115
{
91116
instance = BuildMutable(factory, specPropsWithValue, setPropertyErrors);
92117
}
@@ -95,7 +120,7 @@ public static ParserResult<T> Build<T>(
95120
instance = BuildImmutable(typeInfo, factory, specProps, specPropsWithValue, setPropertyErrors);
96121
}
97122

98-
var validationErrors = specPropsWithValue.Validate(SpecificationPropertyRules.Lookup(tokens));
123+
var validationErrors = specPropsWithValue.Validate(SpecificationPropertyRules.Lookup(tokens, allowMultiInstance));
99124

100125
var allErrors =
101126
tokenizerResult.SuccessMessages()

src/CommandLine/Core/InstanceChooser.cs

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,31 @@ public static ParserResult<object> Choose(
2222
bool autoHelp,
2323
bool autoVersion,
2424
IEnumerable<ErrorType> nonFatalErrors)
25+
{
26+
return Choose(
27+
tokenizer,
28+
types,
29+
arguments,
30+
nameComparer,
31+
ignoreValueCase,
32+
parsingCulture,
33+
autoHelp,
34+
autoVersion,
35+
false,
36+
nonFatalErrors);
37+
}
38+
39+
public static ParserResult<object> Choose(
40+
Func<IEnumerable<string>, IEnumerable<OptionSpecification>, Result<IEnumerable<Token>, Error>> tokenizer,
41+
IEnumerable<Type> types,
42+
IEnumerable<string> arguments,
43+
StringComparer nameComparer,
44+
bool ignoreValueCase,
45+
CultureInfo parsingCulture,
46+
bool autoHelp,
47+
bool autoVersion,
48+
bool allowMultiInstance,
49+
IEnumerable<ErrorType> nonFatalErrors)
2550
{
2651
var verbs = Verb.SelectFromTypes(types);
2752
var defaultVerbs = verbs.Where(t => t.Item1.IsDefault);
@@ -46,7 +71,7 @@ public static ParserResult<object> Choose(
4671
arguments.Skip(1).FirstOrDefault() ?? string.Empty, nameComparer))
4772
: (autoVersion && preprocCompare("version"))
4873
? MakeNotParsed(types, new VersionRequestedError())
49-
: MatchVerb(tokenizer, verbs, defaultVerb, arguments, nameComparer, ignoreValueCase, parsingCulture, autoHelp, autoVersion, nonFatalErrors);
74+
: MatchVerb(tokenizer, verbs, defaultVerb, arguments, nameComparer, ignoreValueCase, parsingCulture, autoHelp, autoVersion, allowMultiInstance, nonFatalErrors);
5075
};
5176

5277
return arguments.Any()
@@ -92,6 +117,7 @@ private static ParserResult<object> MatchVerb(
92117
CultureInfo parsingCulture,
93118
bool autoHelp,
94119
bool autoVersion,
120+
bool allowMultiInstance,
95121
IEnumerable<ErrorType> nonFatalErrors)
96122
{
97123
return verbs.Any(a => nameComparer.Equals(a.Item1.Name, arguments.First()))
@@ -106,6 +132,7 @@ private static ParserResult<object> MatchVerb(
106132
parsingCulture,
107133
autoHelp,
108134
autoVersion,
135+
allowMultiInstance,
109136
nonFatalErrors)
110137
: MatchDefaultVerb(tokenizer, verbs, defaultVerb, arguments, nameComparer, ignoreValueCase, parsingCulture, autoHelp, autoVersion, nonFatalErrors);
111138
}

src/CommandLine/Core/OptionMapper.cs

Lines changed: 20 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -22,26 +22,32 @@ public static Result<
2222
.Select(
2323
pt =>
2424
{
25-
var matched = options.FirstOrDefault(s =>
25+
var matched = options.Where(s =>
2626
s.Key.MatchName(((OptionSpecification)pt.Specification).ShortName, ((OptionSpecification)pt.Specification).LongName, comparer)).ToMaybe();
27-
return matched.IsJust()
28-
? (
29-
from sequence in matched
30-
from converted in
31-
converter(
32-
sequence.Value,
33-
pt.Property.PropertyType,
34-
pt.Specification.TargetType != TargetType.Sequence)
35-
select Tuple.Create(
36-
pt.WithValue(Maybe.Just(converted)), Maybe.Nothing<Error>())
37-
)
27+
28+
if (matched.IsJust())
29+
{
30+
var matches = matched.GetValueOrDefault(Enumerable.Empty<KeyValuePair<string, IEnumerable<string>>>());
31+
var values = new HashSet<string>();
32+
foreach (var kvp in matches)
33+
{
34+
foreach (var value in kvp.Value)
35+
{
36+
values.Add(value);
37+
}
38+
}
39+
40+
return converter(values, pt.Property.PropertyType, pt.Specification.TargetType != TargetType.Sequence)
41+
.Select(value => Tuple.Create(pt.WithValue(Maybe.Just(value)), Maybe.Nothing<Error>()))
3842
.GetValueOrDefault(
3943
Tuple.Create<SpecificationProperty, Maybe<Error>>(
4044
pt,
4145
Maybe.Just<Error>(
4246
new BadFormatConversionError(
43-
((OptionSpecification)pt.Specification).FromOptionSpecification()))))
44-
: Tuple.Create(pt, Maybe.Nothing<Error>());
47+
((OptionSpecification)pt.Specification).FromOptionSpecification()))));
48+
}
49+
50+
return Tuple.Create(pt, Maybe.Nothing<Error>());
4551
}
4652
).Memoize();
4753
return Result.Succeed(

src/CommandLine/Core/Sequence.cs

Lines changed: 132 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -14,30 +14,141 @@ public static IEnumerable<Token> Partition(
1414
IEnumerable<Token> tokens,
1515
Func<string, Maybe<TypeDescriptor>> typeLookup)
1616
{
17-
return from tseq in tokens.Pairwise(
18-
(f, s) =>
19-
f.IsName() && s.IsValue()
20-
? typeLookup(f.Text).MapValueOrDefault(info =>
21-
info.TargetType == TargetType.Sequence
22-
? new[] { f }.Concat(tokens.OfSequence(f, info))
23-
: new Token[] { }, new Token[] { })
24-
: new Token[] { })
25-
from t in tseq
26-
select t;
27-
}
17+
var sequences = new Dictionary<Token, IList<Token>>();
18+
var state = SequenceState.TokenSearch;
19+
Token nameToken = default;
20+
foreach (var token in tokens)
21+
{
22+
switch (state)
23+
{
24+
case SequenceState.TokenSearch:
25+
if (token.IsName())
26+
{
27+
if (typeLookup(token.Text).MatchJust(out var info) && info.TargetType == TargetType.Sequence)
28+
{
29+
nameToken = token;
30+
state = SequenceState.TokenFound;
31+
}
32+
}
33+
break;
2834

29-
private static IEnumerable<Token> OfSequence(this IEnumerable<Token> tokens, Token nameToken, TypeDescriptor info)
30-
{
31-
var nameIndex = tokens.IndexOf(t => t.Equals(nameToken));
32-
if (nameIndex >= 0)
35+
case SequenceState.TokenFound:
36+
if (token.IsValue())
37+
{
38+
if (sequences.TryGetValue(nameToken, out var sequence))
39+
{
40+
sequence.Add(token);
41+
}
42+
else
43+
{
44+
sequences[nameToken] = new List<Token>(new[] { token });
45+
}
46+
}
47+
else if (token.IsName())
48+
{
49+
if (typeLookup(token.Text).MatchJust(out var info) && info.TargetType == TargetType.Sequence)
50+
{
51+
nameToken = token;
52+
state = SequenceState.TokenFound;
53+
}
54+
else
55+
{
56+
state = SequenceState.TokenSearch;
57+
}
58+
}
59+
else
60+
{
61+
state = SequenceState.TokenSearch;
62+
}
63+
break;
64+
}
65+
}
66+
67+
foreach (var kvp in sequences)
3368
{
34-
return info.NextValue.MapValueOrDefault(
35-
_ => info.MaxItems.MapValueOrDefault(
36-
n => tokens.Skip(nameIndex + 1).Take(n),
37-
tokens.Skip(nameIndex + 1).TakeWhile(v => v.IsValue())),
38-
tokens.Skip(nameIndex + 1).TakeWhile(v => v.IsValue()));
69+
yield return kvp.Key;
70+
foreach (var value in kvp.Value)
71+
{
72+
yield return value;
73+
}
3974
}
40-
return new Token[] { };
75+
76+
//return from tseq in tokens.Pairwise(
77+
//(f, s) =>
78+
// f.IsName() && s.IsValue()
79+
// ? typeLookup(f.Text).MapValueOrDefault(info =>
80+
// info.TargetType == TargetType.Sequence
81+
// ? new[] { f }.Concat(tokens.OfSequence(f, info))
82+
// : new Token[] { }, new Token[] { })
83+
// : new Token[] { })
84+
// from t in tseq
85+
// select t;
86+
}
87+
88+
//private static IEnumerable<Token> OfSequence(this IEnumerable<Token> tokens, Token nameToken, TypeDescriptor info)
89+
//{
90+
// var state = SequenceState.TokenSearch;
91+
// var count = 0;
92+
// var max = info.MaxItems.GetValueOrDefault(int.MaxValue);
93+
// var values = max != int.MaxValue
94+
// ? new List<Token>(max)
95+
// : new List<Token>();
96+
97+
// foreach (var token in tokens)
98+
// {
99+
// if (count == max)
100+
// {
101+
// break;
102+
// }
103+
104+
// switch (state)
105+
// {
106+
// case SequenceState.TokenSearch:
107+
// if (token.IsName() && token.Text.Equals(nameToken.Text))
108+
// {
109+
// state = SequenceState.TokenFound;
110+
// }
111+
// break;
112+
113+
// case SequenceState.TokenFound:
114+
// if (token.IsValue())
115+
// {
116+
// state = SequenceState.ValueFound;
117+
// count++;
118+
// values.Add(token);
119+
// }
120+
// else
121+
// {
122+
// // Invalid to provide option without value
123+
// return Enumerable.Empty<Token>();
124+
// }
125+
// break;
126+
127+
// case SequenceState.ValueFound:
128+
// if (token.IsValue())
129+
// {
130+
// count++;
131+
// values.Add(token);
132+
// }
133+
// else if (token.IsName() && token.Text.Equals(nameToken.Text))
134+
// {
135+
// state = SequenceState.TokenFound;
136+
// }
137+
// else
138+
// {
139+
// state = SequenceState.TokenSearch;
140+
// }
141+
// break;
142+
// }
143+
// }
144+
145+
// return values;
146+
//}
147+
148+
private enum SequenceState
149+
{
150+
TokenSearch,
151+
TokenFound,
41152
}
42153
}
43154
}

src/CommandLine/Core/SpecificationPropertyRules.cs

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,14 @@ static class SpecificationPropertyRules
1313
public static IEnumerable<Func<IEnumerable<SpecificationProperty>, IEnumerable<Error>>>
1414
Lookup(
1515
IEnumerable<Token> tokens)
16+
{
17+
return Lookup(tokens, false);
18+
}
19+
20+
public static IEnumerable<Func<IEnumerable<SpecificationProperty>, IEnumerable<Error>>>
21+
Lookup(
22+
IEnumerable<Token> tokens,
23+
bool allowMultiInstance)
1624
{
1725
return new List<Func<IEnumerable<SpecificationProperty>, IEnumerable<Error>>>
1826
{
@@ -21,7 +29,7 @@ public static IEnumerable<Func<IEnumerable<SpecificationProperty>, IEnumerable<E
2129
EnforceMutuallyExclusiveSetAndGroupAreNotUsedTogether(),
2230
EnforceRequired(),
2331
EnforceRange(),
24-
EnforceSingle(tokens)
32+
EnforceSingle(tokens, allowMultiInstance)
2533
};
2634
}
2735

@@ -173,10 +181,15 @@ from s in options
173181
};
174182
}
175183

176-
private static Func<IEnumerable<SpecificationProperty>, IEnumerable<Error>> EnforceSingle(IEnumerable<Token> tokens)
184+
private static Func<IEnumerable<SpecificationProperty>, IEnumerable<Error>> EnforceSingle(IEnumerable<Token> tokens, bool allowMultiInstance)
177185
{
178186
return specProps =>
179187
{
188+
if (allowMultiInstance)
189+
{
190+
return Enumerable.Empty<Error>();
191+
}
192+
180193
var specs = from sp in specProps
181194
where sp.Specification.IsOption()
182195
where sp.Value.IsJust()

src/CommandLine/Core/TokenPartitioner.cs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,10 +21,11 @@ Tuple<IEnumerable<KeyValuePair<string, IEnumerable<string>>>, IEnumerable<string
2121
var switches = new HashSet<Token>(Switch.Partition(tokenList, typeLookup), tokenComparer);
2222
var scalars = new HashSet<Token>(Scalar.Partition(tokenList, typeLookup), tokenComparer);
2323
var sequences = new HashSet<Token>(Sequence.Partition(tokenList, typeLookup), tokenComparer);
24+
var dedupedSequences = new HashSet<Token>(sequences);
2425
var nonOptions = tokenList
2526
.Where(t => !switches.Contains(t))
2627
.Where(t => !scalars.Contains(t))
27-
.Where(t => !sequences.Contains(t)).Memoize();
28+
.Where(t => !dedupedSequences.Contains(t)).Memoize();
2829
var values = nonOptions.Where(v => v.IsValue()).Memoize();
2930
var errors = nonOptions.Except(values, (IEqualityComparer<Token>)ReferenceEqualityComparer.Default).Memoize();
3031

@@ -36,4 +37,4 @@ Tuple<IEnumerable<KeyValuePair<string, IEnumerable<string>>>, IEnumerable<string
3637
errors);
3738
}
3839
}
39-
}
40+
}

src/CommandLine/Core/TypeConverter.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ static class TypeConverter
1616
public static Maybe<object> ChangeType(IEnumerable<string> values, Type conversionType, bool scalar, CultureInfo conversionCulture, bool ignoreValueCase)
1717
{
1818
return scalar
19-
? ChangeTypeScalar(values.Single(), conversionType, conversionCulture, ignoreValueCase)
19+
? ChangeTypeScalar(values.Last(), conversionType, conversionCulture, ignoreValueCase)
2020
: ChangeTypeSequence(values, conversionType, conversionCulture, ignoreValueCase);
2121
}
2222

0 commit comments

Comments
 (0)