Skip to content

Commit

Permalink
Add multi-instance option support
Browse files Browse the repository at this point in the history
  • Loading branch information
tydunkel authored and rmunn committed Mar 13, 2020
1 parent e80b4dc commit 78171b0
Show file tree
Hide file tree
Showing 16 changed files with 505 additions and 72 deletions.
31 changes: 28 additions & 3 deletions src/CommandLine/Core/InstanceBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,31 @@ public static ParserResult<T> Build<T>(
bool autoHelp,
bool autoVersion,
IEnumerable<ErrorType> nonFatalErrors)
{
return Build(
factory,
tokenizer,
arguments,
nameComparer,
ignoreValueCase,
parsingCulture,
autoHelp,
autoVersion,
false,
nonFatalErrors);
}

public static ParserResult<T> Build<T>(
Maybe<Func<T>> factory,
Func<IEnumerable<string>, IEnumerable<OptionSpecification>, Result<IEnumerable<Token>, Error>> tokenizer,
IEnumerable<string> arguments,
StringComparer nameComparer,
bool ignoreValueCase,
CultureInfo parsingCulture,
bool autoHelp,
bool autoVersion,
bool allowMultiInstance,
IEnumerable<ErrorType> nonFatalErrors)
{
var typeInfo = factory.MapValueOrDefault(f => f().GetType(), typeof(T));

Expand Down Expand Up @@ -70,7 +95,7 @@ public static ParserResult<T> Build<T>(
var valueSpecPropsResult =
ValueMapper.MapValues(
(from pt in specProps where pt.Specification.IsValue() orderby ((ValueSpecification)pt.Specification).Index select pt),
valuesPartition,
valuesPartition,
(vals, type, isScalar) => TypeConverter.ChangeType(vals, type, isScalar, parsingCulture, ignoreValueCase));
var missingValueErrors = from token in errorsPartition
Expand All @@ -86,7 +111,7 @@ public static ParserResult<T> Build<T>(
//build the instance, determining if the type is mutable or not.
T instance;
if(typeInfo.IsMutable() == true)
if (typeInfo.IsMutable() == true)
{
instance = BuildMutable(factory, specPropsWithValue, setPropertyErrors);
}
Expand All @@ -95,7 +120,7 @@ public static ParserResult<T> Build<T>(
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()
Expand Down
29 changes: 28 additions & 1 deletion src/CommandLine/Core/InstanceChooser.cs
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,31 @@ public static ParserResult<object> Choose(
bool autoHelp,
bool autoVersion,
IEnumerable<ErrorType> nonFatalErrors)
{
return Choose(
tokenizer,
types,
arguments,
nameComparer,
ignoreValueCase,
parsingCulture,
autoHelp,
autoVersion,
false,
nonFatalErrors);
}

public static ParserResult<object> Choose(
Func<IEnumerable<string>, IEnumerable<OptionSpecification>, Result<IEnumerable<Token>, Error>> tokenizer,
IEnumerable<Type> types,
IEnumerable<string> arguments,
StringComparer nameComparer,
bool ignoreValueCase,
CultureInfo parsingCulture,
bool autoHelp,
bool autoVersion,
bool allowMultiInstance,
IEnumerable<ErrorType> nonFatalErrors)
{
var verbs = Verb.SelectFromTypes(types);
var defaultVerbs = verbs.Where(t => t.Item1.IsDefault);
Expand All @@ -46,7 +71,7 @@ public static ParserResult<object> Choose(
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()
Expand Down Expand Up @@ -92,6 +117,7 @@ private static ParserResult<object> MatchVerb(
CultureInfo parsingCulture,
bool autoHelp,
bool autoVersion,
bool allowMultiInstance,
IEnumerable<ErrorType> nonFatalErrors)
{
return verbs.Any(a => nameComparer.Equals(a.Item1.Name, arguments.First()))
Expand All @@ -106,6 +132,7 @@ private static ParserResult<object> MatchVerb(
parsingCulture,
autoHelp,
autoVersion,
allowMultiInstance,
nonFatalErrors)
: MatchDefaultVerb(tokenizer, verbs, defaultVerb, arguments, nameComparer, ignoreValueCase, parsingCulture, autoHelp, autoVersion, nonFatalErrors);
}
Expand Down
34 changes: 20 additions & 14 deletions src/CommandLine/Core/OptionMapper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -22,26 +22,32 @@ 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<Error>())
)
if (matched.IsJust())
{
var matches = matched.GetValueOrDefault(Enumerable.Empty<KeyValuePair<string, IEnumerable<string>>>());
var values = new HashSet<string>();
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<Error>()))
.GetValueOrDefault(
Tuple.Create<SpecificationProperty, Maybe<Error>>(
pt,
Maybe.Just<Error>(
new BadFormatConversionError(
((OptionSpecification)pt.Specification).FromOptionSpecification()))))
: Tuple.Create(pt, Maybe.Nothing<Error>());
((OptionSpecification)pt.Specification).FromOptionSpecification()))));
}
return Tuple.Create(pt, Maybe.Nothing<Error>());
}
).Memoize();
return Result.Succeed(
Expand Down
153 changes: 132 additions & 21 deletions src/CommandLine/Core/Sequence.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,30 +14,141 @@ public static IEnumerable<Token> Partition(
IEnumerable<Token> tokens,
Func<string, Maybe<TypeDescriptor>> 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;
}
var sequences = new Dictionary<Token, IList<Token>>();
var state = SequenceState.TokenSearch;
Token nameToken = default;
foreach (var token in tokens)
{
switch (state)
{
case SequenceState.TokenSearch:
if (token.IsName())
{
if (typeLookup(token.Text).MatchJust(out var info) && info.TargetType == TargetType.Sequence)
{
nameToken = token;
state = SequenceState.TokenFound;
}
}
break;

private static IEnumerable<Token> OfSequence(this IEnumerable<Token> tokens, Token nameToken, TypeDescriptor info)
{
var nameIndex = tokens.IndexOf(t => t.Equals(nameToken));
if (nameIndex >= 0)
case SequenceState.TokenFound:
if (token.IsValue())
{
if (sequences.TryGetValue(nameToken, out var sequence))
{
sequence.Add(token);
}
else
{
sequences[nameToken] = new List<Token>(new[] { token });
}
}
else if (token.IsName())
{
if (typeLookup(token.Text).MatchJust(out var info) && info.TargetType == TargetType.Sequence)
{
nameToken = token;
state = SequenceState.TokenFound;
}
else
{
state = SequenceState.TokenSearch;
}
}
else
{
state = SequenceState.TokenSearch;
}
break;
}
}

foreach (var kvp in sequences)
{
return info.NextValue.MapValueOrDefault(
_ => info.MaxItems.MapValueOrDefault(
n => tokens.Skip(nameIndex + 1).Take(n),
tokens.Skip(nameIndex + 1).TakeWhile(v => v.IsValue())),
tokens.Skip(nameIndex + 1).TakeWhile(v => v.IsValue()));
yield return kvp.Key;
foreach (var value in kvp.Value)
{
yield return value;
}
}
return new Token[] { };

//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<Token> OfSequence(this IEnumerable<Token> tokens, Token nameToken, TypeDescriptor info)
//{
// var state = SequenceState.TokenSearch;
// var count = 0;
// var max = info.MaxItems.GetValueOrDefault(int.MaxValue);
// var values = max != int.MaxValue
// ? new List<Token>(max)
// : new List<Token>();

// foreach (var token in tokens)
// {
// if (count == max)
// {
// break;
// }

// switch (state)
// {
// case SequenceState.TokenSearch:
// if (token.IsName() && token.Text.Equals(nameToken.Text))
// {
// state = SequenceState.TokenFound;
// }
// break;

// case SequenceState.TokenFound:
// if (token.IsValue())
// {
// state = SequenceState.ValueFound;
// count++;
// values.Add(token);
// }
// else
// {
// // Invalid to provide option without value
// return Enumerable.Empty<Token>();
// }
// break;

// case SequenceState.ValueFound:
// if (token.IsValue())
// {
// count++;
// values.Add(token);
// }
// else if (token.IsName() && token.Text.Equals(nameToken.Text))
// {
// state = SequenceState.TokenFound;
// }
// else
// {
// state = SequenceState.TokenSearch;
// }
// break;
// }
// }

// return values;
//}

private enum SequenceState
{
TokenSearch,
TokenFound,
}
}
}
17 changes: 15 additions & 2 deletions src/CommandLine/Core/SpecificationPropertyRules.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,14 @@ static class SpecificationPropertyRules
public static IEnumerable<Func<IEnumerable<SpecificationProperty>, IEnumerable<Error>>>
Lookup(
IEnumerable<Token> tokens)
{
return Lookup(tokens, false);
}

