Skip to content

Commit 7f19ebf

Browse files
authored
enable CliAction to be non-exclusive (#2147)
* rename symbol types with Cli prefix; some property renames and file moves * update API baseline files * rename ParseErrorResultAction -> ParseErrorAction * add support for CliAction nonexclusivity * don't use LINQ * more PR feedback
1 parent 2819f48 commit 7f19ebf

15 files changed

+379
-163
lines changed

src/System.CommandLine.ApiCompatibility.Tests/ApiCompatibilityApprovalTests.System_CommandLine_api_is_not_changed.approved.txt

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,10 @@ System.CommandLine
1717
public static CliArgument<System.IO.FileSystemInfo> AcceptExistingOnly(this CliArgument<System.IO.FileSystemInfo> argument)
1818
public static CliArgument<T> AcceptExistingOnly<T>(this CliArgument<T> argument)
1919
public abstract class CliAction
20+
public System.Boolean Exclusive { get; }
2021
public System.Int32 Invoke(ParseResult parseResult)
2122
public System.Threading.Tasks.Task<System.Int32> InvokeAsync(ParseResult parseResult, System.Threading.CancellationToken cancellationToken = null)
23+
protected System.Void set_Exclusive(System.Boolean value)
2224
public abstract class CliArgument : CliSymbol
2325
public ArgumentArity Arity { get; set; }
2426
public System.Collections.Generic.List<System.Func<System.CommandLine.Completions.CompletionContext,System.Collections.Generic.IEnumerable<System.CommandLine.Completions.CompletionItem>>> CompletionSources { get; }
@@ -76,6 +78,8 @@ System.CommandLine
7678
public ParseResult Parse(System.Collections.Generic.IReadOnlyList<System.String> args)
7779
public ParseResult Parse(System.String commandLine)
7880
public System.Void ThrowIfInvalid()
81+
public class CliConfigurationException : System.Exception, System.Runtime.Serialization.ISerializable
82+
.ctor(System.String message)
7983
public class CliDirective : CliSymbol
8084
.ctor(System.String name)
8185
public CliAction Action { get; set; }
@@ -111,8 +115,6 @@ System.CommandLine
111115
public System.Collections.Generic.IEnumerable<CliSymbol> Parents { get; }
112116
public System.Collections.Generic.IEnumerable<System.CommandLine.Completions.CompletionItem> GetCompletions(System.CommandLine.Completions.CompletionContext context)
113117
public System.String ToString()
114-
public class CommandLineConfigurationException : System.Exception, System.Runtime.Serialization.ISerializable
115-
.ctor(System.String message)
116118
public static class CompletionSourceExtensions
117119
public static System.Void Add(this System.Collections.Generic.List<System.Func<System.CommandLine.Completions.CompletionContext,System.Collections.Generic.IEnumerable<System.CommandLine.Completions.CompletionItem>>> completionSources, System.Func<System.CommandLine.Completions.CompletionContext,System.Collections.Generic.IEnumerable<System.String>> completionsDelegate)
118120
public static System.Void Add(this System.Collections.Generic.List<System.Func<System.CommandLine.Completions.CompletionContext,System.Collections.Generic.IEnumerable<System.CommandLine.Completions.CompletionItem>>> completionSources, System.String[] completions)

src/System.CommandLine.Tests/CommandLineConfigurationTests.cs

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66

77
namespace System.CommandLine.Tests;
88

9-
public class CommandLineConfigurationTests
9+
public class CliConfigurationTests
1010
{
1111
[Fact]
1212
public void ThrowIfInvalid_throws_if_there_are_duplicate_sibling_option_aliases_on_the_root_command()
@@ -26,7 +26,7 @@ public void ThrowIfInvalid_throws_if_there_are_duplicate_sibling_option_aliases_
2626
var validate = () => config.ThrowIfInvalid();
2727

2828
validate.Should()
29-
.Throw<CommandLineConfigurationException>()
29+
.Throw<CliConfigurationException>()
3030
.Which
3131
.Message
3232
.Should()
@@ -54,7 +54,7 @@ public void ThrowIfInvalid_throws_if_there_are_duplicate_sibling_option_aliases_
5454
var validate = () => config.ThrowIfInvalid();
5555

5656
validate.Should()
57-
.Throw<CommandLineConfigurationException>()
57+
.Throw<CliConfigurationException>()
5858
.Which
5959
.Message
6060
.Should()
@@ -79,7 +79,7 @@ public void ThrowIfInvalid_throws_if_there_are_duplicate_sibling_subcommand_alia
7979
var validate = () => config.ThrowIfInvalid();
8080

8181
validate.Should()
82-
.Throw<CommandLineConfigurationException>()
82+
.Throw<CliConfigurationException>()
8383
.Which
8484
.Message
8585
.Should()
@@ -103,7 +103,7 @@ public void ThrowIfInvalid_throws_if_there_are_duplicate_sibling_subcommand_alia
103103
var validate = () => config.ThrowIfInvalid();
104104

105105
validate.Should()
106-
.Throw<CommandLineConfigurationException>()
106+
.Throw<CliConfigurationException>()
107107
.Which
108108
.Message
109109
.Should()
@@ -128,7 +128,7 @@ public void ThrowIfInvalid_throws_if_sibling_command_and_option_aliases_collide_
128128
var validate = () => config.ThrowIfInvalid();
129129

130130
validate.Should()
131-
.Throw<CommandLineConfigurationException>()
131+
.Throw<CliConfigurationException>()
132132
.Which
133133
.Message
134134
.Should()
@@ -156,7 +156,7 @@ public void ThrowIfInvalid_throws_if_sibling_command_and_option_aliases_collide_
156156
var validate = () => config.ThrowIfInvalid();
157157

158158
validate.Should()
159-
.Throw<CommandLineConfigurationException>()
159+
.Throw<CliConfigurationException>()
160160
.Which
161161
.Message
162162
.Should()
@@ -179,7 +179,7 @@ public void ThrowIfInvalid_throws_if_there_are_duplicate_sibling_global_option_a
179179
var validate = () => config.ThrowIfInvalid();
180180

181181
validate.Should()
182-
.Throw<CommandLineConfigurationException>()
182+
.Throw<CliConfigurationException>()
183183
.Which
184184
.Message
185185
.Should()
@@ -235,7 +235,7 @@ public void ThrowIfInvalid_throws_if_a_command_is_its_own_parent()
235235
var validate = () => config.ThrowIfInvalid();
236236

237237
validate.Should()
238-
.Throw<CommandLineConfigurationException>()
238+
.Throw<CliConfigurationException>()
239239
.Which
240240
.Message
241241
.Should()
@@ -254,7 +254,7 @@ public void ThrowIfInvalid_throws_if_a_parentage_cycle_is_detected()
254254
var validate = () => config.ThrowIfInvalid();
255255

256256
validate.Should()
257-
.Throw<CommandLineConfigurationException>()
257+
.Throw<CliConfigurationException>()
258258
.Which
259259
.Message
260260
.Should()

src/System.CommandLine.Tests/DirectiveTests.cs

Lines changed: 115 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
// Copyright (c) .NET Foundation and contributors. All rights reserved.
22
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
33

4-
using System.Collections.Generic;
54
using System.Linq;
5+
using System.Threading;
6+
using System.Threading.Tasks;
67
using FluentAssertions;
8+
using FluentAssertions.Execution;
79
using Xunit;
810

911
namespace System.CommandLine.Tests
@@ -31,6 +33,16 @@ public void Raw_tokens_still_hold_directives()
3133
result.Tokens.Should().Contain(t => t.Value == "[parse]");
3234
}
3335

36+
[Fact]
37+
public void Directives_must_precede_other_symbols()
38+
{
39+
CliDirective directive = new("parse");
40+
41+
ParseResult result = Parse(new CliOption<bool>("-y"), directive, "-y [parse]");
42+
43+
result.FindResultFor(directive).Should().BeNull();
44+
}
45+
3446
[Fact]
3547
public void Multiple_directives_are_allowed()
3648
{
@@ -47,14 +59,111 @@ public void Multiple_directives_are_allowed()
4759
result.FindResultFor(suggestDirective).Should().NotBeNull();
4860
}
4961

50-
[Fact]
51-
public void Directives_must_be_the_first_argument()
62+
[Theory]
63+
[InlineData(true)]
64+
[InlineData(false)]
65+
public async Task Multiple_instances_of_the_same_directive_can_be_invoked(bool invokeAsync)
5266
{
53-
CliDirective directive = new("parse");
67+
var commandActionWasCalled = false;
68+
var directiveCallCount = 0;
69+
70+
var testDirective = new TestDirective("test")
71+
{
72+
Action = new NonexclusiveTestAction(_ => directiveCallCount++)
73+
};
74+
75+
var config = new CliConfiguration(new CliRootCommand
76+
{
77+
Action = new NonexclusiveTestAction(_ => commandActionWasCalled = true)
78+
})
79+
{
80+
Directives = { testDirective }
81+
};
82+
83+
if (invokeAsync)
84+
{
85+
await config.InvokeAsync("[test:1] [test:2]");
86+
}
87+
else
88+
{
89+
config.Invoke("[test:1] [test:2]");
90+
}
91+
92+
using var _ = new AssertionScope();
93+
94+
commandActionWasCalled.Should().BeTrue();
95+
directiveCallCount.Should().Be(2);
96+
}
5497

55-
ParseResult result = Parse(new CliOption<bool>("-y"), directive, "-y [parse]");
98+
[Theory]
99+
[InlineData(true)]
100+
[InlineData(false)]
101+
public async Task Multiple_different_directives_can_be_invoked(bool invokeAsync)
102+
{
103+
bool commandActionWasCalled = false;
104+
bool directiveOneActionWasCalled = false;
105+
bool directiveTwoActionWasCalled = false;
106+
107+
var directiveOne = new TestDirective("one")
108+
{
109+
Action = new NonexclusiveTestAction(_ => directiveOneActionWasCalled = true)
110+
};
111+
var directiveTwo = new TestDirective("two")
112+
{
113+
Action = new NonexclusiveTestAction(_ => directiveTwoActionWasCalled = true)
114+
};
115+
var config = new CliConfiguration(new CliRootCommand
116+
{
117+
Action = new NonexclusiveTestAction(_ => commandActionWasCalled = true)
118+
})
119+
{
120+
Directives = { directiveOne, directiveTwo }
121+
};
122+
123+
if (invokeAsync)
124+
{
125+
await config.InvokeAsync("[one] [two]");
126+
}
127+
else
128+
{
129+
config.Invoke("[one] [two]");
130+
}
131+
132+
using var _ = new AssertionScope();
133+
134+
commandActionWasCalled.Should().BeTrue();
135+
directiveOneActionWasCalled.Should().BeTrue();
136+
directiveTwoActionWasCalled.Should().BeTrue();
137+
}
56138

57-
result.FindResultFor(directive).Should().BeNull();
139+
public class TestDirective : CliDirective
140+
{
141+
public TestDirective(string name) : base(name)
142+
{
143+
}
144+
}
145+
146+
private class NonexclusiveTestAction : CliAction
147+
{
148+
private readonly Action<ParseResult> _invoke;
149+
150+
public NonexclusiveTestAction(Action<ParseResult> invoke)
151+
{
152+
_invoke = invoke;
153+
Exclusive = false;
154+
}
155+
156+
public override int Invoke(ParseResult parseResult)
157+
{
158+
_invoke(parseResult);
159+
return 0;
160+
}
161+
162+
public override Task<int> InvokeAsync(ParseResult parseResult, CancellationToken cancellationToken = default)
163+
{
164+
;
165+
return Task.FromResult(Invoke(parseResult));
166+
}
58167
}
59168

60169
[Theory]

0 commit comments

Comments
 (0)