Skip to content

Commit ad5aed4

Browse files
committed
Add OptionResult.IdentifierTokenCount property
This fixes #669.
1 parent 1813871 commit ad5aed4

File tree

5 files changed

+104
-93
lines changed

5 files changed

+104
-93
lines changed

src/System.CommandLine.ApiCompatibility.Tests/ApiCompatibilityApprovalTests.System_CommandLine_api_is_not_changed.approved.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -247,6 +247,7 @@ System.CommandLine.Parsing
247247
public System.Collections.Generic.IReadOnlyList<System.String> Values { get; }
248248
public class OptionResult : SymbolResult
249249
public Token IdentifierToken { get; }
250+
public System.Int32 IdentifierTokenCount { get; }
250251
public System.Boolean Implicit { get; }
251252
public System.CommandLine.CliOption Option { get; }
252253
public T GetValueOrDefault<T>()

src/System.CommandLine.Tests/ArgumentTests.cs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
// Copyright (c) .NET Foundation and contributors. All rights reserved.
1+
// Copyright (c) .NET Foundation and contributors. All rights reserved.
22
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
33

44
using System.Collections.Generic;
@@ -89,7 +89,7 @@ public void GetDefaultValue_returns_specified_value()
8989
}
9090

9191
[Fact]
92-
public void GetDefaultValue_returns_null_when_parse_delegate_returns_true_without_setting_a_value()
92+
public void GetDefaultValue_returns_null_when_custom_parser_returns_true_without_setting_a_value()
9393
{
9494
var argument = new CliArgument<string>("arg")
9595
{
@@ -405,7 +405,7 @@ public void Default_value_and_custom_argument_parser_can_be_used_together()
405405
}
406406

407407
[Fact]
408-
public void Multiple_command_arguments_can_have_custom_parse_delegates()
408+
public void Multiple_arguments_can_have_custom_parsers()
409409
{
410410
var root = new CliRootCommand
411411
{
@@ -500,7 +500,7 @@ public void When_argument_cannot_be_parsed_as_the_specified_type_then_getting_va
500500
}
501501

502502
[Fact]
503-
public void Parse_delegate_is_called_once_per_parse_operation_when_input_is_provided()
503+
public void Custom_parser_is_called_once_per_parse_operation_when_input_is_provided()
504504
{
505505
var i = 0;
506506

src/System.CommandLine.Tests/OptionTests.cs

Lines changed: 82 additions & 84 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
1-
// Copyright (c) .NET Foundation and contributors. All rights reserved.
1+
// Copyright (c) .NET Foundation and contributors. All rights reserved.
22
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
33

4+
using System.CommandLine.Parsing;
45
using FluentAssertions;
56
using System.Linq;
67
using System.Threading;
@@ -75,15 +76,15 @@ public void Option_aliases_are_case_sensitive()
7576
}
7677

7778
[Fact]
78-
public void Aliases_accepts_prefixed_short_value()
79+
public void Aliases_contains_prefixed_short_value()
7980
{
8081
var option = new CliOption<string>("--option", "-o");
8182

8283
option.Aliases.Contains("-o").Should().BeTrue();
8384
}
8485

8586
[Fact]
86-
public void HasAlias_accepts_prefixed_long_value()
87+
public void Aliases_contains_prefixed_long_value()
8788
{
8889
var option = new CliOption<string>("-o", "--option");
8990

@@ -202,30 +203,6 @@ public void When_options_use_different_prefixes_they_still_work(string prefix)
202203
result.GetValue(optionC).Should().Be("value-for-c");
203204
}
204205

205-
[Fact]
206-
public void When_option_not_explicitly_provides_help_will_use_default_help()
207-
{
208-
var option = new CliOption<string>("--option", "-o")
209-
{
210-
Description = "desc"
211-
};
212-
213-
option.Name.Should().Be("--option");
214-
option.Description.Should().Be("desc");
215-
option.Hidden.Should().BeFalse();
216-
}
217-
218-
[Fact]
219-
public void Argument_retains_name_when_it_is_provided()
220-
{
221-
var option = new CliOption<string>("-alias")
222-
{
223-
HelpName = "arg"
224-
};
225-
226-
option.HelpName.Should().Be("arg");
227-
}
228-
229206
[Fact]
230207
public void Option_T_default_value_can_be_set_via_the_constructor()
231208
{
@@ -305,19 +282,7 @@ public void Option_of_string_defaults_to_null_when_not_specified()
305282
.Should()
306283
.BeNull();
307284
}
308-
309-
[Theory]
310-
[InlineData("-option value")]
311-
[InlineData("-option:value")]
312-
public void When_aliases_overlap_the_longer_alias_is_chosen(string parseInput)
313-
{
314-
var option = new CliOption<string>("-o", "-option");
315-
316-
var parseResult = new CliRootCommand { option }.Parse(parseInput);
317-
318-
parseResult.GetValue(option).Should().Be("value");
319-
}
320-
285+
321286
[Fact]
322287
public void Option_of_boolean_defaults_to_false_when_not_specified()
323288
{
@@ -342,77 +307,110 @@ public void Option_of_enum_can_limit_enum_members_as_valid_values()
342307
var result = new CliRootCommand { option }.Parse("--color Fuschia");
343308

344309
result.Errors
345-
.Select(e => e.Message)
346-
.Should()
347-
.BeEquivalentTo(new[] { $"Argument 'Fuschia' not recognized. Must be one of:\n\t'Red'\n\t'Green'" });
310+
.Select(e => e.Message)
311+
.Should()
312+
.BeEquivalentTo(new[] { $"Argument 'Fuschia' not recognized. Must be one of:\n\t'Red'\n\t'Green'" });
348313
}
349314

350315
[Fact]
351-
public void Every_option_can_provide_a_handler_and_it_takes_precedence_over_command_handler()
316+
public void Option_result_provides_identifier_token_if_name_was_provided()
352317
{
353-
OptionAction optionAction = new();
354-
bool commandHandlerWasCalled = false;
318+
var option = new CliOption<int>("--name")
319+
{
320+
Aliases = { "-n" }
321+
};
322+
323+
var result = new CliRootCommand { option }.Parse("--name 123");
355324

356-
CliOption<bool> option = new("--test")
325+
result.FindResultFor(option).IdentifierToken.Value.Should().Be("--name");
326+
}
327+
328+
[Fact]
329+
public void Option_result_provides_identifier_token_if_alias_was_provided()
330+
{
331+
var option = new CliOption<int>("--name")
357332
{
358-
Action = optionAction,
333+
Aliases = { "-n" }
359334
};
360-
CliCommand command = new CliCommand("cmd")
335+
336+
var result = new CliRootCommand { option }.Parse("-n 123");
337+
338+
result.FindResultFor(option).IdentifierToken.Value.Should().Be("-n");
339+
}
340+
341+
[Theory]
342+
[InlineData("--name 123", 1)]
343+
[InlineData("--name 123 --name 456", 2)]
344+
[InlineData("-n 123 --name 456", 2)]
345+
[InlineData("--name 123 -x different-option --name 456", 2)]
346+
public void Number_of_occurrences_of_identifier_token_is_exposed_by_option_result(string commandLine, int expectedCount)
347+
{
348+
var option = new CliOption<int>("--name")
361349
{
362-
option
350+
Aliases = { "-n" }
363351
};
364-
command.SetAction((_) =>
352+
353+
var root = new CliRootCommand
365354
{
366-
commandHandlerWasCalled = true;
367-
});
355+
option,
356+
new CliOption<string>("-x")
357+
};
368358

369-
ParseResult parseResult = command.Parse("cmd --test true");
359+
var optionResult = root.Parse(commandLine).FindResultFor(option);
370360

371-
parseResult.Action.Should().NotBeNull();
372-
optionAction.WasCalled.Should().BeFalse();
373-
commandHandlerWasCalled.Should().BeFalse();
361+
optionResult.IdentifierTokenCount.Should().Be(expectedCount);
362+
}
374363

375-
parseResult.Invoke().Should().Be(0);
376-
optionAction.WasCalled.Should().BeTrue();
377-
commandHandlerWasCalled.Should().BeFalse();
364+
[Fact]
365+
public void Multiple_identifier_token_instances_without_argument_tokens_can_be_parsed()
366+
{
367+
var option = new CliOption<bool>("-v");
368+
369+
var root = new CliRootCommand
370+
{
371+
option
372+
};
373+
374+
var result = root.Parse("-v -v -v");
375+
376+
result.GetValue(option).Should().BeTrue();
378377
}
379378

380-
internal sealed class OptionAction : CliAction
379+
[Fact]
380+
public void Multiple_bundled_identifier_token_instances_without_argument_tokens_can_be_parsed()
381381
{
382-
internal bool WasCalled = false;
382+
var option = new CliOption<bool>("-v");
383383

384-
public override int Invoke(ParseResult context)
384+
var root = new CliRootCommand
385385
{
386-
WasCalled = true;
387-
return 0;
388-
}
386+
option
387+
};
388+
389+
var result = root.Parse("-vvv");
389390

390-
public override Task<int> InvokeAsync(ParseResult context, CancellationToken cancellationToken = default)
391-
=> Task.FromResult(Invoke(context));
391+
result.GetValue(option).Should().BeTrue();
392392
}
393393

