Skip to content

Commit 704e9a9

Browse files
committed
Refactor based on feedback from @MiYanni
1 parent 071ad75 commit 704e9a9

File tree

5 files changed

+165
-142
lines changed

5 files changed

+165
-142
lines changed
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
# Microsoft.Dotnet.Cli.CommandLine
2+
3+
This project contains extensions and utilities for building command line applications.
4+
5+
These extensions are layered on top of core System.CommandLine concepts and types, and
6+
do not directly reference concepts that are specific to the `dotnet` CLI. We hope that
7+
these would be published separately as a NuGet package for use by other command line
8+
applications in the future.
9+
10+
From a layering perspective, everything that is specific to the `dotnet` CLI should
11+
be in the `src/Cli/dotnet` or `src/Cli/Microsoft.DotNet.Cli.Utils` projects, which
12+
reference this project. Keep this one generally-speaking clean.
Lines changed: 153 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,153 @@
1+
using System.Collections.Immutable;
2+
using System.CommandLine;
3+
using System.CommandLine.Parsing;
4+
5+
namespace Microsoft.DotNet.Cli.CommandLine;
6+
7+
public static class SpanParserExtensions
8+
{
9+
extension<T>(Option<T> o) where T : ISpanParsable<T>
10+
{
11+
/// <summary>
12+
/// Configures the option with a custom parser that uses the <see cref="ISpanParsable{T}"/> implementation to parse the tokens provided.
13+
/// Will parse a single token with <see cref="ISpanParser{T}.Parse(ReadOnlySpan{char})"/>, and if the option allows multiple tokens will take the 'last one wins' approach.
14+
/// </summary>
15+
/// <remarks>
16+
/// Without this, Options will fall-back to using potentially-reflection-based parsing in S.CL, or
17+
/// if the type doesn't have built-in S.CL parsing support will fail to parse at runtime.
18+
/// </remarks>
19+
public Option<T> AsSpanParsable()
20+
{
21+
o.CustomParser = StaticSingleItemParser<T>;
22+
return o;
23+
}
24+
}
25+
26+
extension<T>(Option<IReadOnlyCollection<T>> o) where T : ISpanParsable<T>
27+
{
28+
/// <summary>
29+
/// Configures the option with a custom parser that uses the <see cref="ISpanParsable{T}"/> implementation to parse the tokens provided.
30+
/// This parser handles multiple tokens, using <see cref="ISpanParser{T}.Parse(ReadOnlySpan{char})"/> for each token.
31+
/// </summary>
32+
/// <remarks>
33+
/// Without this, Options will fall-back to using potentially-reflection-based parsing in S.CL, or
34+
/// if the type doesn't have built-in S.CL parsing support will fail to parse at runtime.
35+
/// </remarks>
36+
public Option<IReadOnlyCollection<T>> AsSpanParsable()
37+
{
38+
o.CustomParser = StaticMultiItemItemParser<T>;
39+
return o;
40+
}
41+
}
42+
43+
extension<T>(Argument<T> a) where T : ISpanParsable<T>
44+
{
45+
/// <summary>
46+
/// Configures the argument with a custom parser that uses the <see cref="ISpanParsable{T}"/> implementation to parse the value.
47+
/// Will parse a single token with <see cref="ISpanParser{T}.Parse(ReadOnlySpan{char})"/>, and if the argument allows multiple tokens will take the 'last one wins' approach.
48+
/// </summary>
49+
/// <remarks>
50+
/// Without this, Arguments will fall-back to using potentially-reflection-based parsing in S.CL, or
51+
/// if the type doesn't have built-in S.CL parsing support will fail to parse at runtime.
52+
/// </remarks>
53+
public Argument<T> AsSpanParsable()
54+
{
55+
a.CustomParser = StaticSingleItemParser<T>;
56+
return a;
57+
}
58+
}
59+
60+
extension<T>(Argument<IReadOnlyCollection<T>> a) where T : ISpanParsable<T>
61+
{
62+
/// <summary>
63+
/// Configures the argument with a custom parser that uses the <see cref="ISpanParsable{T}"/> implementation to parse the value.
64+
/// This parser handles multiple tokens, using <see cref="ISpanParser{T}.Parse(ReadOnlySpan{char})"/> for each token.
65+
/// </summary>
66+
/// <remarks>
67+
/// Without this, Arguments will fall-back to using potentially-reflection-based parsing in S.CL, or
68+
/// if the type doesn't have built-in S.CL parsing support will fail to parse at runtime.
69+
/// </remarks>
70+
public Argument<IReadOnlyCollection<T>> AsSpanParsable()
71+
{
72+
a.CustomParser = StaticMultiItemItemParser<T>;
73+
return a;
74+
}
75+
}
76+
77+
internal static IReadOnlyCollection<T>? StaticMultiItemItemParser<T>(ArgumentResult tokenizationResult)
78+
where T : ISpanParsable<T>
79+
{
80+
if (tokenizationResult.Tokens.Count == 0)
81+
{
82+
return default;
83+
}
84+
85+
var parentName =
86+
tokenizationResult.Parent switch
87+
{
88+
OptionResult optionResult => optionResult.Option.Name,
89+
ArgumentResult argumentResult => argumentResult.Argument.Name,
90+
CommandResult or null => tokenizationResult.Argument.Name,
91+
_ => "<unknown>"
92+
};
93+
var coll = ImmutableArray.CreateBuilder<T>(tokenizationResult.Tokens.Count);
94+
95+
foreach (var token in tokenizationResult.Tokens)
96+
{
97+
var tokenToParse = token.Value;
98+
99+
if (string.IsNullOrEmpty(tokenToParse))
100+
{
101+
tokenizationResult.AddError($"Cannot parse null or empty value for symbol '{parentName}'");
102+
continue;
103+
}
104+
105+
if (!T.TryParse(tokenToParse, null, out var result))
106+
{
107+
tokenizationResult.AddError($"Cannot parse value '{tokenToParse}' for symbol '{parentName}' as a {typeof(T).Name}");
108+
continue;
109+
}
110+
111+
coll.Add(result);
112+
}
113+
114+
return coll.ToImmutableArray();
115+
}
116+
117+
internal static T? StaticSingleItemParser<T>(ArgumentResult tokenizationResult)
118+
where T : ISpanParsable<T>
119+
{
120+
if (tokenizationResult.Tokens.Count == 0)
121+
{
122+
return default;
123+
}
124+
125+
var parentName =
126+
tokenizationResult.Parent switch
127+
{
128+
OptionResult optionResult => optionResult.Option.Name,
129+
ArgumentResult argumentResult => argumentResult.Argument.Name,
130+
CommandResult or null => tokenizationResult.Argument.Name,
131+
_ => "<unknown>"
132+
};
133+
// we explicitly only support parsing one token, so let's do a last-one-wins approach here
134+
var tokenToParse =
135+
tokenizationResult.Tokens switch
136+
{
137+
[var onlyToken] => onlyToken.Value,
138+
_ => tokenizationResult.Tokens[^1].Value
139+
};
140+
141+
if (string.IsNullOrEmpty(tokenToParse))
142+
{
143+
tokenizationResult.AddError($"Cannot parse null or empty value for symbol '{parentName}'");
144+
}
145+
146+
if (!T.TryParse(tokenToParse, null, out var result))
147+
{
148+
tokenizationResult.AddError($"Cannot parse value '{tokenToParse}' for symbol '{parentName}' as a {typeof(T).Name}");
149+
}
150+
151+
return result;
152+
}
153+
}

src/Cli/Microsoft.DotNet.Cli.CommandLine/SpanParseableArgument.cs

Lines changed: 0 additions & 29 deletions
This file was deleted.

src/Cli/Microsoft.DotNet.Cli.CommandLine/SpanParseableOption.cs

Lines changed: 0 additions & 29 deletions
This file was deleted.

src/Cli/Microsoft.DotNet.Cli.CommandLine/SpanParserHelpers.cs

Lines changed: 0 additions & 84 deletions
This file was deleted.

0 commit comments

Comments
 (0)