Skip to content

Commit 9fb290d

Browse files
committed
improved support for custom argument parsing
1 parent eaec1c5 commit 9fb290d

File tree

5 files changed

+266
-1
lines changed

5 files changed

+266
-1
lines changed

src/System.CommandLine.Tests/ArgumentTests.cs

Lines changed: 158 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,11 @@
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.Collections.Generic;
5+
using System.CommandLine.Parsing;
6+
using System.IO;
47
using FluentAssertions;
8+
using System.Linq;
59
using Xunit;
610

711
namespace System.CommandLine.Tests
@@ -50,6 +54,160 @@ public void When_there_is_no_default_value_then_GetDefaultValue_throws()
5054
.Be("Argument \"the-arg\" does not have a default value");
5155
}
5256

57+
public class CustomParsing
58+
{
59+
[Fact]
60+
public void HasDefaultValue_can_be_set_to_true()
61+
{
62+
var argument = new Argument<FileSystemInfo>(result => true, true);
63+
64+
argument.HasDefaultValue
65+
.Should()
66+
.BeTrue();
67+
}
68+
69+
[Fact]
70+
public void HasDefaultValue_can_be_set_to_false()
71+
{
72+
var argument = new Argument<FileSystemInfo>(result => true, false);
73+
74+
argument.HasDefaultValue
75+
.Should()
76+
.BeFalse();
77+
}
78+
79+
[Fact]
80+
public void GetDefaultValue_returns_specified_value()
81+
{
82+
var argument = new Argument<string>(result =>
83+
{
84+
result.Value = "the-default";
85+
return true;
86+
}, isDefault: true);
87+
88+
argument.GetDefaultValue()
89+
.Should()
90+
.Be("the-default");
91+
}
92+
93+
[Fact]
94+
public void GetDefaultValue_returns_null_when_parse_delegate_returns_true_without_setting_a_value()
95+
{
96+
var argument = new Argument<string>(result => { return true; }, isDefault: true);
97+
98+
argument.GetDefaultValue()
99+
.Should()
100+
.BeNull();
101+
}
102+
103+
[Fact]
104+
public void GetDefaultValue_returns_null_when_parse_delegate_returns_true_and_sets_value_to_null()
105+
{
106+
var argument = new Argument<string>(result => { return true; }, isDefault: true);
107+
108+
argument.GetDefaultValue()
109+
.Should()
110+
.BeNull();
111+
}
112+
113+
[Fact]
114+
public void GetDefaultValue_can_return_null()
115+
{
116+
var argument = new Argument<string>(result => { return true; }, isDefault: true);
117+
118+
argument.GetDefaultValue()
119+
.Should()
120+
.BeNull();
121+
}
122+
123+
[Fact]
124+
public void validation_failure_message()
125+
{
126+
var argument = new Argument<FileSystemInfo>(result =>
127+
{
128+
result.ErrorMessage = "oops!";
129+
return true;
130+
});
131+
132+
argument.Parse("x")
133+
.Errors
134+
.Should()
135+
.ContainSingle(e => e.SymbolResult.Symbol == argument)
136+
.Which
137+
.Message
138+
.Should()
139+
.Be("oops!");
140+
}
141+
142+
[Fact]
143+
public void custom_parsing_of_scalar_value_from_an_argument_with_one_token()
144+
{
145+
var argument = new Argument<int>(result =>
146+
{
147+
result.Value = int.Parse(result.Tokens.Single().Value);
148+
149+
return true;
150+
});
151+
152+
argument.Parse("123")
153+
.FindResultFor(argument)
154+
.GetValueOrDefault()
155+
.Should()
156+
.Be(123);
157+
}
158+
159+
[Fact]
160+
public void custom_parsing_of_sequence_value_from_an_argument_with_one_token()
161+
{
162+
var argument = new Argument<IEnumerable<int>>(result =>
163+
{
164+
result.Value = result.Tokens.Single().Value.Split(",").Select(int.Parse);
165+
166+
return true;
167+
});
168+
169+
argument.Parse("1,2,3")
170+
.FindResultFor(argument)
171+
.GetValueOrDefault()
172+
.Should()
173+
.BeEquivalentTo(new[] { 1, 2, 3 });
174+
}
175+
176+
[Fact]
177+
public void custom_parsing_of_sequence_value_from_an_argument_with_multiple_tokens()
178+
{
179+
var argument = new Argument<IEnumerable<int>>(result =>
180+
{
181+
result.Value = result.Tokens.Select(t => int.Parse(t.Value)).ToArray();
182+
return true;
183+
});
184+
185+
argument.Parse("1 2 3")
186+
.FindResultFor(argument)
187+
.GetValueOrDefault()
188+
.Should()
189+
.BeEquivalentTo(new[] { 1, 2, 3 });
190+
}
191+
192+
[Fact]
193+
public void custom_parsing_of_scalar_value_from_an_argument_with_multiple_tokens()
194+
{
195+
var argument = new Argument<int>(result =>
196+
{
197+
result.Value = result.Tokens.Select(t => int.Parse(t.Value)).Sum();
198+
return true;
199+
});
200+
201+
argument.Arity = ArgumentArity.ZeroOrMore;
202+
203+
argument.Parse("1 2 3")
204+
.FindResultFor(argument)
205+
.GetValueOrDefault()
206+
.Should()
207+
.Be(6);
208+
}
209+
}
210+
53211
protected override Symbol CreateSymbol(string name)
54212
{
55213
return new Argument(name);

src/System.CommandLine/Argument{T}.cs

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,5 +64,63 @@ public Argument(TryConvertArgument<T> convert, Func<T> getDefaultValue = default
6464
SetDefaultValueFactory(() => getDefaultValue());
6565
}
6666
}
67+
68+
public Argument(ParseArgument<T> parse, bool isDefault = false) : this()
69+
{
70+
if (isDefault)
71+
{
72+
SetDefaultValueFactory(() =>
73+
{
74+
var argumentResult = new ArgumentResult<T>(
75+
this,
76+
null);
77+
78+
if (parse(argumentResult))
79+
{
80+
argumentResult.ValueWasSpecified = true;
81+
return argumentResult.Value;
82+
}
83+
else
84+
{
85+
return null;
86+
}
87+
});
88+
}
89+
90+
ConvertArguments = (ArgumentResult originalResult, out object value) =>
91+
{
92+
var newResult = new ArgumentResult<T>(
93+
this,
94+
originalResult.Parent);
95+
96+
foreach (var token in originalResult.Tokens)
97+
{
98+
newResult.AddToken(token);
99+
}
100+
101+
if (parse(newResult))
102+
{
103+
if (string.IsNullOrEmpty(newResult.ErrorMessage))
104+
{
105+
newResult.ValueWasSpecified = true;
106+
value = newResult.Value;
107+
originalResult.Parent.Children.Remove(originalResult);
108+
originalResult.Parent.Children.Add(newResult);
109+
return true;
110+
}
111+
else
112+
{
113+
originalResult.ErrorMessage = newResult.ErrorMessage;
114+
value = default(T);
115+
return false;
116+
}
117+
}
118+
else
119+
{
120+
value = default(T);
121+
return false;
122+
}
123+
};
124+
}
67125
}
68126
}

