Skip to content

Commit 20d7700

Browse files
authored
Merge pull request #781 from jonsequitur/dont-mutate-command-more-than-once-on-implicit-invoke
don't mutate command on subsequent calls to Invoke extension
2 parents ea987f1 + 7911c78 commit 20d7700

File tree

6 files changed

+89
-10
lines changed

6 files changed

+89
-10
lines changed
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
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.Builder;
5+
using System.CommandLine.IO;
6+
using FluentAssertions;
7+
using Xunit;
8+
9+
namespace System.CommandLine.Tests
10+
{
11+
public class CommandExtensionsTests
12+
{
13+
[Fact]
14+
public void Command_Invoke_can_be_called_more_than_once_for_the_same_command()
15+
{
16+
var command = new RootCommand("Root command description")
17+
{
18+
new Command("inner")
19+
};
20+
21+
var console1 = new TestConsole();
22+
23+
command.Invoke("-h", console1);
24+
25+
console1.Out.ToString().Should().Contain(command.Description);
26+
27+
var console2 = new TestConsole();
28+
29+
command.Invoke("-h", console2);
30+
31+
console2.Out.ToString().Should().Contain(command.Description);
32+
}
33+
34+
[Fact]
35+
public void When_CommandLineBuilder_is_used_then_Command_Invoke_uses_its_configuration()
36+
{
37+
var command = new RootCommand();
38+
39+
new CommandLineBuilder(command)
40+
.UseMiddleware(context =>
41+
{
42+
context.Console.Out.Write("hello!");
43+
})
44+
.Build();
45+
46+
var console = new TestConsole();
47+
48+
command.Invoke("", console);
49+
50+
console.Out
51+
.ToString()
52+
.Should()
53+
.Contain("hello!");
54+
}
55+
}
56+
}

src/System.CommandLine.Tests/CommandTests.cs

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -336,18 +336,31 @@ public void When_multiple_options_are_configured_then_they_must_differ_by_name()
336336
}
337337

338338
[Fact]
339-
public void When_global_options_are_added_then_they_must_differ_from_local_options_by_name()
339+
public void Global_options_may_be_added_with_aliases_that_conflict_with_local_options()
340340
{
341341
var command = new Command("the-command")
342342
{
343343
new Option("--same")
344344
};
345345

346+
command
347+
.Invoking(c => c.AddGlobalOption(new Option("--same")))
348+
.Should()
349+
.NotThrow<ArgumentException>();
350+
}
351+
352+
[Fact]
353+
public void Global_options_may_not_have_aliases_conflicting_with_other_global_option_aliases()
354+
{
355+
var command = new Command("the-command");
356+
357+
command.AddGlobalOption(new Option("--same"));
358+
346359
command
347360
.Invoking(c => c.AddGlobalOption(new Option("--same")))
348361
.Should()
349362
.Throw<ArgumentException>()
350-
.And
363+
.Which
351364
.Message
352365
.Should()
353366
.Be("Alias '--same' is already in use.");

src/System.CommandLine/Builder/CommandLineBuilder.cs

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,10 @@ public class CommandLineBuilder : CommandBuilder
1717
public CommandLineBuilder(Command rootCommand = null)
1818
: base(rootCommand ?? new RootCommand())
1919
{
20+
if (rootCommand?.ImplicitParser != null)
21+
{
22+
throw new ArgumentException($"Command \"{rootCommand.Name}\" has already been configured.");
23+
}
2024
}
2125

2226
public bool EnableDirectives { get; set; } = true;
@@ -33,7 +37,7 @@ public Parser Build()
3337
{
3438
var rootCommand = Command;
3539

36-
return new Parser(
40+
var parser = new Parser(
3741
new CommandLineConfiguration(
3842
new[] { rootCommand },
3943
enablePosixBundling: EnablePosixBundling,
@@ -44,6 +48,10 @@ public Parser Build()
4448
.Select(m => m.middleware)
4549
.ToArray(),
4650
helpBuilderFactory: HelpBuilderFactory));
51+
52+
Command.ImplicitParser = parser;
53+
54+
return parser;
4755
}
4856

4957
internal void AddMiddleware(

src/System.CommandLine/Builder/CommandLineBuilderExtensions.cs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -401,7 +401,6 @@ public static CommandLineBuilder UseSuggestDirective(
401401
return builder;
402402
}
403403

404-
405404
public static CommandLineBuilder UseTypoCorrections(
406405
this CommandLineBuilder builder, int maxLevenshteinDistance = 3)
407406
{

src/System.CommandLine/Command.cs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33

44
using System.Collections;
55
using System.Collections.Generic;
6+
using System.CommandLine.Builder;
67
using System.CommandLine.Collections;
78
using System.CommandLine.Invocation;
89
using System.CommandLine.Parsing;
@@ -32,7 +33,6 @@ public Command(string name, string description = null) : base(new[] { name }, de
3233

3334
public void AddGlobalOption(Option option)
3435
{
35-
Children.ThrowIfAnyAliasIsInUse(option);
3636
_globalOptions.Add(option);
3737
Children.AddWithoutAliasCollisionCheck(option);
3838
}
@@ -68,5 +68,7 @@ private protected override void AddSymbol(Symbol symbol)
6868
IEnumerable<IArgument> ICommand.Arguments => Arguments;
6969

7070
IEnumerable<IOption> ICommand.Options => Options;
71+
72+
internal Parser ImplicitParser { get; set; }
7173
}
7274
}

src/System.CommandLine/CommandExtensions.cs

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -42,11 +42,12 @@ public static Task<int> InvokeAsync(
4242

4343
private static InvocationPipeline GetInvocationPipeline(Command command, string[] args)
4444
{
45-
var parser = new CommandLineBuilder(command)
46-
.UseDefaults()
47-
.Build();
45+
var parser = command.ImplicitParser ??
46+
new CommandLineBuilder(command)
47+
.UseDefaults()
48+
.Build();
4849

49-
ParseResult parseResult = parser.Parse(args);
50+
var parseResult = parser.Parse(args);
5051

5152
return new InvocationPipeline(parseResult);
5253
}
@@ -62,4 +63,4 @@ public static ParseResult Parse(
6263
IReadOnlyCollection<char> delimiters = null) =>
6364
new Parser(command).Parse(commandLine);
6465
}
65-
}
66+
}

0 commit comments

Comments
 (0)