Skip to content

Commit 18069a6

Browse files
authored
Fix #1663 (#1671)
1 parent 6c71bd7 commit 18069a6

File tree

4 files changed

+143
-105
lines changed

4 files changed

+143
-105
lines changed
Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
// Copyright (c) .NET Foundation and contributors. All rights reserved.
2+
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
3+
4+
using System.CommandLine.Parsing;
5+
using System.Linq;
6+
using FluentAssertions;
7+
using Xunit;
8+
9+
namespace System.CommandLine.Tests;
10+
11+
public partial class ParserTests
12+
{
13+
public partial class RootCommandAndArg0
14+
{
15+
[Fact]
16+
public void When_parsing_a_string_array_a_root_command_can_be_omitted_from_the_parsed_args()
17+
{
18+
var command = new Command("outer")
19+
{
20+
new Command("inner")
21+
{
22+
new Option<string>("-x")
23+
}
24+
};
25+
26+
var result1 = command.Parse(Split("inner -x hello"));
27+
var result2 = command.Parse(Split("outer inner -x hello"));
28+
29+
result1.Diagram().Should().Be(result2.Diagram());
30+
}
31+
32+
[Fact]
33+
public void When_parsing_a_string_array_input_then_a_full_path_to_an_executable_is_not_matched_by_the_root_command()
34+
{
35+
var command = new RootCommand
36+
{
37+
new Command("inner")
38+
{
39+
new Option<string>("-x")
40+
}
41+
};
42+
43+
command.Parse(Split("inner -x hello")).Errors.Should().BeEmpty();
44+
45+
command.Parse(Split($"{RootCommand.ExecutablePath} inner -x hello"))
46+
.Errors
47+
.Should()
48+
.ContainSingle(e => e.Message == $"{LocalizationResources.Instance.UnrecognizedCommandOrArgument(RootCommand.ExecutablePath)}");
49+
}
50+
51+
[Fact]
52+
public void When_parsing_an_unsplit_string_a_root_command_can_be_omitted_from_the_parsed_args()
53+
{
54+
var command = new Command("outer")
55+
{
56+
new Command("inner")
57+
{
58+
new Option<string>("-x")
59+
}
60+
};
61+
62+
var result1 = command.Parse("inner -x hello");
63+
var result2 = command.Parse("outer inner -x hello");
64+
65+
result1.Diagram().Should().Be(result2.Diagram());
66+
}
67+
68+
[Fact]
69+
public void When_parsing_an_unsplit_string_then_input_a_full_path_to_an_executable_is_matched_by_the_root_command()
70+
{
71+
var command = new RootCommand
72+
{
73+
new Command("inner")
74+
{
75+
new Option<string>("-x")
76+
}
77+
};
78+
79+
var result2 = command.Parse($"{RootCommand.ExecutablePath} inner -x hello");
80+
81+
result2.RootCommandResult.Token.Value.Should().Be(RootCommand.ExecutablePath);
82+
}
83+
84+
[Fact]
85+
public void When_parsing_an_unsplit_string_then_a_renamed_RootCommand_can_be_omitted_from_the_parsed_args()
86+
{
87+
var rootCommand = new RootCommand
88+
{
89+
new Command("inner")
90+
{
91+
new Option<string>("-x")
92+
}
93+
};
94+
rootCommand.Name = "outer";
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+
104+
string[] Split(string value) => CommandLineStringSplitter.Instance.Split(value).ToArray();
105+
}
106+
}

src/System.CommandLine.Tests/ParserTests.cs

Lines changed: 4 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,9 @@
1616
namespace System.CommandLine.Tests
1717
{
1818
public partial class ParserTests
19-
{
19+
{public partial class RootCommandAndArg0
20+
{
21+
}
2022
private readonly ITestOutputHelper _output;
2123

2224
public ParserTests(ITestOutputHelper output)
@@ -774,61 +776,6 @@ public void Subsequent_occurrences_of_tokens_matching_command_names_are_parsed_a
774776
completeResult.Tokens.Select(t => t.Value).Should().BeEquivalentTo("the-command");
775777
}
776778

777-
[Fact]
778-
public void A_root_command_can_be_omitted_from_the_parsed_args()
779-
{
780-
var command = new Command("outer")
781-
{
782-
new Command("inner")
783-
{
784-
new Option<string>("-x")
785-
}
786-
};
787-
788-
var result1 = command.Parse("inner -x hello");
789-
var result2 = command.Parse("outer inner -x hello");
790-
791-
result1.Diagram().Should().Be(result2.Diagram());
792-
}
793-
794-
[Fact]
795-
public void A_root_command_can_match_a_full_path_to_an_executable()
796-
{
797-
var command = new RootCommand
798-
{
799-
new Command("inner")
800-
{
801-
new Option<string>("-x")
802-
}
803-
};
804-
805-
ParseResult result1 = command.Parse("inner -x hello");
806-
807-
ParseResult result2 = command.Parse($"{RootCommand.ExecutablePath} inner -x hello");
808-
809-
result1.Diagram().Should().Be(result2.Diagram());
810-
}
811-
812-
[Fact]
813-
public void A_renamed_RootCommand_can_be_omitted_from_the_parsed_args()
814-
{
815-
var rootCommand = new RootCommand
816-
{
817-
new Command("inner")
818-
{
819-
new Option<string>("-x")
820-
}
821-
};
822-
rootCommand.Name = "outer";
823-
824-
var result1 = rootCommand.Parse("inner -x hello");
825-
var result2 = rootCommand.Parse("outer inner -x hello");
826-
var result3 = rootCommand.Parse($"{RootCommand.ExecutableName} inner -x hello");
827-
828-
result2.RootCommandResult.Command.Should().BeSameAs(result1.RootCommandResult.Command);
829-
result3.RootCommandResult.Command.Should().BeSameAs(result1.RootCommandResult.Command);
830-
}
831-
832779
[Fact]
833780
public void Absolute_unix_style_paths_are_lexed_correctly()
834781
{
@@ -1581,7 +1528,7 @@ public void Tokens_are_not_split_if_the_part_before_the_delimiter_is_not_an_opti
15811528
}
15821529

15831530
[Fact]
1584-
public void A_subcommand_wont_overflow_when_checking_maximum_argument_capcity()
1531+
public void A_subcommand_wont_overflow_when_checking_maximum_argument_capacity()
15851532
{
15861533
// Tests bug identified in https://github.com/dotnet/command-line-api/issues/997
15871534

src/System.CommandLine/Parsing/Parser.cs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,9 @@ public ParseResult Parse(
4444
IReadOnlyList<string> arguments,
4545
string? rawInput = null)
4646
{
47-
var tokenizeResult = arguments.Tokenize(Configuration);
47+
var tokenizeResult = arguments.Tokenize(
48+
Configuration,
49+
inferRootCommand: rawInput is not null);
4850

4951
var operation = new ParseOperation(
5052
tokenizeResult,

src/System.CommandLine/Parsing/StringExtensions.cs

Lines changed: 30 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
using System.Collections.Generic;
55
using System.Globalization;
66
using System.IO;
7+
using System.Linq;
78

89
namespace System.CommandLine.Parsing
910
{
@@ -66,14 +67,18 @@ internal static (string? Prefix, string Alias) SplitPrefix(this string rawAlias)
6667

6768
internal static TokenizeResult Tokenize(
6869
this IReadOnlyList<string> args,
69-
CommandLineConfiguration configuration)
70+
CommandLineConfiguration configuration,
71+
bool inferRootCommand = true)
7072
{
7173
var errorList = new List<TokenizeError>();
7274

7375
Command currentCommand = configuration.RootCommand;
7476
var foundDoubleDash = false;
7577
var foundEndOfDirectives = !configuration.EnableDirectives;
76-
var argList = NormalizeRootCommand(configuration, args);
78+
79+
List<string> argList;
80+
argList = NormalizeRootCommand(args, configuration.RootCommand, inferRootCommand);
81+
7782
var tokenList = new List<Token>(argList.Count);
7883

7984
var knownTokens = configuration.RootCommand.ValidTokens();
@@ -308,8 +313,9 @@ void ReadResponseFile(string filePath, int i)
308313
}
309314

310315
private static List<string> NormalizeRootCommand(
311-
CommandLineConfiguration commandLineConfiguration,
312-
IReadOnlyList<string>? args)
316+
IReadOnlyList<string>? args,
317+
Command rootCommand,
318+
bool inferRootCommand = true)
313319
{
314320
if (args is null)
315321
{
@@ -318,64 +324,41 @@ private static List<string> NormalizeRootCommand(
318324

319325
var list = new List<string>();
320326

321-
string? potentialRootCommand = null;
322-
323327
if (args.Count > 0)
324328
{
325-
try
329+
if (inferRootCommand &&
330+
args[0] == RootCommand.ExecutablePath)
326331
{
327-
potentialRootCommand = Path.GetFileName(args[0]);
332+
list.AddRange(args);
333+
return list;
328334
}
329-
catch (ArgumentException)
335+
else
330336
{
331-
// possible exception for illegal characters in path on .NET Framework
332-
}
337+
try
338+
{
339+
var potentialRootCommand = Path.GetFileName(args[0]);
333340

334-
if (potentialRootCommand != null &&
335-
commandLineConfiguration.RootCommand.HasAlias(potentialRootCommand))
336-
{
337-
list.AddRange(args);
338-
return list;
341+
if (rootCommand.HasAlias(potentialRootCommand))
342+
{
343+
list.AddRange(args);
344+
return list;
345+
}
346+
}
347+
catch (ArgumentException)
348+
{
349+
// possible exception for illegal characters in path on .NET Framework
350+
}
339351
}
340352
}
341353

342-
var commandName = commandLineConfiguration.RootCommand.Name;
343-
344-
list.Add(commandName);
345-
346-
int startAt = 0;
354+
list.Add(rootCommand.Name);
347355

348-
if (FirstArgMatchesRootCommand())
349-
{
350-
startAt = 1;
351-
}
352-
353-
for (var i = startAt; i < args.Count; i++)
356+
for (var i = 0; i < args.Count; i++)
354357
{
355358
list.Add(args[i]);
356359
}
357360

358361
return list;
359-
360-
bool FirstArgMatchesRootCommand()
361-
{
362-
if (potentialRootCommand is null)
363-
{
364-
return false;
365-
}
366-
367-
if (potentialRootCommand.Equals($"{commandName}.dll", StringComparison.OrdinalIgnoreCase))
368-
{
369-
return true;
370-
}
371-
372-
if (potentialRootCommand.Equals($"{commandName}.exe", StringComparison.OrdinalIgnoreCase))
373-
{
374-
return true;
375-
}
376-
377-
return false;
378-
}
379362
}
380363

381364
private static string? GetResponseFileReference(this string arg) =>

0 commit comments

Comments
 (0)