Skip to content

Commit 465cbd0

Browse files
committed
Introduce option groups
When one or more options has group set, at least one of these properties should have set value (they behave as required)
1 parent cf64a68 commit 465cbd0

File tree

10 files changed

+149
-55
lines changed

10 files changed

+149
-55
lines changed

src/CommandLine/Core/OptionSpecification.cs

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
// Copyright 2005-2015 Giacomo Stelluti Scala & Contributors. All rights reserved. See License.md in the project root for license information.
1+
// Copyright 2005-2015 Giacomo Stelluti Scala & Contributors. All rights reserved. See License.md in the project root for license information.
22

33
using System;
44
using System.Collections.Generic;
@@ -13,16 +13,18 @@ sealed class OptionSpecification : Specification
1313
private readonly string longName;
1414
private readonly char separator;
1515
private readonly string setName;
16+
private readonly Maybe<string> group;
1617

1718
public OptionSpecification(string shortName, string longName, bool required, string setName, Maybe<int> min, Maybe<int> max,
1819
char separator, Maybe<object> defaultValue, string helpText, string metaValue, IEnumerable<string> enumValues,
19-
Type conversionType, TargetType targetType, bool hidden = false)
20+
Type conversionType, TargetType targetType, Maybe<string> group, bool hidden = false)
2021
: base(SpecificationType.Option, required, min, max, defaultValue, helpText, metaValue, enumValues, conversionType, targetType, hidden)
2122
{
2223
this.shortName = shortName;
2324
this.longName = longName;
2425
this.separator = separator;
2526
this.setName = setName;
27+
this.group = group;
2628
}
2729

2830
public static OptionSpecification FromAttribute(OptionAttribute attribute, Type conversionType, IEnumerable<string> enumValues)
@@ -41,13 +43,14 @@ public static OptionSpecification FromAttribute(OptionAttribute attribute, Type
4143
enumValues,
4244
conversionType,
4345
conversionType.ToTargetType(),
46+
string.IsNullOrWhiteSpace(attribute.Group) ? Maybe.Nothing<string>() : Maybe.Just(attribute.Group),
4447
attribute.Hidden);
4548
}
4649

4750
public static OptionSpecification NewSwitch(string shortName, string longName, bool required, string helpText, string metaValue, bool hidden = false)
4851
{
4952
return new OptionSpecification(shortName, longName, required, string.Empty, Maybe.Nothing<int>(), Maybe.Nothing<int>(),
50-
'\0', Maybe.Nothing<object>(), helpText, metaValue, Enumerable.Empty<string>(), typeof(bool), TargetType.Switch, hidden);
53+
'\0', Maybe.Nothing<object>(), helpText, metaValue, Enumerable.Empty<string>(), typeof(bool), TargetType.Switch, Maybe.Nothing<string>(), hidden);
5154
}
5255

5356
public string ShortName
@@ -69,5 +72,10 @@ public string SetName
6972
{
7073
get { return setName; }
7174
}
75+
76+
public Maybe<string> Group
77+
{
78+
get { return group; }
79+
}
7280
}
73-
}
81+
}

src/CommandLine/Core/SpecificationExtensions.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ public static OptionSpecification WithLongName(this OptionSpecification specific
3434
specification.EnumValues,
3535
specification.ConversionType,
3636
specification.TargetType,
37+
specification.Group,
3738
specification.Hidden);
3839
}
3940

src/CommandLine/Core/SpecificationPropertyRules.cs

Lines changed: 51 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
// Copyright 2005-2015 Giacomo Stelluti Scala & Contributors. All rights reserved. See License.md in the project root for license information.
1+
// Copyright 2005-2015 Giacomo Stelluti Scala & Contributors. All rights reserved. See License.md in the project root for license information.
22

33
using System;
44
using System.Collections.Generic;
@@ -16,12 +16,43 @@ public static IEnumerable<Func<IEnumerable<SpecificationProperty>, IEnumerable<E
1616
return new List<Func<IEnumerable<SpecificationProperty>, IEnumerable<Error>>>
1717
{
1818
EnforceMutuallyExclusiveSet(),
19+
EnforceGroup(),
1920
EnforceRequired(),
2021
EnforceRange(),
2122
EnforceSingle(tokens)
2223
};
2324
}
2425

