Skip to content

Commit fc987d4

Browse files
committed
provide more flexible mechanism for directive handlers: possibility to pass next handler and invoke it or not
1 parent 484bc57 commit fc987d4

File tree

9 files changed

+269
-81
lines changed

9 files changed

+269
-81
lines changed

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

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -108,10 +108,10 @@ System.CommandLine
108108
public static System.Void Write(this IConsole console, System.String value)
109109
public static System.Void WriteLine(this IConsole console, System.String value)
110110
public class Directive : Symbol
111-
.ctor(System.String name, System.String description = null, System.Action<System.CommandLine.Invocation.InvocationContext> syncHandler = null, System.Func<System.CommandLine.Invocation.InvocationContext,System.Threading.CancellationToken,System.Threading.Tasks.Task> asyncHandler = null)
111+
.ctor(System.String name, System.String description = null, System.Action<System.CommandLine.Invocation.InvocationContext,ICommandHandler> syncHandler = null, System.Func<System.CommandLine.Invocation.InvocationContext,ICommandHandler,System.Threading.CancellationToken,System.Threading.Tasks.Task> asyncHandler = null)
112112
public System.Collections.Generic.IEnumerable<System.CommandLine.Completions.CompletionItem> GetCompletions(System.CommandLine.Completions.CompletionContext context)
113-
public System.Void SetAsynchronousHandler(System.Func<System.CommandLine.Invocation.InvocationContext,System.Threading.CancellationToken,System.Threading.Tasks.Task> handler)
114-
public System.Void SetSynchronousHandler(System.Action<System.CommandLine.Invocation.InvocationContext> handler)
113+
public System.Void SetAsynchronousHandler(System.Func<System.CommandLine.Invocation.InvocationContext,ICommandHandler,System.Threading.CancellationToken,System.Threading.Tasks.Task> handler)
114+
public System.Void SetSynchronousHandler(System.Action<System.CommandLine.Invocation.InvocationContext,ICommandHandler> handler)
115115
public class EnvironmentVariablesDirective : Directive
116116
.ctor()
117117
public static class Handler

src/System.CommandLine.Tests/DirectiveTests.cs

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,9 @@
22
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
33

44
using System.Collections.Generic;
5+
using System.Globalization;
56
using System.Linq;
7+
using System.Threading.Tasks;
68
using FluentAssertions;
79
using Xunit;
810

@@ -137,6 +139,50 @@ public void When_directives_are_not_enabled_they_are_treated_as_regular_tokens()
137139
.BeEquivalentTo("[hello]");
138140
}
139141

