Skip to content

Commit 9496576

Browse files
committed
Add FlagCounter property to OptionSpecification
FlagCounter lets an int property count the number of times a flag appears, e.g. "-v -v -v" would produce the value 3 in an int property decorated with [Option('v', FlagCounter=true)].
1 parent 074d050 commit 9496576

13 files changed

+163
-23
lines changed

src/CommandLine/Core/InstanceBuilder.cs

+2-2
Original file line numberDiff line numberDiff line change
@@ -88,14 +88,14 @@ public static ParserResult<T> Build<T>(
8888
OptionMapper.MapValues(
8989
(from pt in specProps where pt.Specification.IsOption() select pt),
9090
optionsPartition,
91-
(vals, type, isScalar) => TypeConverter.ChangeType(vals, type, isScalar, parsingCulture, ignoreValueCase),
91+
(vals, type, isScalar, isFlag) => TypeConverter.ChangeType(vals, type, isScalar, isFlag, parsingCulture, ignoreValueCase),
9292
nameComparer);
9393

9494
var valueSpecPropsResult =
9595
ValueMapper.MapValues(
9696
(from pt in specProps where pt.Specification.IsValue() orderby ((ValueSpecification)pt.Specification).Index select pt),
9797
valuesPartition,
98-
(vals, type, isScalar) => TypeConverter.ChangeType(vals, type, isScalar, parsingCulture, ignoreValueCase));
98+
(vals, type, isScalar) => TypeConverter.ChangeType(vals, type, isScalar, false, parsingCulture, ignoreValueCase));
9999

100100
var missingValueErrors = from token in errorsPartition
101101
select

src/CommandLine/Core/NameLookup.cs

+1-1
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ public static NameLookupResult Contains(string name, IEnumerable<OptionSpecifica
2020
{
2121
var option = specifications.FirstOrDefault(a => name.MatchName(a.ShortName, a.LongName, comparer));
2222
if (option == null) return NameLookupResult.NoOptionFound;
23-
return option.ConversionType == typeof(bool)
23+
return option.ConversionType == typeof(bool) || (option.ConversionType == typeof(int) && option.FlagCounter)
2424
? NameLookupResult.BooleanOptionFound
2525
: NameLookupResult.OtherOptionFound;
2626
}

src/CommandLine/Core/OptionMapper.cs

+5-3
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ public static Result<
1515
MapValues(
1616
IEnumerable<SpecificationProperty> propertyTuples,
1717
IEnumerable<KeyValuePair<string, IEnumerable<string>>> options,
18-
Func<IEnumerable<string>, Type, bool, Maybe<object>> converter,
18+
Func<IEnumerable<string>, Type, bool, bool, Maybe<object>> converter,
1919
StringComparer comparer)
2020
{
2121
var sequencesAndErrors = propertyTuples
@@ -27,7 +27,7 @@ public static Result<
2727
if (matched.IsJust())
2828
{
2929
var matches = matched.GetValueOrDefault(Enumerable.Empty<KeyValuePair<string, IEnumerable<string>>>());
30-
var values = new HashSet<string>();
30+
var values = new List<string>();
3131
foreach (var kvp in matches)
3232
{
3333
foreach (var value in kvp.Value)
@@ -36,7 +36,9 @@ public static Result<
3636
}
3737
}
3838

39-
return converter(values, pt.Property.PropertyType, pt.Specification.TargetType != TargetType.Sequence)
39+
bool isFlag = pt.Specification.Tag == SpecificationType.Option && ((OptionSpecification)pt.Specification).FlagCounter;
40+
41+
return converter(values, isFlag ? typeof(bool) : pt.Property.PropertyType, pt.Specification.TargetType != TargetType.Sequence, isFlag)
4042
.Select(value => Tuple.Create(pt.WithValue(Maybe.Just(value)), Maybe.Nothing<Error>()))
4143
.GetValueOrDefault(
4244
Tuple.Create<SpecificationProperty, Maybe<Error>>(

src/CommandLine/Core/OptionSpecification.cs

+14-3
Original file line numberDiff line numberDiff line change
@@ -14,18 +14,20 @@ sealed class OptionSpecification : Specification
1414
private readonly char separator;
1515
private readonly string setName;
1616
private readonly string group;
17+
private readonly bool flagCounter;
1718

1819
public OptionSpecification(string shortName, string longName, bool required, string setName, Maybe<int> min, Maybe<int> max,
1920
char separator, Maybe<object> defaultValue, string helpText, string metaValue, IEnumerable<string> enumValues,
20-
Type conversionType, TargetType targetType, string group, bool hidden = false)
21+
Type conversionType, TargetType targetType, string group, bool flagCounter = false, bool hidden = false)
2122
: base(SpecificationType.Option,
22-
required, min, max, defaultValue, helpText, metaValue, enumValues, conversionType, targetType, hidden)
23+
required, min, max, defaultValue, helpText, metaValue, enumValues, conversionType, conversionType == typeof(int) && flagCounter ? TargetType.Switch : targetType, hidden)
2324
{
2425
this.shortName = shortName;
2526
this.longName = longName;
2627
this.separator = separator;
2728
this.setName = setName;
2829
this.group = group;
30+
this.flagCounter = flagCounter;
2931
}
3032

3133
public static OptionSpecification FromAttribute(OptionAttribute attribute, Type conversionType, IEnumerable<string> enumValues)
@@ -45,13 +47,14 @@ public static OptionSpecification FromAttribute(OptionAttribute attribute, Type
4547
conversionType,
4648
conversionType.ToTargetType(),
4749
attribute.Group,
50+
attribute.FlagCounter,
4851
attribute.Hidden);
4952
}
5053

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

5760
public string ShortName
@@ -78,5 +81,13 @@ public string Group
7881
{
7982
get { return group; }
8083
}
84+
85+
/// <summary>
86+
/// Whether this is an int option that counts how many times a flag was set rather than taking a value on the command line
87+
/// </summary>
88+
public bool FlagCounter
89+
{
90+
get { return flagCounter; }
91+
}
8192
}
8293
}