394-
[Fact]
395-
public void When_multiple_options_with_handlers_are_parsed_only_the_last_one_is_effective()
394+
[Theory] // https://github.com/dotnet/command-line-api/issues/669
395+
[InlineData("-vvv")]
396+
[InlineData("-v -v -v")]
397+
public void Custom_parser_can_be_used_to_implement_int_binding_based_on_token_count(string commandLine)
396398
{
397-
OptionAction optionAction1 = new();
398-
OptionAction optionAction2 = new();
399-
OptionAction optionAction3 = new();
400-
401-
CliCommand command = new CliCommand("cmd")
399+
var option = new CliOption<int>("-v")
402400
{
403-
new CliOption<bool>("--1") { Action = optionAction1 },
404-
new CliOption<bool>("--2") { Action = optionAction2 },
405-
new CliOption<bool>("--3") { Action = optionAction3 }
401+
Arity = ArgumentArity.Zero,
402+
AllowMultipleArgumentsPerToken = true,
403+
CustomParser = argumentResult => ((OptionResult)argumentResult.Parent).IdentifierTokenCount,
406404
};
407405

408-
ParseResult parseResult = command.Parse("cmd --1 true --3 false --2 true ");
406+
var root = new CliRootCommand
407+
{
408+
option
409+
};
409410

410-
parseResult.Action.Should().Be(optionAction2);
411+
var result = root.Parse(commandLine);
411412

412-
parseResult.Invoke().Should().Be(0);
413-
optionAction1.WasCalled.Should().BeFalse();
414-
optionAction2.WasCalled.Should().BeTrue();
415-
optionAction3.WasCalled.Should().BeFalse();
413+
result.GetValue(option).Should().Be(3);
416414
}
417415
}
418416
}