142+
[Theory]
143+
[InlineData(false)]
144+
[InlineData(true)]
145+
public async Task Directive_can_restore_the_state_after_running_continuation(bool async)
146+
{
147+
const string plCulture = "pl-PL", enUsCulture = "en-US";
148+
const string envVarName = "uniqueName", envVarValue = "just";
149+
150+
var before = CultureInfo.CurrentUICulture;
151+
152+
try
153+
{
154+
CultureInfo.CurrentUICulture = new(enUsCulture);
155+
156+
bool invoked = false;
157+
Option<bool> option = new("-a");
158+
RootCommand root = new() { option };
159+
CommandLineBuilder builder = new(root);
160+
builder.Directives.Add(new EnvironmentVariablesDirective());
161+
builder.Directives.Add(new CultureDirective());
162+
root.SetHandler(ctx =>
163+
{
164+
invoked = true;
165+
CultureInfo.CurrentUICulture.Name.Should().Be(plCulture);
166+
Environment.GetEnvironmentVariable(envVarName).Should().Be(envVarValue);
167+
});
168+
169+
if (async)
170+
{
171+
await builder.Build().InvokeAsync($"[culture:{plCulture}] [env:{envVarName}={envVarValue}]");
172+
}
173+
else
174+
{
175+
builder.Build().Invoke($"[culture:{plCulture}] [env:{envVarName}={envVarValue}]");
176+
}
177+
178+
invoked.Should().BeTrue();
179+
}
180+
finally
181+
{
182+
CultureInfo.CurrentUICulture = before;
183+
}
184+
}
185+
140186
private static ParseResult Parse(Option option, Directive directive, string commandLine)
141187
{
142188
RootCommand root = new() { option };
@@ -145,5 +191,47 @@ private static ParseResult Parse(Option option, Directive directive, string comm
145191

146192
return root.Parse(commandLine, builder.Build());
147193
}
194+
195+
private sealed class CultureDirective : Directive
196+
{
197+
public CultureDirective() : base("culture")
198+
{
199+
SetSynchronousHandler((ctx, next) =>
200+
{
201+
CultureInfo cultureBefore = CultureInfo.CurrentUICulture;
202+
203+
try
204+
{
205+
string cultureName = ctx.ParseResult.FindResultFor(this).Values.Single();
206+
207+
CultureInfo.CurrentUICulture = new CultureInfo(cultureName);
208+
209+
next?.Invoke(ctx);
210+
}
211+
finally
212+
{
213+
CultureInfo.CurrentUICulture = cultureBefore;
214+
}
215+
});
216+
SetAsynchronousHandler(async (ctx, next, ct) =>
217+
{
218+
CultureInfo cultureBefore = CultureInfo.CurrentUICulture;
219+
220+
try
221+
{
222+
string cultureName = ctx.ParseResult.FindResultFor(this).Values.Single();
223+
224+
CultureInfo.CurrentUICulture = new CultureInfo(cultureName);
225+
226+
await next?.InvokeAsync(ctx, ct);
227+
}
228+
finally
229+
{
230+
CultureInfo.CurrentUICulture = cultureBefore;
231+
}
232+
});
233+
}
234+
}
235+
148236
}
149237
}

src/System.CommandLine/Directive.cs

Lines changed: 14 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33
using System.CommandLine.Invocation;
44
using System.Threading.Tasks;
55
using System.Threading;
6-
using System.CommandLine.Parsing;
76

