Skip to content

Commit 649ddfc

Browse files
add SetAction overloads returning int and Task<int> (#2121)
* add SetAction overloads returning int and Task<int> * respond to PR comments, improve XML docs --------- Co-authored-by: Adam Sitnik <[email protected]>
1 parent 4185c60 commit 649ddfc

File tree

4 files changed

+128
-33
lines changed

4 files changed

+128
-33
lines changed

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,9 @@ System.CommandLine
5454
public ParseResult Parse(System.Collections.Generic.IReadOnlyList<System.String> args, CommandLineConfiguration configuration = null)
5555
public ParseResult Parse(System.String commandLine, CommandLineConfiguration configuration = null)
5656
public System.Void SetAction(System.Action<System.CommandLine.Invocation.InvocationContext> action)
57+
public System.Void SetAction(System.Func<System.CommandLine.Invocation.InvocationContext,System.Int32> action)
5758
public System.Void SetAction(System.Func<System.CommandLine.Invocation.InvocationContext,System.Threading.CancellationToken,System.Threading.Tasks.Task> action)
59+
public System.Void SetAction(System.Func<System.CommandLine.Invocation.InvocationContext,System.Threading.CancellationToken,System.Threading.Tasks.Task<System.Int32>> action)
5860
public class CommandLineConfiguration
5961
.ctor(Command rootCommand)
6062
public System.Collections.Generic.List<Directive> Directives { get; }

src/System.CommandLine.Tests/Invocation/InvocationExtensionsTests.cs

Lines changed: 60 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ namespace System.CommandLine.Tests.Invocation
1414
public class InvocationExtensionsTests
1515
{
1616
[Fact]
17-
public async Task Command_InvokeAsync_uses_default_pipeline_by_default()
17+
public async Task Command_InvokeAsync_enables_help_by_default()
1818
{
1919
var command = new Command("the-command")
2020
{
@@ -31,14 +31,13 @@ public async Task Command_InvokeAsync_uses_default_pipeline_by_default()
3131

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

34-
output
35-
.ToString()
36-
.Should()
37-
.Contain(theHelpText);
34+
output.ToString()
35+
.Should()
36+
.Contain(theHelpText);
3837
}
3938

4039
[Fact]
41-
public void Command_Invoke_uses_default_pipeline_by_default()
40+
public void Command_Invoke_enables_help_by_default()
4241
{
4342
var command = new Command("the-command")
4443
{
@@ -55,10 +54,9 @@ public void Command_Invoke_uses_default_pipeline_by_default()
5554

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

58-
output
59-
.ToString()
60-
.Should()
61-
.Contain(theHelpText);
57+
output.ToString()
58+
.Should()
59+
.Contain(theHelpText);
6260
}
6361

6462
[Fact]
@@ -132,17 +130,66 @@ public void RootCommand_Invoke_returns_1_when_handler_throws()
132130
}
133131

134132
[Fact]
135-
public async Task RootCommand_Action_can_set_custom_result_code()
133+
public void Custom_RootCommand_Action_can_set_custom_result_code_via_Invoke()
136134
{
137-
var rootCommand = new RootCommand()
135+
var rootCommand = new RootCommand
138136
{
139137
Action = new CustomExitCodeAction()
140138
};
141139

142140
rootCommand.Parse("").Invoke().Should().Be(123);
141+
}
142+
143+
[Fact]
144+
public async Task Custom_RootCommand_Action_can_set_custom_result_code_via_InvokeAsync()
145+
{
146+
var rootCommand = new RootCommand
147+
{
148+
Action = new CustomExitCodeAction()
149+
};
150+
143151
(await rootCommand.Parse("").InvokeAsync()).Should().Be(456);
144152
}
145153

154+
[Fact]
155+
public void Anonymous_RootCommand_Task_returning_Action_can_set_custom_result_code_via_Invoke()
156+
{
157+
var rootCommand = new RootCommand();
158+
159+
rootCommand.SetAction((_, _) => Task.FromResult(123));
160+
161+
rootCommand.Parse("").Invoke().Should().Be(123);
162+
}
163+
164+
[Fact]
165+
public async Task Anonymous_RootCommand_Task_returning_Action_can_set_custom_result_code_via_InvokeAsync()
166+
{
167+
var rootCommand = new RootCommand();
168+
169+
rootCommand.SetAction((_, _) => Task.FromResult(123));
170+
171+
(await rootCommand.Parse("").InvokeAsync()).Should().Be(123);
172+
}
173+
[Fact]
174+
public void Anonymous_RootCommand_int_returning_Action_can_set_custom_result_code_via_Invoke()
175+
{
176+
var rootCommand = new RootCommand();
177+
178+
rootCommand.SetAction(_ => 123);
179+
180+
rootCommand.Parse("").Invoke().Should().Be(123);
181+
}
182+
183+
[Fact]
184+
public async Task Anonymous_RootCommand_int_returning_Action_can_set_custom_result_code_via_InvokeAsync()
185+
{
186+
var rootCommand = new RootCommand();
187+
188+
rootCommand.SetAction(_ => 123);
189+
190+
(await rootCommand.Parse("").InvokeAsync()).Should().Be(123);
191+
}
192+
146193
internal sealed class CustomExitCodeAction : CliAction
147194
{
148195
public override int Invoke(InvocationContext context)
@@ -157,7 +204,7 @@ public async Task Command_InvokeAsync_with_cancelation_token_invokes_command_han
157204
{
158205
using CancellationTokenSource cts = new();
159206
var command = new Command("test");
160-
command.SetAction((InvocationContext context, CancellationToken cancellationToken) =>
207+
command.SetAction((_, cancellationToken) =>
161208
{
162209
cancellationToken.Should().Be(cts.Token);
163210
return Task.CompletedTask;

src/System.CommandLine/Command.cs

Lines changed: 54 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -101,16 +101,66 @@ public IEnumerable<Symbol> Children
101101
public CliAction? Action { get; set; }
102102

103103
/// <summary>
104-
/// Sets a synchronous action.
104+
/// Sets a synchronous action to be run when the command is invoked.
105105
/// </summary>
106106
public void SetAction(Action<InvocationContext> action)
107-
=> Action = new AnonymousCliAction(action);
107+
{
108+
if (action is null)
109+
{
110+
throw new ArgumentNullException(nameof(action));
111+
}
112+
113+
Action = new AnonymousCliAction(context =>
114+
{
115+
action(context);
116+
return 0;
117+
});
118+
}
119+
120+
/// <summary>
121+
/// Sets a synchronous action to be run when the command is invoked.
122+
/// </summary>
123+
/// <remarks>The value returned from the <paramref name="action"/> delegate can be used to set the process exit code.</remarks>
124+
public void SetAction(Func<InvocationContext, int> action)
125+
{
126+
if (action is null)
127+
{
128+
throw new ArgumentNullException(nameof(action));
129+
}
130+
131+
Action = new AnonymousCliAction(action);
132+
}
108133

109134
/// <summary>
110-
/// Sets an asynchronous action.
135+
/// Sets an asynchronous action to be run when the command is invoked.
111136
/// </summary>
112137
public void SetAction(Func<InvocationContext, CancellationToken, Task> action)
113-
=> Action = new AnonymousCliAction(action);
138+
{
139+
if (action is null)
140+
{
141+
throw new ArgumentNullException(nameof(action));
142+
}
143+
144+
Action = new AnonymousCliAction(async (context, cancellationToken) =>
145+
{
146+
await action(context, cancellationToken);
147+
return 0;
148+
});
149+
}
150+
151+
/// <summary>
152+
/// Sets an asynchronous action when the command is invoked.
153+
/// </summary>
154+
/// <remarks>The value returned from the <paramref name="action"/> delegate can be used to set the process exit code.</remarks>
155+
public void SetAction(Func<InvocationContext, CancellationToken, Task<int>> action)
156+
{
157+
if (action is null)
158+
{
159+
throw new ArgumentNullException(nameof(action));
160+
}
161+
162+
Action = new AnonymousCliAction(action);
163+
}
114164

115165
/// <summary>
116166
/// Adds a <see cref="Symbol"/> to the command.

src/System.CommandLine/Invocation/AnonymousCliAction.cs

Lines changed: 12 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -8,44 +8,40 @@ namespace System.CommandLine.Invocation
88
{
99
internal sealed class AnonymousCliAction : CliAction
1010
{
11-
private readonly Func<InvocationContext, CancellationToken, Task>? _asyncAction;
12-
private readonly Action<InvocationContext>? _syncAction;
11+
private readonly Func<InvocationContext, CancellationToken, Task<int>>? _asyncAction;
12+
private readonly Func<InvocationContext, int>? _syncAction;
1313

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

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

2020
public override int Invoke(InvocationContext context)
2121
{
2222
if (_syncAction is not null)
2323
{
24-
_syncAction(context);
24+
return _syncAction(context);
2525
}
2626
else
2727
{
28-
SyncUsingAsync(context); // kept in a separate method to avoid JITting
28+
return SyncUsingAsync(context); // kept in a separate method to avoid JITting
2929
}
3030

31-
return 0;
32-
33-
void SyncUsingAsync(InvocationContext context)
31+
int SyncUsingAsync(InvocationContext context)
3432
=> _asyncAction!(context, CancellationToken.None).GetAwaiter().GetResult();
3533
}
3634

37-
public async override Task<int> InvokeAsync(InvocationContext context, CancellationToken cancellationToken)
35+
public override async Task<int> InvokeAsync(InvocationContext context, CancellationToken cancellationToken)
3836
{
3937
if (_asyncAction is not null)
4038
{
41-
await _asyncAction(context, cancellationToken);
39+
return await _asyncAction(context, cancellationToken);
4240
}
4341
else
4442
{
45-
_syncAction!(context);
43+
return _syncAction!(context);
4644
}
47-
48-
return 0;
4945
}
5046
}
5147
}

0 commit comments

Comments
 (0)