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 globally and per option #625

Open
wants to merge 3 commits into
base: develop
Choose a base branch
from
Open
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 allowMultiInstanceByDefault,
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, allowMultiInstanceByDefault));

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
16 changes: 12 additions & 4 deletions src/CommandLine/Core/OptionSpecification.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,11 @@ sealed class OptionSpecification : Specification
private readonly char separator;
private readonly string setName;
private readonly string group;
private readonly bool? allowMultiInstance;

public OptionSpecification(string shortName, string longName, bool required, string setName, Maybe<int> min, Maybe<int> max,
char separator, Maybe<object> defaultValue, string helpText, string metaValue, IEnumerable<string> enumValues,
Type conversionType, TargetType targetType, string group, bool hidden = false)
Type conversionType, TargetType targetType, string group, bool hidden = false, bool? allowMultiInstance = null)
: base(SpecificationType.Option,
required, min, max, defaultValue, helpText, metaValue, enumValues, conversionType, targetType, hidden)
{
Expand All @@ -26,6 +27,7 @@ public OptionSpecification(string shortName, string longName, bool required, str
this.separator = separator;
this.setName = setName;
this.group = group;
this.allowMultiInstance = allowMultiInstance;
}

public static OptionSpecification FromAttribute(OptionAttribute attribute, Type conversionType, IEnumerable<string> enumValues)
Expand All @@ -45,13 +47,14 @@ public static OptionSpecification FromAttribute(OptionAttribute attribute, Type
conversionType,
conversionType.ToTargetType(),
attribute.Group,
attribute.Hidden);
attribute.Hidden,
attribute.AllowMultiInstance);
}

public static OptionSpecification NewSwitch(string shortName, string longName, bool required, string helpText, string metaValue, bool hidden = false)
public static OptionSpecification NewSwitch(string shortName, string longName, bool required, string helpText, string metaValue, bool hidden = false, bool? allowMultiInstance = null)
{
return new OptionSpecification(shortName, longName, required, string.Empty, Maybe.Nothing<int>(), Maybe.Nothing<int>(),
'\0', Maybe.Nothing<object>(), helpText, metaValue, Enumerable.Empty<string>(), typeof(bool), TargetType.Switch, string.Empty, hidden);
'\0', Maybe.Nothing<object>(), helpText, metaValue, Enumerable.Empty<string>(), typeof(bool), TargetType.Switch, string.Empty, hidden, allowMultiInstance);
}

public string ShortName
Expand All @@ -78,5 +81,10 @@ public string Group
{
get { return group; }
}

public bool? AllowMultiInstance
{
get { return allowMultiInstance; }
}
}
}
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,
}
}
}
Loading