Skip to content

Commit a9f4669

Browse files
committed
使用相同的方式实现高性能的 Url 命令行解析
1 parent d6f361c commit a9f4669

File tree

1 file changed

+227
-61
lines changed

1 file changed

+227
-61
lines changed
Lines changed: 227 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
using System.Runtime.CompilerServices;
12
using System.Web;
23
using DotNetCampus.Cli.Exceptions;
34
using DotNetCampus.Cli.Utils.Collections;
@@ -10,6 +11,7 @@ namespace DotNetCampus.Cli.Utils.Parsers;
1011
/// </summary>
1112
internal sealed class UrlStyleParser : ICommandLineParser
1213
{
14+
private const string FragmentName = "fragment";
1315
private readonly string _scheme;
1416

1517
/// <summary>
@@ -30,97 +32,261 @@ public CommandLineParsedResult Parse(IReadOnlyList<string> commandLineArguments)
3032

3133
var url = commandLineArguments[0];
3234

33-
// 验证URL格式:scheme://[path][?query][#fragment]
34-
if (!url.StartsWith($"{_scheme}://", StringComparison.OrdinalIgnoreCase))
35-
{
36-
throw new CommandLineParseException($"URL must start with '{_scheme}://'");
37-
}
38-
3935
var longOptions = new OptionDictionary(true);
40-
List<string> arguments = [];
36+
var shortOptions = new OptionDictionary(true);
4137
string? guessedVerbName = null;
38+
List<string> arguments = [];
4239

43-
// 移除scheme://前缀
44-
string urlWithoutScheme = url.Substring(_scheme.Length + 3);
45-
46-
// 分离fragment
47-
string urlWithoutFragment = urlWithoutScheme;
40+
string? lastParameterName = null;
41+
var lastType = UrlParsedType.Start;
4842

49-
int fragmentIndex = urlWithoutScheme.IndexOf('#');
50-
if (fragmentIndex >= 0)
43+
for (var i = 0; i < url.Length;)
5144
{
52-
urlWithoutFragment = urlWithoutScheme.Substring(0, fragmentIndex);
53-
var fragment = urlWithoutScheme.Substring(fragmentIndex + 1);
54-
55-
// 添加fragment作为选项
56-
longOptions.AddValue("fragment", fragment);
57-
}
45+
var result = UrlPart.ReadNext(url, ref i, lastType);
46+
lastType = result.Type;
5847

59-
// 分离查询参数和路径
60-
string path = urlWithoutFragment;
61-
int queryIndex = urlWithoutFragment.IndexOf('?');
48+
if (result.Type is UrlParsedType.VerbOrPositionalArgument)
49+
{
50+
lastParameterName = null;
51+
guessedVerbName = result.Value;
52+
arguments.Add(guessedVerbName);
53+
continue;
54+
}
6255

63-
if (queryIndex >= 0)
64-
{
65-
path = urlWithoutFragment.Substring(0, queryIndex);
66-
string queryString = urlWithoutFragment.Substring(queryIndex + 1);
56+
if (result.Type is UrlParsedType.PositionalArgument)
57+
{
58+
lastParameterName = null;
59+
arguments.Add(result.Value);
60+
continue;
61+
}
6762

68-
// 解析查询字符串参数
69-
ParseQueryString(queryString, longOptions);
70-
}
63+
if (result.Type is UrlParsedType.ParameterName)
64+
{
65+
lastParameterName = result.Name;
66+
longOptions.AddOption(result.Name);
67+
continue;
68+
}
7169

72-
// 如果路径不为空,将其添加为位置参数
73-
if (!string.IsNullOrEmpty(path))
74-
{
75-
// URL解码路径
76-
string decodedPath = HttpUtility.UrlDecode(path);
77-
string[] pathSegments = decodedPath.Split('/', StringSplitOptions.RemoveEmptyEntries);
70+
if (result.Type is UrlParsedType.ParameterValue)
71+
{
72+
if (lastParameterName is null)
73+
{
74+
throw new CommandLineParseException($"Invalid URL format: {url}. Parameter value '{result.Value}' without a name.");
75+
}
7876

79-
arguments.AddRange(pathSegments);
77+
longOptions.AddValue(lastParameterName, result.Value);
78+
lastParameterName = null;
79+
continue;
80+
}
8081

81-
// 猜测动词名称
82-
if (pathSegments.Length > 0)
82+
if (result.Type is UrlParsedType.Fragment)
8383
{
84-
guessedVerbName = pathSegments[0];
84+
lastParameterName = null;
85+
longOptions.AddValue(result.Name, result.Value);
86+
continue;
8587
}
8688
}
8789

8890
return new CommandLineParsedResult(guessedVerbName,
8991
longOptions,
90-
// URL 不支持短选项,所以直接使用空字典。
91-
OptionDictionary.Empty,
92+
shortOptions,
9293
arguments.ToReadOnlyList());
9394
}
9495