26+
private static Func<IEnumerable<SpecificationProperty>, IEnumerable<Error>> EnforceGroup()
27+
{
28+
return specProps =>
29+
{
30+
var optionsValues =
31+
from sp in specProps
32+
where sp.Specification.IsOption()
33+
let o = (OptionSpecification)sp.Specification
34+
where o.Group.IsJust()
35+
select new
36+
{
37+
Option = o,
38+
Value = sp.Value
39+
};
40+
41+
var groups = from o in optionsValues
42+
group o by o.Option.Group.GetValueOrDefault(null) into g
43+
select g;
44+
45+
var errorGroups = groups.Where(gr => gr.All(g => g.Value.IsNothing()));
46+
47+
if (errorGroups.Any())
48+
{
49+
return errorGroups.Select(gr => new MissingGroupOptionError(gr.Key));
50+
}
51+
52+
return Enumerable.Empty<Error>();
53+
};
54+
}
55+
2556
private static Func<IEnumerable<SpecificationProperty>, IEnumerable<Error>> EnforceMutuallyExclusiveSet()
2657
{
2758
return specProps =>
@@ -51,26 +82,26 @@ private static Func<IEnumerable<SpecificationProperty>, IEnumerable<Error>> Enfo
5182
return specProps =>
5283
{
5384
var requiredWithValue = from sp in specProps
54-
where sp.Specification.IsOption()
55-
where sp.Specification.Required
56-
where sp.Value.IsJust()
57-
let o = (OptionSpecification)sp.Specification
58-
where o.SetName.Length > 0
59-
select sp.Specification;
85+
where sp.Specification.IsOption()
86+
where sp.Specification.Required
87+
where sp.Value.IsJust()
88+
let o = (OptionSpecification)sp.Specification
89+
where o.SetName.Length > 0
90+
select sp.Specification;
6091
var setWithRequiredValue = (
6192
from s in requiredWithValue
6293
let o = (OptionSpecification)s
6394
where o.SetName.Length > 0
6495
select o.SetName)
6596
.Distinct();
6697
var requiredWithoutValue = from sp in specProps
67-
where sp.Specification.IsOption()
68-
where sp.Specification.Required
69-
where sp.Value.IsNothing()
70-
let o = (OptionSpecification)sp.Specification
71-
where o.SetName.Length > 0
72-
where setWithRequiredValue.ContainsIfNotEmpty(o.SetName)
73-
select sp.Specification;
98+
where sp.Specification.IsOption()
99+
where sp.Specification.Required
100+
where sp.Value.IsNothing()
101+
let o = (OptionSpecification)sp.Specification
102+
where o.SetName.Length > 0
103+
where setWithRequiredValue.ContainsIfNotEmpty(o.SetName)
104+
select sp.Specification;
74105
var missing =
75106
requiredWithoutValue
76107
.Except(requiredWithValue)
@@ -130,11 +161,11 @@ from o in to.DefaultIfEmpty()
130161
where o != null
131162
select new { o.ShortName, o.LongName };
132163
var longOptions = from t in tokens
133-
where t.IsName()
134-
join o in specs on t.Text equals o.LongName into to
135-
from o in to.DefaultIfEmpty()
136-
where o != null
137-
select new { o.ShortName, o.LongName };
164+
where t.IsName()
165+
join o in specs on t.Text equals o.LongName into to
166+
from o in to.DefaultIfEmpty()
167+
where o != null
168+
select new { o.ShortName, o.LongName };
138169
var groups = from x in shortOptions.Concat(longOptions)
139170
group x by x into g
140171
let count = g.Count()
@@ -155,4 +186,4 @@ private static bool ContainsIfNotEmpty<T>(this IEnumerable<T> sequence, T value)
155186
return true;
156187
}
157188
}
158-
}
189+
}

src/CommandLine/Error.cs

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -210,7 +210,7 @@ public override bool Equals(object obj)
210210
/// <remarks>A hash code for the current <see cref="System.Object"/>.</remarks>
211211
public override int GetHashCode()
212212
{
213-
return new {Tag, StopsProcessing, Token}.GetHashCode();
213+
return new { Tag, StopsProcessing, Token }.GetHashCode();
214214
}
215215

216216
/// <summary>
@@ -289,7 +289,7 @@ public override bool Equals(object obj)
289289
/// <remarks>A hash code for the current <see cref="System.Object"/>.</remarks>
290290
public override int GetHashCode()
291291
{
292-
return new {Tag, StopsProcessing, NameInfo}.GetHashCode();
292+
return new { Tag, StopsProcessing, NameInfo }.GetHashCode();
293293
}
294294

295295
/// <summary>
@@ -526,4 +526,22 @@ internal InvalidAttributeConfigurationError()
526526
{
527527
}
528528
}
529+
530+
public sealed class MissingGroupOptionError : Error
531+
{
532+
public const string ErrorMessage = "At least one option in a group must have value.";
533+
534+
private readonly string group;
535+
536+
internal MissingGroupOptionError(string group)
537+
: base(ErrorType.HelpRequestedError, true)
538+
{
539+
this.group = group;
540+
}
541+
542+
public string Group
543+
{
544+
get { return group; }
545+
}
546+
}
529547
}