public static IEnumerable<Func<IEnumerable<SpecificationProperty>, IEnumerable<Error>>>
Lookup(
IEnumerable<Token> tokens,
bool allowMultiInstance)
{
return new List<Func<IEnumerable<SpecificationProperty>, IEnumerable<Error>>>
{
Expand All @@ -21,7 +29,7 @@ public static IEnumerable<Func<IEnumerable<SpecificationProperty>, IEnumerable<E
EnforceMutuallyExclusiveSetAndGroupAreNotUsedTogether(),
EnforceRequired(),
EnforceRange(),
EnforceSingle(tokens)
EnforceSingle(tokens, allowMultiInstance)
};
}

Expand Down Expand Up @@ -173,10 +181,15 @@ from s in options
};
}

private static Func<IEnumerable<SpecificationProperty>, IEnumerable<Error>> EnforceSingle(IEnumerable<Token> tokens)
private static Func<IEnumerable<SpecificationProperty>, IEnumerable<Error>> EnforceSingle(IEnumerable<Token> tokens, bool allowMultiInstance)
{
return specProps =>
{
if (allowMultiInstance)
{
return Enumerable.Empty<Error>();
}
var specs = from sp in specProps
where sp.Specification.IsOption()
where sp.Value.IsJust()
Expand Down
5 changes: 3 additions & 2 deletions src/CommandLine/Core/TokenPartitioner.cs
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,11 @@ Tuple<IEnumerable<KeyValuePair<string, IEnumerable<string>>>, IEnumerable<string
var switches = new HashSet<Token>(Switch.Partition(tokenList, typeLookup), tokenComparer);
var scalars = new HashSet<Token>(Scalar.Partition(tokenList, typeLookup), tokenComparer);
var sequences = new HashSet<Token>(Sequence.Partition(tokenList, typeLookup), tokenComparer);
var dedupedSequences = new HashSet<Token>(sequences);
var nonOptions = tokenList
.Where(t => !switches.Contains(t))
.Where(t => !scalars.Contains(t))
.Where(t => !sequences.Contains(t)).Memoize();
.Where(t => !dedupedSequences.Contains(t)).Memoize();
var values = nonOptions.Where(v => v.IsValue()).Memoize();
var errors = nonOptions.Except(values, (IEqualityComparer<Token>)ReferenceEqualityComparer.Default).Memoize();

Expand All @@ -36,4 +37,4 @@ Tuple<IEnumerable<KeyValuePair<string, IEnumerable<string>>>, IEnumerable<string
errors);
}
}
}
}
2 changes: 1 addition & 1 deletion src/CommandLine/Core/TypeConverter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ static class TypeConverter
public static Maybe<object> ChangeType(IEnumerable<string> 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);
}

Expand Down
Loading

0 comments on commit 78171b0

Please sign in to comment.