Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add multi-instance option support #594

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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