87
namespace System.CommandLine
98
{
@@ -18,17 +17,22 @@ namespace System.CommandLine
1817
/// </summary>
1918
public class Directive : Symbol
2019
{
20+
internal Action<InvocationContext, ICommandHandler?>? SyncHandler;
21+
internal Func<InvocationContext, ICommandHandler?, CancellationToken, Task>? AsyncHandler;
22+
2123
/// <summary>
2224
/// Initializes a new instance of the Directive class.
2325
/// </summary>
2426
/// <param name="name">The name of the directive. It can't contain whitespaces.</param>
2527
/// <param name="description">The description of the directive, shown in help.</param>
2628
/// <param name="syncHandler">The synchronous action that is invoked when directive is parsed.</param>
2729
/// <param name="asyncHandler">The asynchronous action that is invoked when directive is parsed.</param>
30+
/// <remarks>The second argument of both handlers is next handler than can be invoked.
31+
/// Example: a custom directive might just change current culture and run actual command afterwards.</remarks>
2832
public Directive(string name,
2933
string? description = null,
30-
Action<InvocationContext>? syncHandler = null,
31-
Func<InvocationContext, CancellationToken, Task>? asyncHandler = null)
34+
Action<InvocationContext, ICommandHandler?>? syncHandler = null,
35+
Func<InvocationContext, ICommandHandler?, CancellationToken, Task>? asyncHandler = null)
3236
{
3337
if (string.IsNullOrWhiteSpace(name))
3438
{
@@ -46,37 +50,17 @@ public Directive(string name,
4650
Name = name;
4751
Description = description;
4852

49-
if (syncHandler is not null)
50-
{
51-
SetSynchronousHandler(syncHandler);
52-
}
53-
else if (asyncHandler is not null)
54-
{
55-
SetAsynchronousHandler(asyncHandler);
56-
}
53+
SyncHandler = syncHandler;
54+
AsyncHandler = asyncHandler;
5755
}
5856

59-
public void SetAsynchronousHandler(Func<InvocationContext, CancellationToken, Task> handler)
60-
{
61-
if (handler is null)
62-
{
63-
throw new ArgumentNullException(nameof(handler));
64-
}
57+
internal bool HasHandler => SyncHandler != null || AsyncHandler != null;
6558

66-
Handler = new AnonymousCommandHandler(handler);
67-
}
68-
69-
public void SetSynchronousHandler(Action<InvocationContext> handler)
70-
{
71-
if (handler is null)
72-
{
73-
throw new ArgumentNullException(nameof(handler));
74-
}
75-
76-
Handler = new AnonymousCommandHandler(handler);
77-
}
59+
public void SetAsynchronousHandler(Func<InvocationContext, ICommandHandler?, CancellationToken, Task> handler)
60+
=> AsyncHandler = handler ?? throw new ArgumentNullException(nameof(handler));
7861

79-
internal ICommandHandler? Handler { get; private set; }
62+
public void SetSynchronousHandler(Action<InvocationContext, ICommandHandler?> handler)
63+
=> SyncHandler = handler ?? throw new ArgumentNullException(nameof(handler));
8064

8165
private protected override string DefaultName => throw new NotImplementedException();
8266

src/System.CommandLine/EnvironmentVariablesDirective.cs

Lines changed: 21 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
using System.CommandLine.Invocation;
2-
using System.CommandLine.Parsing;
1+
using System.CommandLine.Parsing;
2+
using System.Threading.Tasks;
33

44
namespace System.CommandLine
55
{
@@ -10,12 +10,28 @@ public sealed class EnvironmentVariablesDirective : Directive
1010
{
1111
public EnvironmentVariablesDirective() : base("env")
1212
{
13-
SetSynchronousHandler(SyncHandler);
13+
SetSynchronousHandler((context, next) =>
14+
{
15+
SetEnvVars(context.ParseResult);
16+
17+
next?.Invoke(context);
18+
});
19+
SetAsynchronousHandler((context, next, cancellationToken) =>
20+
{
21+
if (cancellationToken.IsCancellationRequested)
22+
{
23+
return Task.FromCanceled(cancellationToken);
24+
}
25+
26+
SetEnvVars(context.ParseResult);
27+
28+
return next?.InvokeAsync(context, cancellationToken) ?? Task.CompletedTask;
29+
});
1430
}
1531

16-
private void SyncHandler(InvocationContext context)
32+
private void SetEnvVars(ParseResult parseResult)
1733
{
18-
DirectiveResult directiveResult = context.ParseResult.FindResultFor(this)!;
34+
DirectiveResult directiveResult = parseResult.FindResultFor(this)!;
1935

2036
for (int i = 0; i < directiveResult.Values.Count; i++)
2137
{
@@ -35,9 +51,6 @@ private void SyncHandler(InvocationContext context)
3551
}
3652
}
3753
}
38-
39-
// we need a cleaner, more flexible and intuitive way of continuing the execution
40-
context.ParseResult.CommandResult.Command.Handler?.Invoke(context);
4154
}
4255
}
4356
}
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
using System.CommandLine.Parsing;
2+
using System.Threading;
3+
using System.Threading.Tasks;
4+
5+
namespace System.CommandLine.Invocation
6+
{
7+
internal sealed class ChainedCommandHandler : ICommandHandler
8+
{
9+
private readonly SymbolResultTree _symbols;
10+
private readonly ICommandHandler? _commandHandler;
11+
12+
internal ChainedCommandHandler(SymbolResultTree symbols, ICommandHandler? commandHandler)
13+
{
14+
_symbols = symbols;
15+
_commandHandler = commandHandler;
16+
}
17+
18+
public int Invoke(InvocationContext context)
19+
{
20+
// We want to build a stack of (action, next) pairs. But we are not using any collection or LINQ.
21+
// Each handler is closure (a lambda with state), where state is the "next" handler.
22+
Action<InvocationContext, ICommandHandler?>? chainedHandler = _commandHandler is not null
23+
? (ctx, next) => _commandHandler.Invoke(ctx)
24+
: null;
25+
ICommandHandler? chainedHandlerArgument = null;
26+
27+
foreach (var pair in _symbols)
28+
{
29+
if (pair.Key is Directive directive && directive.HasHandler)
30+
{
31+
var syncHandler = directive.SyncHandler
32+
?? throw new NotSupportedException($"Directive {directive.Name} does not provide a synchronous handler.");
33+
34+
if (chainedHandler is not null)
35+
{
36+
// capture the state in explicit way, to hint the compiler that the current state needs to be used
37+
var capturedHandler = chainedHandler;
38+
var capturedArgument = chainedHandlerArgument;
39+
40+
chainedHandlerArgument = new AnonymousCommandHandler(ctx => capturedHandler.Invoke(ctx, capturedArgument));
41+
}
42+
chainedHandler = syncHandler;
43+
}
44+
}
45+
46+
chainedHandler!.Invoke(context, chainedHandlerArgument);
47+
48+
return context.ExitCode;
49+
}
50+
51+
public async Task<int> InvokeAsync(InvocationContext context, CancellationToken cancellationToken = default)
52+
{
53+
Func<InvocationContext, ICommandHandler?, CancellationToken, Task>? chainedHandler = _commandHandler is not null
54+
? (ctx, next, ct) => _commandHandler.InvokeAsync(ctx, ct)
55+
: null;
56+
ICommandHandler? chainedHandlerArgument = null;
57+
58+
foreach (var pair in _symbols)
59+
{
60+
if (pair.Key is Directive directive && directive.HasHandler)
61+
{
62+
var asyncHandler = directive.AsyncHandler
63+
?? throw new NotSupportedException($"Directive {directive.Name} does not provide an asynchronous handler.");
64+
65+
if (chainedHandler is not null)
66+
{
67+
var capturedHandler = chainedHandler;
68+
var capturedArgument = chainedHandlerArgument;
69+
70+
chainedHandlerArgument = new AnonymousCommandHandler((ctx, ct) => capturedHandler.Invoke(ctx, capturedArgument, ct));
71+
}
72+
chainedHandler = asyncHandler;
73+
}
74+
}
75+
76+
await chainedHandler!.Invoke(context, chainedHandlerArgument, cancellationToken);
77+
78+
return context.ExitCode;
79+
}
80+
}
81+
}

src/System.CommandLine/ParseDirective.cs

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
using System.CommandLine.Invocation;
22
using System.CommandLine.IO;
33
using System.CommandLine.Parsing;
4+
using System.Threading.Tasks;
45

56
namespace System.CommandLine
67
{
@@ -12,17 +13,32 @@ public sealed class ParseDirective : Directive
1213
/// <param name="errorExitCode">If the parse result contains errors, this exit code will be used when the process exits.</param>
1314
public ParseDirective(int errorExitCode = 1) : base("parse")
1415
{
15-
SetSynchronousHandler(SyncHandler);
1616
ErrorExitCode = errorExitCode;
17+
18+
SetSynchronousHandler(PrintDiagramAndQuit);
19+
SetAsynchronousHandler((context, next, cancellationToken) =>
20+
{
21+
if (cancellationToken.IsCancellationRequested)
22+
{
23+
return Task.FromCanceled(cancellationToken);
24+
}
25+
26+
PrintDiagramAndQuit(context, null);
27+
28+
return Task.CompletedTask;
29+
});
1730
}
1831

1932
internal int ErrorExitCode { get; }
2033

21-
private void SyncHandler(InvocationContext context)
34+
private void PrintDiagramAndQuit(InvocationContext context, ICommandHandler? next)
2235
{
2336
var parseResult = context.ParseResult;
2437
context.Console.Out.WriteLine(parseResult.Diagram());
2538
context.ExitCode = parseResult.Errors.Count == 0 ? 0 : ErrorExitCode;
39+
40+
// parse directive has a precedence over --help and --version and any command
41+
// we don't invoke next here.
2642
}
2743
}
2844
}

0 commit comments

Comments
 (0)