Skip to content

add SetAction overloads returning int and Task<int> #2121

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

Merged
merged 3 commits into from
Mar 28, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,9 @@ System.CommandLine
public ParseResult Parse(System.Collections.Generic.IReadOnlyList<System.String> args, CommandLineConfiguration configuration = null)
public ParseResult Parse(System.String commandLine, CommandLineConfiguration configuration = null)
public System.Void SetAction(System.Action<System.CommandLine.Invocation.InvocationContext> action)
public System.Void SetAction(System.Func<System.CommandLine.Invocation.InvocationContext,System.Int32> action)
public System.Void SetAction(System.Func<System.CommandLine.Invocation.InvocationContext,System.Threading.CancellationToken,System.Threading.Tasks.Task> action)
public System.Void SetAction(System.Func<System.CommandLine.Invocation.InvocationContext,System.Threading.CancellationToken,System.Threading.Tasks.Task<System.Int32>> action)
public class CommandLineConfiguration
.ctor(Command rootCommand)
public System.Collections.Generic.List<Directive> Directives { get; }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ namespace System.CommandLine.Tests.Invocation
public class InvocationExtensionsTests
{
[Fact]
public async Task Command_InvokeAsync_uses_default_pipeline_by_default()
public async Task Command_InvokeAsync_enables_help_by_default()
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

thank you for updating the test names 👍

{
var command = new Command("the-command")
{
Expand All @@ -31,14 +31,13 @@ public async Task Command_InvokeAsync_uses_default_pipeline_by_default()

await command.Parse("-h", config).InvokeAsync();

output
.ToString()
.Should()
.Contain(theHelpText);
output.ToString()
.Should()
.Contain(theHelpText);
}

[Fact]
public void Command_Invoke_uses_default_pipeline_by_default()
public void Command_Invoke_enables_help_by_default()
{
var command = new Command("the-command")
{
Expand All @@ -55,10 +54,9 @@ public void Command_Invoke_uses_default_pipeline_by_default()

command.Parse("-h", config).Invoke();

output
.ToString()
.Should()
.Contain(theHelpText);
output.ToString()
.Should()
.Contain(theHelpText);
}

[Fact]
Expand Down Expand Up @@ -132,17 +130,66 @@ public void RootCommand_Invoke_returns_1_when_handler_throws()
}

[Fact]
public async Task RootCommand_Action_can_set_custom_result_code()
public void Custom_RootCommand_Action_can_set_custom_result_code_via_Invoke()
{
var rootCommand = new RootCommand()
var rootCommand = new RootCommand
{
Action = new CustomExitCodeAction()
};

rootCommand.Parse("").Invoke().Should().Be(123);
}

[Fact]
public async Task Custom_RootCommand_Action_can_set_custom_result_code_via_InvokeAsync()
{
var rootCommand = new RootCommand
{
Action = new CustomExitCodeAction()
};

(await rootCommand.Parse("").InvokeAsync()).Should().Be(456);
}

[Fact]
public void Anonymous_RootCommand_Task_returning_Action_can_set_custom_result_code_via_Invoke()
{
var rootCommand = new RootCommand();

rootCommand.SetAction((_, _) => Task.FromResult(123));

rootCommand.Parse("").Invoke().Should().Be(123);
}

[Fact]
public async Task Anonymous_RootCommand_Task_returning_Action_can_set_custom_result_code_via_InvokeAsync()
{
var rootCommand = new RootCommand();

rootCommand.SetAction((_, _) => Task.FromResult(123));

(await rootCommand.Parse("").InvokeAsync()).Should().Be(123);
}
[Fact]
public void Anonymous_RootCommand_int_returning_Action_can_set_custom_result_code_via_Invoke()
{
var rootCommand = new RootCommand();

rootCommand.SetAction(_ => 123);

rootCommand.Parse("").Invoke().Should().Be(123);
}

[Fact]
public async Task Anonymous_RootCommand_int_returning_Action_can_set_custom_result_code_via_InvokeAsync()
{
var rootCommand = new RootCommand();

rootCommand.SetAction(_ => 123);

(await rootCommand.Parse("").InvokeAsync()).Should().Be(123);
}

internal sealed class CustomExitCodeAction : CliAction
{
public override int Invoke(InvocationContext context)
Expand All @@ -157,7 +204,7 @@ public async Task Command_InvokeAsync_with_cancelation_token_invokes_command_han
{
using CancellationTokenSource cts = new();
var command = new Command("test");
command.SetAction((InvocationContext context, CancellationToken cancellationToken) =>
command.SetAction((_, cancellationToken) =>
{
cancellationToken.Should().Be(cts.Token);
return Task.CompletedTask;
Expand Down
58 changes: 54 additions & 4 deletions src/System.CommandLine/Command.cs
Original file line number Diff line number Diff line change
Expand Up @@ -101,16 +101,66 @@ public IEnumerable<Symbol> Children
public CliAction? Action { get; set; }

/// <summary>
/// Sets a synchronous action.
/// Sets a synchronous action to be run when the command is invoked.
/// </summary>
public void SetAction(Action<InvocationContext> action)
=> Action = new AnonymousCliAction(action);
{
if (action is null)
{
throw new ArgumentNullException(nameof(action));
}

Action = new AnonymousCliAction(context =>
{
action(context);
return 0;
});
}

/// <summary>
/// Sets a synchronous action to be run when the command is invoked.
/// </summary>
/// <remarks>The value returned from the <paramref name="action"/> delegate can be used to set the process exit code.</remarks>
public void SetAction(Func<InvocationContext, int> action)
{
if (action is null)
{
throw new ArgumentNullException(nameof(action));
}

Action = new AnonymousCliAction(action);
}

/// <summary>
/// Sets an asynchronous action.
/// Sets an asynchronous action to be run when the command is invoked.
/// </summary>
public void SetAction(Func<InvocationContext, CancellationToken, Task> action)
=> Action = new AnonymousCliAction(action);
{
if (action is null)
{
throw new ArgumentNullException(nameof(action));
}

Action = new AnonymousCliAction(async (context, cancellationToken) =>
{
await action(context, cancellationToken);
return 0;
});
}

/// <summary>
/// Sets an asynchronous action when the command is invoked.
/// </summary>
/// <remarks>The value returned from the <paramref name="action"/> delegate can be used to set the process exit code.</remarks>
public void SetAction(Func<InvocationContext, CancellationToken, Task<int>> action)
{
if (action is null)
{
throw new ArgumentNullException(nameof(action));
}

Action = new AnonymousCliAction(action);
}

/// <summary>
/// Adds a <see cref="Symbol"/> to the command.
Expand Down
28 changes: 12 additions & 16 deletions src/System.CommandLine/Invocation/AnonymousCliAction.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,44 +8,40 @@ namespace System.CommandLine.Invocation
{
internal sealed class AnonymousCliAction : CliAction
{
private readonly Func<InvocationContext, CancellationToken, Task>? _asyncAction;
private readonly Action<InvocationContext>? _syncAction;
private readonly Func<InvocationContext, CancellationToken, Task<int>>? _asyncAction;
private readonly Func<InvocationContext, int>? _syncAction;

internal AnonymousCliAction(Action<InvocationContext> action)
=> _syncAction = action ?? throw new ArgumentNullException(nameof(action));
internal AnonymousCliAction(Func<InvocationContext, int> action)
=> _syncAction = action;

internal AnonymousCliAction(Func<InvocationContext, CancellationToken, Task> action)
=> _asyncAction = action ?? throw new ArgumentNullException(nameof(action));
internal AnonymousCliAction(Func<InvocationContext, CancellationToken, Task<int>> action)
=> _asyncAction = action;

public override int Invoke(InvocationContext context)
{
if (_syncAction is not null)
{
_syncAction(context);
return _syncAction(context);
}
else
{
SyncUsingAsync(context); // kept in a separate method to avoid JITting
return SyncUsingAsync(context); // kept in a separate method to avoid JITting
}

return 0;

void SyncUsingAsync(InvocationContext context)
int SyncUsingAsync(InvocationContext context)
=> _asyncAction!(context, CancellationToken.None).GetAwaiter().GetResult();
}

public async override Task<int> InvokeAsync(InvocationContext context, CancellationToken cancellationToken)
public override async Task<int> InvokeAsync(InvocationContext context, CancellationToken cancellationToken)
{
if (_asyncAction is not null)
{
await _asyncAction(context, cancellationToken);
return await _asyncAction(context, cancellationToken);
}
else
{
_syncAction!(context);
return _syncAction!(context);
}

return 0;
}
}
}