1
+ using System . Runtime . CompilerServices ;
1
2
using System . Web ;
2
3
using DotNetCampus . Cli . Exceptions ;
3
4
using DotNetCampus . Cli . Utils . Collections ;
@@ -10,6 +11,7 @@ namespace DotNetCampus.Cli.Utils.Parsers;
10
11
/// </summary>
11
12
internal sealed class UrlStyleParser : ICommandLineParser
12
13
{
14
+ private const string FragmentName = "fragment" ;
13
15
private readonly string _scheme ;
14
16
15
17
/// <summary>
@@ -30,97 +32,261 @@ public CommandLineParsedResult Parse(IReadOnlyList<string> commandLineArguments)
30
32
31
33
var url = commandLineArguments [ 0 ] ;
32
34
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
-
39
35
var longOptions = new OptionDictionary ( true ) ;
40
- List < string > arguments = [ ] ;
36
+ var shortOptions = new OptionDictionary ( true ) ;
41
37
string ? guessedVerbName = null ;
38
+ List < string > arguments = [ ] ;
42
39
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 ;
48
42
49
- int fragmentIndex = urlWithoutScheme . IndexOf ( '#' ) ;
50
- if ( fragmentIndex >= 0 )
43
+ for ( var i = 0 ; i < url . Length ; )
51
44
{
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 ;
58
47
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
+ }
62
55
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
+ }
67
62
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
+ }
71
69
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
+ }
78
76
79
- arguments . AddRange ( pathSegments ) ;
77
+ longOptions . AddValue ( lastParameterName , result . Value ) ;
78
+ lastParameterName = null ;
79
+ continue ;
80
+ }
80
81
81
- // 猜测动词名称
82
- if ( pathSegments . Length > 0 )
82
+ if ( result . Type is UrlParsedType . Fragment )
83
83
{
84
- guessedVerbName = pathSegments [ 0 ] ;
84
+ lastParameterName = null ;
85
+ longOptions . AddValue ( result . Name , result . Value ) ;
86
+ continue ;
85
87
}
86
88
}
87
89
88
90
return new CommandLineParsedResult ( guessedVerbName ,
89
91
longOptions ,
90
- // URL 不支持短选项,所以直接使用空字典。
91
- OptionDictionary . Empty ,
92
+ shortOptions ,
92
93
arguments . ToReadOnlyList ( ) ) ;
93
94
}
94
95
95
- private static void ParseQueryString ( string queryString , OptionDictionary options )
96
+
97
+ internal readonly ref struct UrlPart ( UrlParsedType type )
96
98
{
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 )
98
104
{
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
+ }
101
165
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
+ }
103
177
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 )
105
183
{
106
- // 处理无值参数 (如 ?debug)
107
- if ( ! param . Contains ( '=' ) )
184
+ var startIndex = index ;
185
+ var endIndex = url . IndexOfAny ( [ '/' , '?' , '#' , '&' ] , startIndex + 1 ) ;
186
+ if ( endIndex < 0 )
108
187
{
109
- string decodedName1 = HttpUtility . UrlDecode ( param ) ;
110
- options . AddOption ( OptionName . MakeKebabCase ( decodedName1 . AsSpan ( ) ) ) ;
111
- continue ;
188
+ endIndex = url . Length ;
189
+ index = endIndex + 1 ;
112
190
}
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
+ }
113
202
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
+ }
118
224
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
+ }
122
246
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
+ } ;
124
257
}
125
258
}
126
259
}
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