src/System.CommandLine/Parsing/ArgumentResult.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ internal ParseError CustomError(Argument argument)
3939
return null;
4040
}
4141

42-
internal ArgumentConversionResult Convert(
42+
internal virtual ArgumentConversionResult Convert(
4343
IArgument argument)
4444
{
4545
var parentResult = Parent;
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
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 System.CommandLine.Binding;
5+
6+
namespace System.CommandLine.Parsing
7+
{
8+
public class ArgumentResult<T> : ArgumentResult
9+
{
10+
private T _value;
11+
12+
internal ArgumentResult(
13+
Argument<T> argument,
14+
SymbolResult parent) : base(argument, parent)
15+
{
16+
}
17+
18+
public T Value
19+
{
20+
get => ArgumentConversionResult.GetValueOrDefault<T>();
21+
set
22+
{
23+
_value = value;
24+
ValueWasSpecified = true;
25+
}
26+
}
27+
28+
internal bool ValueWasSpecified { get; set; }
29+
30+
internal override ArgumentConversionResult Convert(IArgument argument)
31+
{
32+
if (ValueWasSpecified)
33+
{
34+
return new SuccessfulArgumentConversionResult(argument, _value);
35+
}
36+
else
37+
{
38+
return base.Convert(argument);
39+
}
40+
}
41+
}
42+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
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+
namespace System.CommandLine.Parsing
5+
{
6+
public delegate bool ParseArgument<T>(ArgumentResult<T> result);
7+
}

0 commit comments

Comments
 (0)