Skip to content

Commit 42c5853

Browse files
authored
introduce ParseResult.GetValue<T>(string name) (#2083)
1 parent 4d1e6ac commit 42c5853

15 files changed

+522
-357
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
@@ -155,6 +155,7 @@ System.CommandLine
155155
public System.Collections.Generic.IEnumerable<System.CommandLine.Completions.CompletionItem> GetCompletions(System.Nullable<System.Int32> position = null)
156156
public T GetValue<T>(Argument<T> argument)
157157
public T GetValue<T>(Option<T> option)
158+
public T GetValue<T>(System.String name)
158159
public System.Int32 Invoke(IConsole console = null)
159160
public System.Threading.Tasks.Task<System.Int32> InvokeAsync(IConsole console = null, System.Threading.CancellationToken cancellationToken = null)
160161
public System.String ToString()

src/System.CommandLine.Hosting.Tests/HostingHandlerTest.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -206,7 +206,7 @@ public class MyOtherCommand : Command
206206
public MyOtherCommand() : base(name: "myothercommand")
207207
{
208208
Options.Add(new Option<int>("--int-option")); // or nameof(Handler.IntOption).ToKebabCase() if you don't like the string literal
209-
Arguments.Add(new Argument<string>("One"));
209+
Arguments.Add(new Argument<string>("One") { Arity = ArgumentArity.ZeroOrOne });
210210
}
211211

212212
public class MyHandler : ICommandHandler

src/System.CommandLine.Tests/Binding/TypeConversionTests.cs

Lines changed: 72 additions & 297 deletions
Large diffs are not rendered by default.
Lines changed: 290 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,290 @@
1+
// Copyright (c) .NET Foundation and contributors. All rights reserved.
2+
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
3+
4+
using FluentAssertions;
5+
using System.Collections.Generic;
6+
using Xunit;
7+
using Xunit.Abstractions;
8+
9+
namespace System.CommandLine.Tests
10+
{
11+
public class GetValueByNameParserTests : ParserTests
12+
{
13+
public GetValueByNameParserTests(ITestOutputHelper output) : base(output)
14+
{
15+
}
16+
17+
protected override T GetValue<T>(ParseResult parseResult, Option<T> option)
18+
=> parseResult.GetValue<T>(option.Name);
19+
20+
protected override T GetValue<T>(ParseResult parseResult, Argument<T> argument)
21+
=> parseResult.GetValue<T>(argument.Name);
22+
23+
[Fact]
24+
public void In_case_of_argument_name_conflict_the_value_which_belongs_to_the_last_parsed_command_is_returned()
25+
{
26+
RootCommand command = new()
27+
{
28+
new Argument<int>("arg"),
29+
new Command("inner1")
30+
{
31+
new Argument<int>("arg"),
32+
new Command("inner2")
33+
{
34+
new Argument<int>("arg"),
35+
}
36+
}
37+
};
38+
39+
ParseResult parseResult = command.Parse("1 inner1 2 inner2 3");
40+
41+
parseResult.GetValue<int>("arg").Should().Be(3);
42+
}
43+
44+
[Fact]
45+
public void In_case_of_option_name_conflict_the_value_which_belongs_to_the_last_parsed_command_is_returned()
46+
{
47+
RootCommand command = new()
48+
{
49+
new Option<int>("--integer", "-i"),
50+
new Command("inner1")
51+
{
52+
new Option<int>("--integer", "-i"),
53+
new Command("inner2")
54+
{
55+
new Option<int>("--integer", "-i")
56+
}
57+
}
58+
};
59+
60+
ParseResult parseResult = command.Parse("-i 1 inner1 --integer 2 inner2 -i 3");
61+
62+
parseResult.GetValue<int>("--integer").Should().Be(3);
63+
}
64+
65+
[Fact]
66+
public void When_option_value_is_not_parsed_then_default_value_is_returned()
67+
{
68+
RootCommand command = new()
69+
{
70+
new Option<int>("--integer", "-i")
71+
};
72+
73+
ParseResult parseResult = command.Parse("");
74+
75+
parseResult.GetValue<int>("--integer").Should().Be(default);
76+
}
77+
78+
[Fact]
79+
public void When_optional_argument_is_not_parsed_then_default_value_is_returned()
80+
{
81+
RootCommand command = new()
82+
{
83+
new Argument<int>("arg")
84+
{
85+
Arity = ArgumentArity.ZeroOrOne
86+
}
87+
};
88+
89+
ParseResult parseResult = command.Parse("");
90+
91+
parseResult.GetValue<int>("arg").Should().Be(default);
92+
}
93+
94+
[Fact]
95+
public void When_required_option_value_is_not_parsed_then_an_exception_is_thrown()
96+
{
97+
RootCommand command = new()
98+
{
99+
new Option<int>("--required")
100+
{
101+
IsRequired = true
102+
}
103+
};
104+
105+
ParseResult parseResult = command.Parse("");
106+
107+
Action getRequired = () => parseResult.GetValue<int>("--required");
108+
109+
getRequired
110+
.Should()
111+
.Throw<InvalidOperationException>()
112+
.Where(ex => ex.Message == LocalizationResources.RequiredOptionWasNotProvided("--required"));
113+
}
114+
115+
[Fact]
116+
public void When_required_argument_value_is_not_parsed_then_an_exception_is_thrown()
117+
{
118+
RootCommand command = new()
119+
{
120+
new Argument<int>("required")
121+
{
122+
Arity = ArgumentArity.ExactlyOne
123+
}
124+
};
125+
126+
ParseResult parseResult = command.Parse("");
127+
128+
Action getRequired = () => parseResult.GetValue<int>("required");
129+
130+
getRequired
131+
.Should()
132+
.Throw<InvalidOperationException>()
133+
.Where(ex => ex.Message == LocalizationResources.RequiredArgumentMissing(parseResult.FindResultFor(command.Arguments[0])));
134+
}
135+
136+
[Fact]
137+
public void When_non_existing_name_is_used_then_exception_is_thrown()
138+
{
139+
const string nonExistingName = "nonExisting";
140+
Command command = new ("noSymbols");
141+
ParseResult parseResult = command.Parse("");
142+
143+
Action getRequired = () => parseResult.GetValue<int>(nonExistingName);
144+
145+
getRequired
146+
.Should()
147+
.Throw<ArgumentException>()
148+
.Where(ex => ex.Message == $"No symbol result found for \"{nonExistingName}\" for command \"{command.Name}\".");
149+
}
150+
151+
[Fact]
152+
public void When_an_option_and_argument_use_same_name_on_the_same_level_of_the_tree_an_exception_is_thrown()
153+
{
154+
const string sameName = "same";
155+
156+
RootCommand command = new()
157+
{
158+
new Argument<int>(sameName)
159+
{
160+
Arity = ArgumentArity.ZeroOrOne
161+
},
162+
new Option<int>(sameName)
163+
};
164+
165+
ParseResult parseResult = command.Parse("");
166+
167+
Action getConflicted = () => parseResult.GetValue<int>(sameName);
168+
169+
getConflicted
170+
.Should()
171+
.Throw<NotSupportedException>()
172+
.Where(ex => ex.Message == $"More than one symbol uses name \"{sameName}\" for command \"{command.Name}\".");
173+
}
174+
175+
[Fact]
176+
public void When_an_option_and_argument_use_same_name_on_different_levels_of_the_tree_the_value_which_belongs_to_parsed_command_is_returned()
177+
{
178+
const string sameName = "same";
179+
180+
Command command = new("outer")
181+
{
182+
new Argument<int>(sameName),
183+
new Command("inner")
184+
{
185+
new Option<int>(sameName)
186+
}
187+
};
188+
189+
ParseResult parseResult = command.Parse($"outer 123 inner {sameName} 456");
190+
parseResult.GetValue<int>(sameName).Should().Be(456);
191+
192+
parseResult = command.Parse($"outer 123");
193+
parseResult.GetValue<int>(sameName).Should().Be(123);
194+
}
195+
196+
[Fact]
197+
public void When_an_option_and_argument_use_same_name_on_different_levels_of_the_tree_the_default_value_which_belongs_to_parsed_command_is_returned()
198+
{
199+
const string sameName = "same";
200+
201+
Command command = new("outer")
202+
{
203+
new Argument<int>(sameName)
204+
{
205+
DefaultValueFactory = (_) => 123
206+
},
207+
new Command("inner")
208+
{
209+
new Option<int>(sameName)
210+
{
211+
DefaultValueFactory = (_) => 456
212+
}
213+
}
214+
};
215+
216+
ParseResult parseResult = command.Parse($"outer inner 456");
217+
parseResult.GetValue<int>(sameName).Should().Be(456);
218+
219+
parseResult = command.Parse($"outer 123");
220+
parseResult.GetValue<int>(sameName).Should().Be(123);
221+
}
222+
223+
[Fact]
224+
public void T_can_be_casted_to_nullable_of_T()
225+
{
226+
RootCommand command = new()
227+
{
228+
new Argument<int>("name")
229+
};
230+
231+
ParseResult parseResult = command.Parse("123");
232+
233+
parseResult.GetValue<int?>("name").Should().Be(123);
234+
}
235+
236+
[Fact]
237+
public void Array_of_T_can_be_casted_to_ienumerable_of_T()
238+
{
239+
RootCommand command = new()
240+
{
241+
new Argument<int[]>("name")
242+
};
243+
244+
ParseResult parseResult = command.Parse("1 2 3");
245+
246+
parseResult.GetValue<IEnumerable<int>>("name").Should().BeEquivalentTo(new int[] { 1, 2, 3 });
247+
}
248+
249+
[Fact]
250+
public void When_casting_is_not_allowed_an_exception_is_thrown()
251+
{
252+
const string Name = "name";
253+
254+
RootCommand command = new()
255+
{
256+
new Argument<int>(Name)
257+
};
258+
259+
ParseResult parseResult = command.Parse("123");
260+
261+
Assert(() => parseResult.GetValue<double>(Name));
262+
Assert(() => parseResult.GetValue<int[]>(Name));
263+
Assert(() => parseResult.GetValue<string>(Name));
264+
265+
static void Assert(Action invalidCast)
266+
=> invalidCast.Should().Throw<InvalidCastException>();
267+
}
268+
269+
[Fact]
270+
public void Parse_errors_have_precedence_over_type_mismatch()
271+
{
272+
RootCommand command = new()
273+
{
274+
new Option<int>("--required")
275+
{
276+
IsRequired = true
277+
}
278+
};
279+
280+
ParseResult parseResult = command.Parse("");
281+
282+
Action getRequiredWithTypeMismatch = () => parseResult.GetValue<double>("--required");
283+
284+
getRequiredWithTypeMismatch
285+
.Should()
286+
.Throw<InvalidOperationException>()
287+
.Where(ex => ex.Message == LocalizationResources.RequiredOptionWasNotProvided("--required"));
288+
}
289+
}
290+
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
using System.CommandLine.Tests.Binding;
2+
3+
namespace System.CommandLine.Tests
4+
{
5+
public class GetValueByNameTypeConversionTests : TypeConversionTests
6+
{
7+
protected override T GetValue<T>(Argument<T> argument, string commandLine)
8+
{
9+
var result = new RootCommand { argument }.Parse(commandLine);
10+
return result.GetValue<T>(argument.Name);
11+
}
12+
13+
protected override T GetValue<T>(Option<T> option, string commandLine)
14+
{
15+
var result = new RootCommand { option }.Parse(commandLine);
16+
return result.GetValue<T>(option.Name);
17+
}
18+
}
19+
}

src/System.CommandLine.Tests/ParserTests.DoubleDash.cs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
// 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;
54
using System.CommandLine.Tests.Utility;
65
using FluentAssertions;
76
using Xunit;

src/System.CommandLine.Tests/ParserTests.MultipleArguments.cs

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -257,7 +257,7 @@ public void Unsatisfied_subsequent_argument_with_min_arity_1_parses_as_default_v
257257

258258
var result = rootCommand.Parse("");
259259

260-
result.FindResultFor(arg1).Should().BeNull();
260+
result.FindResultFor(arg1).Should().NotBeNull();
261261
result.GetValue(arg2).Should().Be("the-default");
262262
}
263263

@@ -299,12 +299,9 @@ public void When_there_are_not_enough_tokens_for_all_arguments_then_the_correct_
299299

300300
var result = Parser.Parse(command, providedArgs);
301301

302-
var numberOfMissingArgs =
303-
result
304-
.Errors
305-
.Count(e => e.Message == LocalizationResources.RequiredArgumentMissing(result.CommandResult));
306-
307-
numberOfMissingArgs
302+
result
303+
.Errors
304+
.Count
308305
.Should()
309306
.Be(4 - providedArgs.Split(new[] { ' ' }, StringSplitOptions.RemoveEmptyEntries).Length);
310307
}

0 commit comments

Comments
 (0)