95-
private static void ParseQueryString(string queryString, OptionDictionary options)
96+
97+
internal readonly ref struct UrlPart(UrlParsedType type)
9698
{
97-
if (string.IsNullOrEmpty(queryString))
99+
public UrlParsedType Type { get; } = type;
100+
public string Name { get; private init; } = "";
101+
public string Value { get; private init; } = "";
102+
103+
public static UrlPart ReadNext(string url, ref int index, UrlParsedType lastType)
98104
{
99-
return;
100-
}
105+
if (lastType is UrlParsedType.Start)
106+
{
107+
// 取出第一个位置参数(或谓词)
108+
var startIndex = -1;
109+
for (var i = index; i < url.Length - 3; i++)
110+
{
111+
if (url[i] is ':' && url[i + 1] is '/' && url[i + 2] is '/')
112+
{
113+
startIndex = i + 3;
114+
break;
115+
}
116+
}
117+
if (startIndex < 0)
118+
{
119+
throw new CommandLineParseException($"Invalid URL format: {url}. Missing '://'");
120+
}
121+
var endIndex = url.IndexOfAny(['/', '?', '#', '&'], startIndex);
122+
if (endIndex < 0)
123+
{
124+
endIndex = url.Length;
125+
index = endIndex + 1;
126+
}
127+
else
128+
{
129+
index = endIndex;
130+
}
131+
var value = HttpUtility.UrlDecode(url.AsSpan(startIndex, endIndex - startIndex).ToString());
132+
return new UrlPart(UrlParsedType.VerbOrPositionalArgument)
133+
{
134+
Value = value,
135+
};
136+
}
137+
138+
if (lastType is UrlParsedType.VerbOrPositionalArgument or UrlParsedType.PositionalArgument)
139+
{
140+
return url[index] switch
141+
{
142+
// 新的位置参数。
143+
'/' => ReadNextPositionalArgument(url, ref index),
144+
// 查询参数名。
145+
'?' => ReadNextParameterName(url, ref index),
146+
// 片段。
147+
'#' => ReadFragment(url, ref index),
148+
_ => throw new CommandLineParseException($"Invalid URL format: {url}. Expected '/', '?' or '#' after a positional argument."),
149+
};
150+
}
151+
152+
if (lastType is UrlParsedType.ParameterName)
153+
{
154+
return url[index] switch
155+
{
156+
// 查询参数值。
157+
'=' => ReadNextParameterValue(url, ref index),
158+
// 查询新的参数名。
159+
'&' => ReadNextParameterName(url, ref index),
160+
// 片段。
161+
'#' => ReadFragment(url, ref index),
162+
_ => throw new CommandLineParseException($"Invalid URL format: {url}. Expected '=', '&' or '#' after a parameter name."),
163+
};
164+
}
101165

102-
string[] queryParams = queryString.Split('&');
166+
if (lastType is UrlParsedType.ParameterValue)
167+
{
168+
return url[index] switch
169+
{
170+
// 查询新的参数名。
171+
'&' => ReadNextParameterName(url, ref index),
172+
// 片段。
173+
'#' => ReadFragment(url, ref index),
174+
_ => throw new CommandLineParseException($"Invalid URL format: {url}. Expected '&' or '#' after a parameter value."),
175+
};
176+
}
103177

104-
foreach (var param in queryParams)
178+
throw new CommandLineParseException($"Invalid URL format: {url}");
179+
}
180+
181+
[MethodImpl(MethodImplOptions.AggressiveInlining)]
182+
private static UrlPart ReadNextPositionalArgument(string url, ref int index)
105183
{
106-
// 处理无值参数 (如 ?debug)
107-
if (!param.Contains('='))
184+
var startIndex = index;
185+
var endIndex = url.IndexOfAny(['/', '?', '#', '&'], startIndex + 1);
186+
if (endIndex < 0)
108187
{
109-
string decodedName1 = HttpUtility.UrlDecode(param);
110-
options.AddOption(OptionName.MakeKebabCase(decodedName1.AsSpan()));
111-
continue;
188+
endIndex = url.Length;
189+
index = endIndex + 1;
112190
}
191+
else
192+
{
193+
index = endIndex;
194+
}
195+
var value = HttpUtility.UrlDecode(url.AsSpan(startIndex + 1, endIndex - startIndex - 1).ToString());
196+
index = endIndex;
197+
return new UrlPart(UrlParsedType.PositionalArgument)
198+
{
199+
Value = value,
200+
};
201+
}
113202