src/System.CommandLine/Parsing/OptionResult.cs

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
// Copyright (c) .NET Foundation and contributors. All rights reserved.
1+
// Copyright (c) .NET Foundation and contributors. All rights reserved.
22
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
33

44
using System.CommandLine.Binding;
@@ -39,8 +39,14 @@ internal OptionResult(
3939
/// <summary>
4040
/// The token that was parsed to specify the option.
4141
/// </summary>
42+
/// <remarks>An identifier token is a token that matches either the option's name or one of its aliases.</remarks>
4243
public Token? IdentifierToken { get; }
4344

45+
/// <summary>
46+
/// The number of occurrences of an identifier token matching the option.
47+
/// </summary>
48+
public int IdentifierTokenCount { get; internal set; }
49+
4450
/// <inheritdoc/>
4551
public override string ToString() => $"{nameof(OptionResult)}: {IdentifierToken?.Value ?? Option.Name} {string.Join(" ", Tokens.Select(t => t.Value))}";
4652

src/System.CommandLine/Parsing/ParseOperation.cs

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
// Copyright (c) .NET Foundation and contributors. All rights reserved.
1+
// Copyright (c) .NET Foundation and contributors. All rights reserved.
22
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
33

44
using System.Collections.Generic;
@@ -217,6 +217,8 @@ private void ParseOption()
217217
optionResult = (OptionResult)symbolResult;
218218
}
219219

220+
optionResult.IdentifierTokenCount++;
221+
220222
Advance();
221223

222224
ParseOptionArguments(optionResult);
@@ -243,7 +245,8 @@ private void ParseOptionArguments(OptionResult optionResult)
243245
break;
244246
}
245247
}
246-
else if (argument.ValueType == typeof(bool) && !bool.TryParse(CurrentToken.Value, out _))
248+
else if (argument.ValueType == typeof(bool) &&
249+
!bool.TryParse(CurrentToken.Value, out _))
247250
{
248251
break;
249252
}
@@ -276,8 +279,11 @@ private void ParseOptionArguments(OptionResult optionResult)
276279

277280
if (argumentCount == 0)
278281
{
279-
ArgumentResult argumentResult = new(optionResult.Option.Argument, _symbolResultTree, optionResult);
280-
_symbolResultTree.Add(optionResult.Option.Argument, argumentResult);
282+
if (!_symbolResultTree.ContainsKey(argument))
283+
{
284+
var argumentResult = new ArgumentResult(argument, _symbolResultTree, optionResult);
285+
_symbolResultTree.Add(argument, argumentResult);
286+
}
281287
}
282288
}
283289

0 commit comments

Comments
 (0)