-
Notifications
You must be signed in to change notification settings - Fork 383
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Introduce Directive Symbol type #2063
Changes from 1 commit
0d76a52
8ce5271
3a24a45
ac851d3
d39fe8f
47f9a89
7127bc2
da63f85
96876bc
f200208
484bc57
fc987d4
ea619bf
f37d8df
8f16b15
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -2,7 +2,9 @@ | |
// Licensed under the MIT license. See LICENSE file in the project root for full license information. | ||
|
||
using System.Collections.Generic; | ||
using System.Globalization; | ||
using System.Linq; | ||
using System.Threading.Tasks; | ||
using FluentAssertions; | ||
using Xunit; | ||
|
||
|
@@ -137,6 +139,50 @@ public void When_directives_are_not_enabled_they_are_treated_as_regular_tokens() | |
.BeEquivalentTo("[hello]"); | ||
} | ||
|
||
[Theory] | ||
[InlineData(false)] | ||
[InlineData(true)] | ||
public async Task Directive_can_restore_the_state_after_running_continuation(bool async) | ||
{ | ||
const string plCulture = "pl-PL", enUsCulture = "en-US"; | ||
const string envVarName = "uniqueName", envVarValue = "just"; | ||
|
||
var before = CultureInfo.CurrentUICulture; | ||
|
||
try | ||
{ | ||
CultureInfo.CurrentUICulture = new(enUsCulture); | ||
|
||
bool invoked = false; | ||
Option<bool> option = new("-a"); | ||
RootCommand root = new() { option }; | ||
CommandLineBuilder builder = new(root); | ||
builder.Directives.Add(new EnvironmentVariablesDirective()); | ||
builder.Directives.Add(new CultureDirective()); | ||
root.SetHandler(ctx => | ||
{ | ||
invoked = true; | ||
CultureInfo.CurrentUICulture.Name.Should().Be(plCulture); | ||
Environment.GetEnvironmentVariable(envVarName).Should().Be(envVarValue); | ||
}); | ||
|
||
if (async) | ||
{ | ||
await builder.Build().InvokeAsync($"[culture:{plCulture}] [env:{envVarName}={envVarValue}]"); | ||
} | ||
else | ||
{ | ||
builder.Build().Invoke($"[culture:{plCulture}] [env:{envVarName}={envVarValue}]"); | ||
} | ||
|
||
invoked.Should().BeTrue(); | ||
} | ||
finally | ||
{ | ||
CultureInfo.CurrentUICulture = before; | ||
} | ||
} | ||
|
||
private static ParseResult Parse(Option option, Directive directive, string commandLine) | ||
{ | ||
RootCommand root = new() { option }; | ||
|
@@ -145,5 +191,47 @@ private static ParseResult Parse(Option option, Directive directive, string comm | |
|
||
return root.Parse(commandLine, builder.Build()); | ||
} | ||
|
||
private sealed class CultureDirective : Directive | ||
{ | ||
public CultureDirective() : base("culture") | ||
{ | ||
SetSynchronousHandler((ctx, next) => | ||
{ | ||
CultureInfo cultureBefore = CultureInfo.CurrentUICulture; | ||
|
||
try | ||
{ | ||
string cultureName = ctx.ParseResult.FindResultFor(this).Values.Single(); | ||
|
||
CultureInfo.CurrentUICulture = new CultureInfo(cultureName); | ||
|
||
next?.Invoke(ctx); | ||
} | ||
finally | ||
{ | ||
CultureInfo.CurrentUICulture = cultureBefore; | ||
} | ||
}); | ||
SetAsynchronousHandler(async (ctx, next, ct) => | ||
{ | ||
CultureInfo cultureBefore = CultureInfo.CurrentUICulture; | ||
|
||
try | ||
{ | ||
string cultureName = ctx.ParseResult.FindResultFor(this).Values.Single(); | ||
|
||
CultureInfo.CurrentUICulture = new CultureInfo(cultureName); | ||
|
||
await next?.InvokeAsync(ctx, ct); | ||
} | ||
finally | ||
{ | ||
CultureInfo.CurrentUICulture = cultureBefore; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. On .NET Core 3 or greater, this doesn't even need to restore CultureInfo.CurrentUICulture, because the runtime keeps the value in an AsyncLocal<CultureInfo> and it cannot propagate to the caller of the async lambda in any case. On .NET Framework, if the application targets something lower than .NET Framework 4.6, then CultureInfo.CurrentUICulture is thread-local by default. But in that environment, this can restore the culture to the wrong thread anyway. |
||
} | ||
}); | ||
} | ||
} | ||
|
||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,81 @@ | ||
using System.CommandLine.Parsing; | ||
using System.Threading; | ||
using System.Threading.Tasks; | ||
|
||
namespace System.CommandLine.Invocation | ||
{ | ||
internal sealed class ChainedCommandHandler : ICommandHandler | ||
{ | ||
private readonly SymbolResultTree _symbols; | ||
private readonly ICommandHandler? _commandHandler; | ||
|
||
internal ChainedCommandHandler(SymbolResultTree symbols, ICommandHandler? commandHandler) | ||
{ | ||
_symbols = symbols; | ||
_commandHandler = commandHandler; | ||
} | ||
|
||
public int Invoke(InvocationContext context) | ||
{ | ||
// We want to build a stack of (action, next) pairs. But we are not using any collection or LINQ. | ||
// Each handler is closure (a lambda with state), where state is the "next" handler. | ||
Action<InvocationContext, ICommandHandler?>? chainedHandler = _commandHandler is not null | ||
? (ctx, next) => _commandHandler.Invoke(ctx) | ||
: null; | ||
ICommandHandler? chainedHandlerArgument = null; | ||
|
||
foreach (var pair in _symbols) | ||
{ | ||
if (pair.Key is Directive directive && directive.HasHandler) | ||
{ | ||
var syncHandler = directive.SyncHandler | ||
?? throw new NotSupportedException($"Directive {directive.Name} does not provide a synchronous handler."); | ||
|
||
if (chainedHandler is not null) | ||
{ | ||
// capture the state in explicit way, to hint the compiler that the current state needs to be used | ||
var capturedHandler = chainedHandler; | ||
var capturedArgument = chainedHandlerArgument; | ||
|
||
chainedHandlerArgument = new AnonymousCommandHandler(ctx => capturedHandler.Invoke(ctx, capturedArgument)); | ||
} | ||
chainedHandler = syncHandler; | ||
} | ||
} | ||
|
||
chainedHandler!.Invoke(context, chainedHandlerArgument); | ||
|
||
return context.ExitCode; | ||
} | ||
|
||
public async Task<int> InvokeAsync(InvocationContext context, CancellationToken cancellationToken = default) | ||
{ | ||
Func<InvocationContext, ICommandHandler?, CancellationToken, Task>? chainedHandler = _commandHandler is not null | ||
? (ctx, next, ct) => _commandHandler.InvokeAsync(ctx, ct) | ||
: null; | ||
ICommandHandler? chainedHandlerArgument = null; | ||
|
||
foreach (var pair in _symbols) | ||
{ | ||
if (pair.Key is Directive directive && directive.HasHandler) | ||
{ | ||
var asyncHandler = directive.AsyncHandler | ||
?? throw new NotSupportedException($"Directive {directive.Name} does not provide an asynchronous handler."); | ||
|
||
if (chainedHandler is not null) | ||
{ | ||
var capturedHandler = chainedHandler; | ||
var capturedArgument = chainedHandlerArgument; | ||
|
||
chainedHandlerArgument = new AnonymousCommandHandler((ctx, ct) => capturedHandler.Invoke(ctx, capturedArgument, ct)); | ||
} | ||
chainedHandler = asyncHandler; | ||
} | ||
} | ||
|
||
await chainedHandler!.Invoke(context, chainedHandlerArgument, cancellationToken); | ||
|
||
return context.ExitCode; | ||
} | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If
next
is null, thenawait next?.InvokeAsync(ctx, ct)
meansawait (Task)null
, which throws NullReferenceException.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
C# doesn't yet have null-conditional await dotnet/csharplang#35.