src/CommandLine/Core/SpecificationExtensions.cs

+1
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ public static OptionSpecification WithLongName(this OptionSpecification specific
3535
specification.ConversionType,
3636
specification.TargetType,
3737
specification.Group,
38+
specification.FlagCounter,
3839
specification.Hidden);
3940
}
4041

src/CommandLine/Core/TypeConverter.cs

+14-4
Original file line numberDiff line numberDiff line change
@@ -13,11 +13,13 @@ namespace CommandLine.Core
1313
{
1414
static class TypeConverter
1515
{
16-
public static Maybe<object> ChangeType(IEnumerable<string> values, Type conversionType, bool scalar, CultureInfo conversionCulture, bool ignoreValueCase)
16+
public static Maybe<object> ChangeType(IEnumerable<string> values, Type conversionType, bool scalar, bool isFlag, CultureInfo conversionCulture, bool ignoreValueCase)
1717
{
18-
return scalar
19-
? ChangeTypeScalar(values.Last(), conversionType, conversionCulture, ignoreValueCase)
20-
: ChangeTypeSequence(values, conversionType, conversionCulture, ignoreValueCase);
18+
return isFlag
19+
? ChangeTypeFlagCounter(values, conversionType, conversionCulture, ignoreValueCase)
20+
: scalar
21+
? ChangeTypeScalar(values.Last(), conversionType, conversionCulture, ignoreValueCase)
22+
: ChangeTypeSequence(values, conversionType, conversionCulture, ignoreValueCase);
2123
}
2224

2325
private static Maybe<object> ChangeTypeSequence(IEnumerable<string> values, Type conversionType, CultureInfo conversionCulture, bool ignoreValueCase)
@@ -46,6 +48,14 @@ private static Maybe<object> ChangeTypeScalar(string value, Type conversionType,
4648
return result.ToMaybe();
4749
}
4850

51+
private static Maybe<object> ChangeTypeFlagCounter(IEnumerable<string> values, Type conversionType, CultureInfo conversionCulture, bool ignoreValueCase)
52+
{
53+
var converted = values.Select(value => ChangeTypeScalar(value, typeof(bool), conversionCulture, ignoreValueCase));
54+
return converted.Any(maybe => maybe.MatchNothing())
55+
? Maybe.Nothing<object>()
56+
: Maybe.Just((object)converted.Count(value => value.IsJust()));
57+
}
58+
4959
private static object ConvertString(string value, Type type, CultureInfo conversionCulture)
5060
{
5161
try

src/CommandLine/OptionAttribute.cs

+11
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ public sealed class OptionAttribute : BaseAttribute
1515
private readonly string longName;
1616
private readonly string shortName;
1717
private string setName;
18+
private bool flagCounter;
1819
private char separator;
1920
private string group=string.Empty;
2021

@@ -96,6 +97,16 @@ public string SetName
9697
}
9798
}
9899

100+
/// <summary>
101+
/// If true, this is an int option that counts how many times a flag was set (e.g. "-v -v -v" or "-vvv" would return 3).
102+
/// The property must be of type int (signed 32-bit integer).
103+
/// </summary>
104+
public bool FlagCounter
105+
{
106+
get { return flagCounter; }
107+
set { flagCounter = value; }
108+
}
109+
99110
/// <summary>
100111
/// When applying attribute to <see cref="System.Collections.Generic.IEnumerable{T}"/> target properties,
101112
/// it allows you to split an argument and consume its content as a sequence.

src/CommandLine/UnParserExtensions.cs

+13-5
Original file line numberDiff line numberDiff line change
@@ -153,7 +153,9 @@ public static string FormatCommandLine<T>(this Parser parser, T options, Action<
153153

154154
var allOptSpecs = from info in specs.Where(i => i.Specification.Tag == SpecificationType.Option)
155155
let o = (OptionSpecification)info.Specification
156-
where o.TargetType != TargetType.Switch || (o.TargetType == TargetType.Switch && ((bool)info.Value))
156+
where o.TargetType != TargetType.Switch ||
157+
(o.TargetType == TargetType.Switch && o.FlagCounter && ((int)info.Value > 0)) ||
158+
(o.TargetType == TargetType.Switch && ((bool)info.Value))
157159
where !o.Hidden || settings.ShowHidden
158160
orderby o.UniqueName()
159161
select info;
@@ -176,7 +178,12 @@ orderby v.Index
176178

177179
builder = settings.GroupSwitches && shortSwitches.Any()
178180
? builder.Append('-').Append(string.Join(string.Empty, shortSwitches.Select(
179-
info => ((OptionSpecification)info.Specification).ShortName).ToArray())).Append(' ')
181+
info => {
182+
var o = (OptionSpecification)info.Specification;
183+
return o.FlagCounter
184+
? string.Concat(Enumerable.Repeat(o.ShortName, (int)info.Value))
185+
: o.ShortName;
186+
}).ToArray())).Append(' ')
180187
: builder;
181188
optSpecs.ForEach(
182189
opt =>
@@ -250,24 +257,25 @@ private static char SeperatorOrSpace(this Specification spec)
250257
private static string FormatOption(OptionSpecification spec, object value, UnParserSettings settings)
251258
{
252259
return new StringBuilder()
253-
.Append(spec.FormatName(settings))
260+
.Append(spec.FormatName(value, settings))
254261
.AppendWhen(spec.TargetType != TargetType.Switch, FormatValue(spec, value))
255262
.ToString();
256263
}
257264

258-
private static string FormatName(this OptionSpecification optionSpec, UnParserSettings settings)
265+
private static string FormatName(this OptionSpecification optionSpec, object value, UnParserSettings settings)
259266
{
260267
// Have a long name and short name not preferred? Go with long!
261268
// No short name? Has to be long!
262269
var longName = (optionSpec.LongName.Length > 0 && !settings.PreferShortName)
263270
|| optionSpec.ShortName.Length == 0;
264271

265-
return
272+
var formattedName =
266273
new StringBuilder(longName
267274
? "--".JoinTo(optionSpec.LongName)
268275
: "-".JoinTo(optionSpec.ShortName))
269276
.AppendWhen(optionSpec.TargetType != TargetType.Switch, longName && settings.UseEqualToken ? "=" : " ")
270277
.ToString();
278+
return optionSpec.FlagCounter ? String.Join(" ", Enumerable.Repeat(formattedName, (int)value)) : formattedName;
271279
}
272280

273281
private static object NormalizeValue(this object value)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
// Copyright 2005-2015 Giacomo Stelluti Scala & Contributors. All rights reserved. See License.md in the project root for license information.
2+
3+
namespace CommandLine.Tests.Fakes
4+
{
5+
public class Options_With_FlagCounter_Switches
6+
{
7+
[Option('v', FlagCounter=true)]
8+
public int Verbose { get; set; }
9+
10+
[Option('s', FlagCounter=true)]
11+
public int Silent { get; set; }
12+
}
13+
}

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

+3-3
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ public void Map_boolean_switch_creates_boolean_value()
3737
var result = OptionMapper.MapValues(
3838
specProps.Where(pt => pt.Specification.IsOption()),
3939
tokenPartitions,
40-
(vals, type, isScalar) => TypeConverter.ChangeType(vals, type, isScalar, CultureInfo.InvariantCulture, false),
40+
(vals, type, isScalar, isFlag) => TypeConverter.ChangeType(vals, type, isScalar, isFlag, CultureInfo.InvariantCulture, false),
4141
StringComparer.Ordinal
4242
);
4343

@@ -72,7 +72,7 @@ public void Map_with_multi_instance_scalar()
7272
var result = OptionMapper.MapValues(
7373
specProps.Where(pt => pt.Specification.IsOption()),
7474
tokenPartitions,
75-
(vals, type, isScalar) => TypeConverter.ChangeType(vals, type, isScalar, CultureInfo.InvariantCulture, false),
75+
(vals, type, isScalar, isFlag) => TypeConverter.ChangeType(vals, type, isScalar, isFlag, CultureInfo.InvariantCulture, false),
7676
StringComparer.Ordinal);
7777

7878
var property = result.SucceededWith().Single();
@@ -101,7 +101,7 @@ public void Map_with_multi_instance_sequence()
101101
var result = OptionMapper.MapValues(
102102
specProps.Where(pt => pt.Specification.IsOption()),
103103
tokenPartitions,
104-
(vals, type, isScalar) => TypeConverter.ChangeType(vals, type, isScalar, CultureInfo.InvariantCulture, false),
104+
(vals, type, isScalar, isFlag) => TypeConverter.ChangeType(vals, type, isScalar, isFlag, CultureInfo.InvariantCulture, false),
105105
StringComparer.Ordinal);
106106

107107
var property = result.SucceededWith().Single();

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

+34-2
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ enum TestFlagEnum
2727
[MemberData(nameof(ChangeType_scalars_source))]
2828
public void ChangeType_scalars(string testValue, Type destinationType, bool expectFail, object expectedResult)
2929
{
30-
Maybe<object> result = TypeConverter.ChangeType(new[] {testValue}, destinationType, true, CultureInfo.InvariantCulture, true);
30+
Maybe<object> result = TypeConverter.ChangeType(new[] {testValue}, destinationType, true, false, CultureInfo.InvariantCulture, true);
3131

3232
if (expectFail)
3333
{
@@ -121,11 +121,43 @@ public static IEnumerable<object[]> ChangeType_scalars_source
121121
}
122122
}
123123

124+
[Theory]
125+
[MemberData(nameof(ChangeType_flagCounters_source))]
126+
public void ChangeType_flagCounters(string[] testValue, Type destinationType, bool expectFail, object expectedResult)
127+
{
128+
Maybe<object> result = TypeConverter.ChangeType(testValue, destinationType, true, true, CultureInfo.InvariantCulture, true);
129+
130+
if (expectFail)
131+
{
132+
result.MatchNothing().Should().BeTrue("should fail parsing");
133+
}
134+
else
135+
{
136+
result.MatchJust(out object matchedValue).Should().BeTrue("should parse successfully");
137+
Assert.Equal(matchedValue, expectedResult);
138+
}
139+
}
140+
141+
public static IEnumerable<object[]> ChangeType_flagCounters_source
142+
{
143+
get
144+
{
145+
return new[]
146+
{
147+
new object[] {new string[0], typeof (int), false, 0},
148+
new object[] {new[] {"true"}, typeof (int), false, 1},
149+
new object[] {new[] {"true", "true"}, typeof (int), false, 2},
150+
new object[] {new[] {"true", "true", "true"}, typeof (int), false, 3},
151+
new object[] {new[] {"true", "x"}, typeof (int), true, 0},
152+
};
153+
}
154+
}
155+
124156
[Fact]
125157
public void ChangeType_Scalar_LastOneWins()
126158
{
127159
var values = new[] { "100", "200", "300", "400", "500" };
128-
var result = TypeConverter.ChangeType(values, typeof(int), true, CultureInfo.InvariantCulture, true);
160+
var result = TypeConverter.ChangeType(values, typeof(int), true, false, CultureInfo.InvariantCulture, true);
129161
result.MatchJust(out var matchedValue).Should().BeTrue("should parse successfully");
130162
Assert.Equal(500, matchedValue);
131163

tests/CommandLine.Tests/Unit/ParserTests.cs

+30
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,36 @@ public void Parse_options_with_short_name(string outputFile, string[] args)
9595
// Teardown
9696
}
9797

98+
[Theory]
99+
[InlineData(new string[0], 0, 0)]
100+
[InlineData(new[] { "-v" }, 1, 0)]
101+
[InlineData(new[] { "-vv" }, 2, 0)]
102+
[InlineData(new[] { "-v", "-v" }, 2, 0)]
103+
[InlineData(new[] { "-v", "-v", "-v" }, 3, 0)]
104+
[InlineData(new[] { "-v", "-vv" }, 3, 0)]
105+
[InlineData(new[] { "-vv", "-v" }, 3, 0)]
106+
[InlineData(new[] { "-vvv" }, 3, 0)]
107+
[InlineData(new[] { "-v", "-s", "-v", "-v" }, 3, 1)]
108+
[InlineData(new[] { "-v", "-ss", "-v", "-v" }, 3, 2)]
109+
[InlineData(new[] { "-v", "-s", "-sv", "-v" }, 3, 2)]
110+
[InlineData(new[] { "-vsvv" }, 3, 1)]
111+
[InlineData(new[] { "-vssvv" }, 3, 2)]
112+
[InlineData(new[] { "-vsvsv" }, 3, 2)]
113+
public void Parse_FlagCounter_options_with_short_name(string[] args, int verboseCount, int silentCount)
114+
{
115+
// Fixture setup
116+
var expectedOptions = new Options_With_FlagCounter_Switches { Verbose = verboseCount, Silent = silentCount };
117+
var sut = new Parser(with => with.AllowMultiInstance = true);
118+
119+
// Exercize system
120+
var result = sut.ParseArguments<Options_With_FlagCounter_Switches>(args);
121+
122+
// Verify outcome
123+
// ((NotParsed<Options_With_FlagCounter_Switches>)result).Errors.Should().BeEmpty();
124+
((Parsed<Options_With_FlagCounter_Switches>)result).Value.Should().BeEquivalentTo(expectedOptions);
125+
// Teardown
126+
}
127+
98128
[Fact]
99129
public void Parse_repeated_options_with_default_parser()
100130
{

0 commit comments

Comments
 (0)