src/CommandLine/GroupAttribute.cs

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
using System;
2+
3+
namespace CommandLine
4+
{
5+
[AttributeUsage(AttributeTargets.Property, AllowMultiple = false, Inherited = true)]
6+
public class GroupAttribute : Attribute
7+
{
8+
public GroupAttribute(string name)
9+
{
10+
Name = name;
11+
}
12+
13+
public string Name { get; }
14+
}
15+
}

src/CommandLine/OptionAttribute.cs

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
// Copyright 2005-2015 Giacomo Stelluti Scala & Contributors. All rights reserved. See License.md in the project root for license information.
22

3-
using System;
43
using CommandLine.Infrastructure;
54

5+
using System;
6+
67
namespace CommandLine
78
{
89
/// <summary>
@@ -15,6 +16,7 @@ public sealed class OptionAttribute : BaseAttribute
1516
private readonly string shortName;
1617
private string setName;
1718
private char separator;
19+
private string group;
1820

1921
private OptionAttribute(string shortName, string longName) : base()
2022
{
@@ -100,8 +102,14 @@ public string SetName
100102
/// </summary>
101103
public char Separator
102104
{
103-
get { return separator ; }
105+
get { return separator; }
104106
set { separator = value; }
105107
}
108+
109+
public string Group
110+
{
111+
get { return group; }
112+
set { group = value; }
113+
}
106114
}
107-
}
115+
}

tests/CommandLine.Tests/Unit/Core/NameLookupTests.cs

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,15 @@
11
// Copyright 2005-2015 Giacomo Stelluti Scala & Contributors. All rights reserved. See License.md in the project root for license information.
22

3-
using System;
4-
using System.Collections.Generic;
53
using CommandLine.Core;
4+
5+
using CSharpx;
6+
67
using FluentAssertions;
8+
9+
using System;
10+
using System.Collections.Generic;
11+
712
using Xunit;
8-
using CSharpx;
913

1014
namespace CommandLine.Tests.Unit.Core
1115
{
@@ -17,7 +21,7 @@ public void Lookup_name_of_sequence_option_with_separator()
1721
// Fixture setup
1822
var expected = Maybe.Just(".");
1923
var specs = new[] { new OptionSpecification(string.Empty, "string-seq",
20-
false, string.Empty, Maybe.Nothing<int>(), Maybe.Nothing<int>(), '.', null, string.Empty, string.Empty, new List<string>(), typeof(IEnumerable<string>), TargetType.Sequence)};
24+
false, string.Empty, Maybe.Nothing<int>(), Maybe.Nothing<int>(), '.', null, string.Empty, string.Empty, new List<string>(), typeof(IEnumerable<string>), TargetType.Sequence, Maybe.Nothing<string>())};
2125

2226
// Exercize system
2327
var result = NameLookup.HavingSeparator("string-seq", specs, StringComparer.Ordinal);
@@ -35,7 +39,7 @@ public void Get_name_from_option_specification()
3539

3640
// Fixture setup
3741
var expected = new NameInfo(ShortName, LongName);
38-
var spec = new OptionSpecification(ShortName, LongName, false, string.Empty, Maybe.Nothing<int>(), Maybe.Nothing<int>(), '.', null, string.Empty, string.Empty, new List<string>(), typeof(IEnumerable<string>), TargetType.Sequence);
42+
var spec = new OptionSpecification(ShortName, LongName, false, string.Empty, Maybe.Nothing<int>(), Maybe.Nothing<int>(), '.', null, string.Empty, string.Empty, new List<string>(), typeof(IEnumerable<string>), TargetType.Sequence, Maybe.Nothing<string>());
3943

4044
// Exercize system
4145
var result = spec.FromOptionSpecification();

tests/CommandLine.Tests/Unit/Core/OptionMapperTests.cs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,17 @@
44
using System.Collections.Generic;
55
using System.Globalization;
66
using System.Linq;
7+
78
#if PLATFORM_DOTNET
89
using System.Reflection;
910
#endif
1011
using CommandLine.Core;
1112
using CommandLine.Tests.Fakes;
13+
1214
using Xunit;
15+
1316
using CSharpx;
17+
1418
using RailwaySharp.ErrorHandling;
1519

