22// Licensed under the MIT license. See LICENSE file in the project root for full license information.
33
44using System . CommandLine . Parsing ;
5+ using System . IO ;
56using System . Linq ;
7+ using System . Runtime . InteropServices ;
68using FluentAssertions ;
9+ using FluentAssertions . Execution ;
710using Xunit ;
811
912namespace System . CommandLine . Tests ;
@@ -15,18 +18,29 @@ public partial class RootCommandAndArg0
1518 [ Fact ]
1619 public void When_parsing_a_string_array_a_root_command_can_be_omitted_from_the_parsed_args ( )
1720 {
21+ var option = new Option < string > ( "-x" ) ;
1822 var command = new Command ( "outer" )
1923 {
2024 new Command ( "inner" )
2125 {
22- new Option < string > ( "-x" )
26+ option
2327 }
2428 } ;
2529
2630 var result1 = command . Parse ( Split ( "inner -x hello" ) ) ;
2731 var result2 = command . Parse ( Split ( "outer inner -x hello" ) ) ;
2832
33+ using var _ = new AssertionScope ( ) ;
34+
2935 result1 . Diagram ( ) . Should ( ) . Be ( result2 . Diagram ( ) ) ;
36+
37+ foreach ( var result in new [ ] { result1 , result2 } )
38+ {
39+ result . Errors . Should ( ) . BeEmpty ( ) ;
40+ result . RootCommandResult . Command . Name . Should ( ) . Be ( "outer" ) ;
41+ result . CommandResult . Command . Name . Should ( ) . Be ( "inner" ) ;
42+ result . GetValue ( option ) . Should ( ) . Be ( "hello" ) ;
43+ }
3044 }
3145
3246 [ Fact ]
@@ -40,8 +54,6 @@ public void When_parsing_a_string_array_input_then_a_full_path_to_an_executable_
4054 }
4155 } ;
4256
43- command . Parse ( Split ( "inner -x hello" ) ) . Errors . Should ( ) . BeEmpty ( ) ;
44-
4557 var parserResult = command . Parse ( Split ( $ "\" { RootCommand . ExecutablePath } \" inner -x hello") ) ;
4658 parserResult
4759 . Errors
@@ -52,18 +64,29 @@ public void When_parsing_a_string_array_input_then_a_full_path_to_an_executable_
5264 [ Fact ]
5365 public void When_parsing_an_unsplit_string_a_root_command_can_be_omitted_from_the_parsed_args ( )
5466 {
67+ var option = new Option < string > ( "-x" ) ;
5568 var command = new Command ( "outer" )
5669 {
5770 new Command ( "inner" )
5871 {
59- new Option < string > ( "-x" )
72+ option
6073 }
6174 } ;
6275
6376 var result1 = command . Parse ( "inner -x hello" ) ;
6477 var result2 = command . Parse ( "outer inner -x hello" ) ;
6578
79+ using var _ = new AssertionScope ( ) ;
80+
6681 result1 . Diagram ( ) . Should ( ) . Be ( result2 . Diagram ( ) ) ;
82+
83+ foreach ( var result in new [ ] { result1 , result2 } )
84+ {
85+ result . Errors . Should ( ) . BeEmpty ( ) ;
86+ result . RootCommandResult . Command . Name . Should ( ) . Be ( "outer" ) ;
87+ result . CommandResult . Command . Name . Should ( ) . Be ( "inner" ) ;
88+ result . GetValue ( option ) . Should ( ) . Be ( "hello" ) ;
89+ }
6790 }
6891
6992 [ Fact ]
@@ -82,25 +105,6 @@ public void When_parsing_an_unsplit_string_then_input_a_full_path_to_an_executab
82105 result2 . RootCommandResult . IdentifierToken . Value . Should ( ) . Be ( RootCommand . ExecutablePath ) ;
83106 }
84107
85- [ Fact ]
86- public void When_parsing_an_unsplit_string_then_a_renamed_RootCommand_can_be_omitted_from_the_parsed_args ( )
87- {
88- var rootCommand = new Command ( "outer" )
89- {
90- new Command ( "inner" )
91- {
92- new Option < string > ( "-x" )
93- }
94- } ;
95-
96- var result1 = rootCommand . Parse ( "inner -x hello" ) ;
97- var result2 = rootCommand . Parse ( "outer inner -x hello" ) ;
98- var result3 = rootCommand . Parse ( $ "{ RootCommand . ExecutableName } inner -x hello") ;
99-
100- result2 . RootCommandResult . Command . Should ( ) . BeSameAs ( result1 . RootCommandResult . Command ) ;
101- result3 . RootCommandResult . Command . Should ( ) . BeSameAs ( result1 . RootCommandResult . Command ) ;
102- }
103-
104108 [ Fact ]
105109 public void When_parsing_a_string_array_option_values_containing_command_name_after_slash_are_not_treated_as_root_command ( )
106110 {
@@ -112,6 +116,8 @@ public void When_parsing_a_string_array_option_values_containing_command_name_af
112116
113117 var result = command . Parse ( Split ( "/p:Key=something/myapp" ) ) ;
114118
119+ using var _ = new AssertionScope ( ) ;
120+
115121 result . Errors . Should ( ) . BeEmpty ( ) ;
116122 result . GetResult ( option ) . Should ( ) . NotBeNull ( ) ;
117123 }
@@ -130,10 +136,101 @@ public void When_parsing_a_string_array_option_values_ending_with_slash_command_
130136
131137 var result = command . Parse ( Split ( arg ) ) ;
132138
139+ using var _ = new AssertionScope ( ) ;
140+
133141 result . Errors . Should ( ) . BeEmpty ( ) ;
134142 result . GetResult ( option ) . Should ( ) . NotBeNull ( ) ;
135143 }
136144
145+ [ Theory ]
146+ [ MemberData ( nameof ( PathLikeFirstArgsMatchingTheRootCommand ) ) ]
147+ public void When_parsing_a_string_array_a_path_like_first_arg_matching_the_root_command_is_treated_as_the_root_command ( string pathLikeRootArg )
148+ {
149+ var option = new Option < string > ( "-x" ) ;
150+ var command = new Command ( "outer" )
151+ {
152+ new Command ( "inner" )
153+ {
154+ option
155+ }
156+ } ;
157+
158+ var result = command . Parse ( [ pathLikeRootArg , "inner" , "-x" , "hello" ] ) ;
159+
160+ using var _ = new AssertionScope ( ) ;
161+
162+ result . Errors . Should ( ) . BeEmpty ( ) ;
163+ result . RootCommandResult . IdentifierToken . Value . Should ( ) . Be ( pathLikeRootArg ) ;
164+ result . CommandResult . Command . Name . Should ( ) . Be ( "inner" ) ;
165+ result . GetValue ( option ) . Should ( ) . Be ( "hello" ) ;
166+ }
167+
168+ public static TheoryData < string > PathLikeFirstArgsMatchingTheRootCommand
169+ {
170+ get
171+ {
172+ var data = new TheoryData < string >
173+ {
174+ "./outer" ,
175+ "tools/outer"
176+ } ;
177+
178+ if ( RuntimeInformation . IsOSPlatform ( OSPlatform . Windows ) )
179+ {
180+ data . Add ( @".\outer" ) ;
181+ data . Add ( @"tools\outer" ) ;
182+ }
183+
184+ return data ;
185+ }
186+ }
187+
188+ [ Fact ]
189+ public void RootCommand_matches_its_executable_name_as_the_first_arg_the_same_way_a_named_Command_matches_its_name ( )
190+ {
191+ // Guards the assumption that a named Command can stand in for a RootCommand
192+ // in arg0-matching tests: RootCommand.Name is forced to the executable name and
193+ // cannot be changed, so the equivalent scenario uses the executable name as arg0.
194+ var option = new Option < string > ( "-x" ) ;
195+ var rootCommand = new RootCommand
196+ {
197+ new Command ( "inner" )
198+ {
199+ option
200+ }
201+ } ;
202+
203+ var result = rootCommand . Parse ( [ RootCommand . ExecutableName , "inner" , "-x" , "hello" ] ) ;
204+
205+ using var _ = new AssertionScope ( ) ;
206+
207+ result . Errors . Should ( ) . BeEmpty ( ) ;
208+ result . RootCommandResult . IdentifierToken . Value . Should ( ) . Be ( RootCommand . ExecutableName ) ;
209+ result . CommandResult . Command . Name . Should ( ) . Be ( "inner" ) ;
210+ result . GetValue ( option ) . Should ( ) . Be ( "hello" ) ;
211+ }
212+
213+ #if NETFRAMEWORK
214+ [ Fact ]
215+ public void When_parsing_a_string_array_arguments_containing_invalid_path_characters_the_netframework_PathGetFileName_exception_is_ignored ( )
216+ {
217+ const string invalidPath = "my\0 app" ;
218+
219+ // On .NET Framework, this throws ArgumentException for illegal characters in path.
220+ Action getFileName = ( ) => Path . GetFileName ( invalidPath ) ;
221+
222+ var argument = new Argument < string > ( "value" ) ;
223+ var command = new Command ( "myapp" ) { argument } ;
224+ var result = command . Parse ( invalidPath ) ;
225+
226+ using var _ = new AssertionScope ( ) ;
227+
228+ getFileName . Should ( ) . Throw < ArgumentException > ( ) ;
229+ result . Errors . Should ( ) . BeEmpty ( ) ;
230+ result . GetValue ( argument ) . Should ( ) . Be ( invalidPath ) ;
231+ }
232+ #endif
233+
137234 string [ ] Split ( string value ) => CommandLineParser . SplitCommandLine ( value ) . ToArray ( ) ;
138235 }
139236}
0 commit comments