114-
// 处理有值参数 (如 ?name=value)
115-
int equalIndex = param.IndexOf('=');
116-
string name = param.Substring(0, equalIndex);
117-
string value = equalIndex + 1 < param.Length ? param.Substring(equalIndex + 1) : string.Empty;
203+
[MethodImpl(MethodImplOptions.AggressiveInlining)]
204+
private static UrlPart ReadNextParameterName(string url, ref int index)
205+
{
206+
var startIndex = index;
207+
var endIndex = url.IndexOfAny(['=', '#', '&'], index + 1);
208+
if (endIndex < 0)
209+
{
210+
endIndex = url.Length;
211+
index = endIndex + 1;
212+
}
213+
else
214+
{
215+
index = endIndex;
216+
}
217+
var value = HttpUtility.UrlDecode(url.AsSpan(startIndex + 1, endIndex - startIndex - 1).ToString());
218+
index = endIndex;
219+
return new UrlPart(UrlParsedType.ParameterName)
220+
{
221+
Name = OptionName.MakeKebabCase(value),
222+
};
223+
}
118224

119-
// URL解码参数名和值
120-
string decodedName = HttpUtility.UrlDecode(name);
121-
string decodedValue = HttpUtility.UrlDecode(value);
225+
[MethodImpl(MethodImplOptions.AggressiveInlining)]
226+
private static UrlPart ReadNextParameterValue(string url, ref int index)
227+
{
228+
var startIndex = index;
229+
var endIndex = url.IndexOfAny(['&', '#'], index + 1);
230+
if (endIndex < 0)
231+
{
232+
endIndex = url.Length;
233+
index = endIndex + 1;
234+
}
235+
else
236+
{
237+
index = endIndex;
238+
}
239+
var value = HttpUtility.UrlDecode(url.AsSpan(startIndex + 1, endIndex - startIndex - 1).ToString());
240+
index = endIndex;
241+
return new UrlPart(UrlParsedType.ParameterValue)
242+
{
243+
Value = value,
244+
};
245+
}
122246

123-
options.AddValue(OptionName.MakeKebabCase(decodedName.AsSpan()), decodedValue);
247+
[MethodImpl(MethodImplOptions.AggressiveInlining)]
248+
private static UrlPart ReadFragment(string url, ref int index)
249+
{
250+
var startIndex = index;
251+
index = url.Length + 1;
252+
return new UrlPart(UrlParsedType.Fragment)
253+
{
254+
Name = FragmentName,
255+
Value = HttpUtility.UrlDecode(url.AsSpan(startIndex + 1).ToString()),
256+
};
124257
}
125258
}
126259
}
260+
261+
internal enum UrlParsedType
262+
{
263+
/// <summary>
264+
/// 尚未开始解析。
265+
/// </summary>
266+
Start,
267+
268+
/// <summary>
269+
/// 第一个位置参数,也可能是谓词。
270+
/// </summary>
271+
VerbOrPositionalArgument,
272+
273+
/// <summary>
274+
/// 位置参数。
275+
/// </summary>
276+
PositionalArgument,
277+
278+
/// <summary>
279+
/// 查询参数名。
280+
/// </summary>
281+
ParameterName,
282+
283+
/// <summary>
284+
/// 查询参数值。
285+
/// </summary>
286+
ParameterValue,
287+
288+
/// <summary>
289+
/// 片段参数名。
290+
/// </summary>
291+
Fragment,
292+
}

0 commit comments

Comments
 (0)