1+ using System . Runtime . CompilerServices ;
12using System . Web ;
23using DotNetCampus . Cli . Exceptions ;
34using DotNetCampus . Cli . Utils . Collections ;
@@ -10,6 +11,7 @@ namespace DotNetCampus.Cli.Utils.Parsers;
1011/// </summary>
1112internal 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