1620
namespace CommandLine.Tests.Unit.Core
@@ -28,7 +32,7 @@ public void Map_boolean_switch_creates_boolean_value()
2832
var specProps = new[]
2933
{
3034
SpecificationProperty.Create(
31-
new OptionSpecification("x", string.Empty, false, string.Empty, Maybe.Nothing<int>(), Maybe.Nothing<int>(), '\0', Maybe.Nothing<object>(), string.Empty, string.Empty, new List<string>(), typeof(bool), TargetType.Switch),
35+
new OptionSpecification("x", string.Empty, false, string.Empty, Maybe.Nothing<int>(), Maybe.Nothing<int>(), '\0', Maybe.Nothing<object>(), string.Empty, string.Empty, new List<string>(), typeof(bool), TargetType.Switch, Maybe.Nothing<string>()),
3236
typeof(Simple_Options).GetProperties().Single(p => p.Name.Equals("BoolValue", StringComparison.Ordinal)),
3337
Maybe.Nothing<object>())
3438
};

tests/CommandLine.Tests/Unit/Core/TokenPartitionerTests.cs

Lines changed: 13 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,13 @@
11
// Copyright 2005-2015 Giacomo Stelluti Scala & Contributors. All rights reserved. See License.md in the project root for license information.
22

3+
using CommandLine.Core;
4+
5+
using CSharpx;
6+
37
using System;
48
using System.Collections.Generic;
59
using System.Linq;
6-
using CommandLine.Core;
7-
using CSharpx;
10+
811
using Xunit;
912

1013
namespace CommandLine.Tests.Unit.Core
@@ -17,12 +20,12 @@ public void Partition_sequence_returns_sequence()
1720
// Fixture setup
1821
var expectedSequence = new[]
1922
{
20-
new KeyValuePair<string, IEnumerable<string>>("i", new[] {"10", "20", "30", "40"})
23+
new KeyValuePair<string, IEnumerable<string>>("i", new[] {"10", "20", "30", "40"})
2124
};
22-
var specs =new[]
25+
var specs = new[]
2326
{
24-
new OptionSpecification(string.Empty, "stringvalue", false, string.Empty, Maybe.Nothing<int>(), Maybe.Nothing<int>(), '\0', null, string.Empty, string.Empty, new List<string>(), typeof(string), TargetType.Scalar),
25-
new OptionSpecification("i", string.Empty, false, string.Empty, Maybe.Just(3), Maybe.Just(4), '\0', null, string.Empty, string.Empty, new List<string>(), typeof(IEnumerable<int>), TargetType.Sequence)
27+
new OptionSpecification(string.Empty, "stringvalue", false, string.Empty, Maybe.Nothing<int>(), Maybe.Nothing<int>(), '\0', null, string.Empty, string.Empty, new List<string>(), typeof(string), TargetType.Scalar, Maybe.Nothing<string>()),
28+
new OptionSpecification("i", string.Empty, false, string.Empty, Maybe.Just(3), Maybe.Just(4), '\0', null, string.Empty, string.Empty, new List<string>(), typeof(IEnumerable<int>), TargetType.Sequence, Maybe.Nothing<string>())
2629
};
2730

2831
// Exercize system
@@ -44,12 +47,12 @@ public void Partition_sequence_returns_sequence_with_duplicates()
4447
// Fixture setup
4548
var expectedSequence = new[]
4649
{
47-
new KeyValuePair<string, IEnumerable<string>>("i", new[] {"10", "10", "30", "40"})
50+
new KeyValuePair<string, IEnumerable<string>>("i", new[] {"10", "10", "30", "40"})
4851
};
49-
var specs =new[]
52+
var specs = new[]
5053
{
51-
new OptionSpecification(string.Empty, "stringvalue", false, string.Empty, Maybe.Nothing<int>(), Maybe.Nothing<int>(), '\0', null, string.Empty, string.Empty, new List<string>(), typeof(string), TargetType.Scalar),
52-
new OptionSpecification("i", string.Empty, false, string.Empty, Maybe.Just(3), Maybe.Just(4), '\0', null, string.Empty, string.Empty, new List<string>(), typeof(IEnumerable<int>), TargetType.Sequence)
54+
new OptionSpecification(string.Empty, "stringvalue", false, string.Empty, Maybe.Nothing<int>(), Maybe.Nothing<int>(), '\0', null, string.Empty, string.Empty, new List<string>(), typeof(string), TargetType.Scalar, Maybe.Nothing<string>()),
55+
new OptionSpecification("i", string.Empty, false, string.Empty, Maybe.Just(3), Maybe.Just(4), '\0', null, string.Empty, string.Empty, new List<string>(), typeof(IEnumerable<int>), TargetType.Sequence, Maybe.Nothing<string>())
5356
};
5457

5558
// Exercize system

0 commit comments

Comments
 (0)