diff --git a/src/System.CommandLine.ApiCompatibility.Tests/ApiCompatibilityApprovalTests.System_CommandLine_api_is_not_changed.approved.txt b/src/System.CommandLine.ApiCompatibility.Tests/ApiCompatibilityApprovalTests.System_CommandLine_api_is_not_changed.approved.txt index a2fddaf5a3..44fca21c49 100644 --- a/src/System.CommandLine.ApiCompatibility.Tests/ApiCompatibilityApprovalTests.System_CommandLine_api_is_not_changed.approved.txt +++ b/src/System.CommandLine.ApiCompatibility.Tests/ApiCompatibilityApprovalTests.System_CommandLine_api_is_not_changed.approved.txt @@ -50,8 +50,8 @@ System.CommandLine public System.Collections.Generic.IEnumerator GetEnumerator() public ParseResult Parse(System.Collections.Generic.IReadOnlyList args, CommandLineConfiguration configuration = null) public ParseResult Parse(System.String commandLine, CommandLineConfiguration configuration = null) - public System.Void SetHandler(System.Action handle) - public System.Void SetHandler(System.Func handle) + public System.Void SetHandler(System.Func handler) + public System.Void SetHandler(System.Func> handler) public static class CommandExtensions public static System.Int32 Invoke(this Command command, System.String[] args, IConsole console = null) public static System.Int32 Invoke(this Command command, System.String commandLine, IConsole console = null) @@ -69,7 +69,7 @@ System.CommandLine public CommandLineBuilder RegisterWithDotnetSuggest() public CommandLineBuilder UseDefaults() public CommandLineBuilder UseEnvironmentVariableDirective() - public CommandLineBuilder UseExceptionHandler(System.Action onException = null, System.Nullable errorExitCode = null) + public CommandLineBuilder UseExceptionHandler(System.Func onException = null, System.Int32 errorExitCode = 1) public CommandLineBuilder UseHelp(System.Nullable maxWidth = null) public CommandLineBuilder UseHelp(System.String name, System.String[] helpAliases) public CommandLineBuilder UseHelp(System.Action customize, System.Nullable maxWidth = null) @@ -104,10 +104,10 @@ System.CommandLine public static System.Void Write(this IConsole console, System.String value) public static System.Void WriteLine(this IConsole console, System.String value) public class Directive : Symbol - .ctor(System.String name, System.Action syncHandler = null, System.Func asyncHandler = null) + .ctor(System.String name, System.Func syncHandler = null, System.Func> asyncHandler = null) public System.Collections.Generic.IEnumerable GetCompletions(System.CommandLine.Completions.CompletionContext context) - public System.Void SetAsynchronousHandler(System.Func handler) - public System.Void SetSynchronousHandler(System.Action handler) + public System.Void SetAsynchronousHandler(System.Func> handler) + public System.Void SetSynchronousHandler(System.Func handler) public class EnvironmentVariablesDirective : Directive .ctor() public interface ICommandHandler @@ -255,9 +255,7 @@ System.CommandLine.Invocation .ctor(System.CommandLine.ParseResult parseResult, System.CommandLine.IConsole console = null) public System.CommandLine.Binding.BindingContext BindingContext { get; } public System.CommandLine.IConsole Console { get; set; } - public System.Int32 ExitCode { get; set; } public System.CommandLine.Help.HelpBuilder HelpBuilder { get; } - public System.Action InvocationResult { get; set; } public System.CommandLine.ParseResult ParseResult { get; set; } public T GetValue(Option option) public T GetValue(Argument argument) diff --git a/src/System.CommandLine.Benchmarks/CommandLine/Perf_Parser_Simple.cs b/src/System.CommandLine.Benchmarks/CommandLine/Perf_Parser_Simple.cs index 9c0cb8bc83..d05fdc1065 100644 --- a/src/System.CommandLine.Benchmarks/CommandLine/Perf_Parser_Simple.cs +++ b/src/System.CommandLine.Benchmarks/CommandLine/Perf_Parser_Simple.cs @@ -37,6 +37,8 @@ private static RootCommand BuildCommand() { bool boolean = ctx.ParseResult.GetValue(boolOption); string text = ctx.ParseResult.GetValue(stringOption); + + return 0; }); return command; diff --git a/src/System.CommandLine.Generator/Invocations/ConstructorModelBindingInvocation.cs b/src/System.CommandLine.Generator/Invocations/ConstructorModelBindingInvocation.cs index 98ff286bbc..167b58046c 100644 --- a/src/System.CommandLine.Generator/Invocations/ConstructorModelBindingInvocation.cs +++ b/src/System.CommandLine.Generator/Invocations/ConstructorModelBindingInvocation.cs @@ -48,17 +48,17 @@ public override string InvokeContents() { case ReturnPattern.InvocationContextExitCode: builder.Append(@" - return await global::System.Threading.Tasks.Task.FromResult(context.ExitCode);"); + return 0;"); break; case ReturnPattern.FunctionReturnValue: builder.Append(@" - return await global::System.Threading.Tasks.Task.FromResult(rv);"); + return rv;"); break; case ReturnPattern.AwaitFunction: builder.Append(@" await rv;"); builder.Append(@" - return context.ExitCode;"); + return 0;"); break; case ReturnPattern.AwaitFunctionReturnValue: builder.Append(@" diff --git a/src/System.CommandLine.Generator/Invocations/DelegateInvocation.cs b/src/System.CommandLine.Generator/Invocations/DelegateInvocation.cs index 00cc083026..075d1b2688 100644 --- a/src/System.CommandLine.Generator/Invocations/DelegateInvocation.cs +++ b/src/System.CommandLine.Generator/Invocations/DelegateInvocation.cs @@ -50,17 +50,17 @@ public virtual string InvokeContents() { case ReturnPattern.InvocationContextExitCode: builder.Append(@" - return await Task.FromResult(context.ExitCode);"); + return 0;"); break; case ReturnPattern.FunctionReturnValue: builder.Append(@" - return await Task.FromResult(rv);"); + return rv;"); break; case ReturnPattern.AwaitFunction: builder.Append(@" await rv;"); builder.Append(@" - return context.ExitCode;"); + return 0;"); break; case ReturnPattern.AwaitFunctionReturnValue: builder.Append(@" diff --git a/src/System.CommandLine.Hosting/HostingExtensions.cs b/src/System.CommandLine.Hosting/HostingExtensions.cs index 93573e7332..df96dd1db7 100644 --- a/src/System.CommandLine.Hosting/HostingExtensions.cs +++ b/src/System.CommandLine.Hosting/HostingExtensions.cs @@ -36,7 +36,6 @@ public static CommandLineBuilder UseHost(this CommandLineBuilder builder, services.AddSingleton(invocation); services.AddSingleton(invocation.BindingContext); services.AddSingleton(invocation.Console); - services.AddTransient(_ => invocation.InvocationResult); services.AddTransient(_ => invocation.ParseResult); }); hostBuilder.UseInvocationLifetime(invocation); diff --git a/src/System.CommandLine.NamingConventionBinder.Tests/ModelBindingCommandHandlerTests.cs b/src/System.CommandLine.NamingConventionBinder.Tests/ModelBindingCommandHandlerTests.cs index 016aa1c0ad..d2f2a3100d 100644 --- a/src/System.CommandLine.NamingConventionBinder.Tests/ModelBindingCommandHandlerTests.cs +++ b/src/System.CommandLine.NamingConventionBinder.Tests/ModelBindingCommandHandlerTests.cs @@ -321,16 +321,12 @@ public async Task Unexpected_return_types_result_in_exit_code_0_if_no_exception_ private static void CaptureMethod(T value, InvocationContext invocationContext) { BoundValueCapturer.Capture(value, invocationContext); - - invocationContext.InvocationResult = ctx => BoundValueCapturer.Apply(ctx); } private static Action CaptureDelegate() => (value, invocationContext) => { BoundValueCapturer.Capture(value, invocationContext); - - invocationContext.InvocationResult = ctx => BoundValueCapturer.Apply(ctx); }; private static class BoundValueCapturer diff --git a/src/System.CommandLine.NamingConventionBinder/CommandHandler.cs b/src/System.CommandLine.NamingConventionBinder/CommandHandler.cs index dbcf4daed9..2e1f70f26d 100644 --- a/src/System.CommandLine.NamingConventionBinder/CommandHandler.cs +++ b/src/System.CommandLine.NamingConventionBinder/CommandHandler.cs @@ -611,13 +611,10 @@ internal static async Task GetExitCodeAsync(object? returnValue, Invocation { case Task exitCodeTask: return await exitCodeTask; - case Task task: - await task; - return context.ExitCode; case int exitCode: return exitCode; default: - return context.ExitCode; + return 0; } } } \ No newline at end of file diff --git a/src/System.CommandLine.Suggest.Tests/EndToEndTestApp/Program.cs b/src/System.CommandLine.Suggest.Tests/EndToEndTestApp/Program.cs index 85626fc381..e72dcfb121 100644 --- a/src/System.CommandLine.Suggest.Tests/EndToEndTestApp/Program.cs +++ b/src/System.CommandLine.Suggest.Tests/EndToEndTestApp/Program.cs @@ -30,7 +30,7 @@ static async Task Main(string[] args) string cherry = ctx.ParseResult.GetValue(cherryOption); string durian = ctx.ParseResult.GetValue(durianOption); - return Task.CompletedTask; + return Task.FromResult(0); }); var commandLine = new CommandLineBuilder(rootCommand) diff --git a/src/System.CommandLine.Suggest/SuggestionDispatcher.cs b/src/System.CommandLine.Suggest/SuggestionDispatcher.cs index 228eb06f7e..e3fd8b7b50 100644 --- a/src/System.CommandLine.Suggest/SuggestionDispatcher.cs +++ b/src/System.CommandLine.Suggest/SuggestionDispatcher.cs @@ -7,6 +7,7 @@ using System.CommandLine.Parsing; using System.IO; using System.Linq; +using System.Threading; using System.Threading.Tasks; namespace System.CommandLine.Suggest @@ -31,6 +32,7 @@ public SuggestionDispatcher(ISuggestionRegistration suggestionRegistration, ISug CompleteScriptCommand.SetHandler(context => { SuggestionShellScriptHandler.Handle(context.Console, context.ParseResult.GetValue(shellTypeArgument)); + return 0; }); ListCommand = new Command("list") @@ -48,7 +50,7 @@ public SuggestionDispatcher(ISuggestionRegistration suggestionRegistration, ISug ExecutableOption, PositionOption }; - GetCommand.SetHandler(context => Get(context)); + GetCommand.SetHandler(Get); var commandPathOption = new Option("--command-path") { Description = "The path to the command for which to register suggestions" }; @@ -132,7 +134,7 @@ private void Register( } } - private Task Get(InvocationContext context) + private Task Get(InvocationContext context, CancellationToken cancellationToken) { var parseResult = context.ParseResult; var commandPath = parseResult.GetValue(ExecutableOption); diff --git a/src/System.CommandLine.Tests/ArgumentTests.cs b/src/System.CommandLine.Tests/ArgumentTests.cs index 35a227c64d..c87fff641d 100644 --- a/src/System.CommandLine.Tests/ArgumentTests.cs +++ b/src/System.CommandLine.Tests/ArgumentTests.cs @@ -378,7 +378,7 @@ public async Task Custom_argument_parser_is_only_called_once() }; var command = new RootCommand(); - command.SetHandler((ctx) => handlerWasCalled = true); + command.SetHandler((ctx) => { handlerWasCalled = true; return 0; }); command.Options.Add(option); await command.InvokeAsync("--value 42"); diff --git a/src/System.CommandLine.Tests/Binding/SetHandlerTests.cs b/src/System.CommandLine.Tests/Binding/SetHandlerTests.cs index ed9ef18e5a..26099f3a88 100644 --- a/src/System.CommandLine.Tests/Binding/SetHandlerTests.cs +++ b/src/System.CommandLine.Tests/Binding/SetHandlerTests.cs @@ -16,26 +16,6 @@ namespace System.CommandLine.Tests.Binding { public class SetHandlerTests { - [Fact] - public async Task Unexpected_return_types_result_in_exit_code_0_if_no_exception_was_thrown() - { - var wasCalled = false; - - var command = new Command("wat"); - - var handle = (InvocationContext ctx, CancellationToken cancellationToken) => - { - wasCalled = true; - return Task.FromResult(new { NovelType = true }); - }; - - command.SetHandler(handle); - - var exitCode = await command.InvokeAsync(""); - wasCalled.Should().BeTrue(); - exitCode.Should().Be(0); - } - [Fact] public async Task When_User_Requests_Cancellation_Its_Reflected_By_The_Token_Passed_To_Handler() { @@ -47,11 +27,11 @@ public async Task When_User_Requests_Cancellation_Its_Reflected_By_The_Token_Pas try { await Task.Delay(Timeout.InfiniteTimeSpan, cancellationToken); - context.ExitCode = ExpectedExitCode * -1; + return ExpectedExitCode * -1; } catch (OperationCanceledException) { - context.ExitCode = ExpectedExitCode; + return ExpectedExitCode; } }); diff --git a/src/System.CommandLine.Tests/EnvironmentVariableDirectiveTests.cs b/src/System.CommandLine.Tests/EnvironmentVariableDirectiveTests.cs index b66b53dabe..d4fabcba4f 100644 --- a/src/System.CommandLine.Tests/EnvironmentVariableDirectiveTests.cs +++ b/src/System.CommandLine.Tests/EnvironmentVariableDirectiveTests.cs @@ -23,6 +23,7 @@ public async Task Sets_environment_variable_to_value() { asserted = true; Environment.GetEnvironmentVariable(variable).Should().Be(value); + return 0; }); var config = new CommandLineBuilder(rootCommand) @@ -45,6 +46,7 @@ public async Task Trims_environment_variable_name() { asserted = true; Environment.GetEnvironmentVariable(variable).Should().Be(value); + return 0; }); var config = new CommandLineBuilder(rootCommand) @@ -67,6 +69,7 @@ public async Task Trims_environment_variable_value() { asserted = true; Environment.GetEnvironmentVariable(variable).Should().Be(value); + return 0; }); var config = new CommandLineBuilder(rootCommand) @@ -89,6 +92,7 @@ public async Task Sets_environment_variable_value_containing_equals_sign() { asserted = true; Environment.GetEnvironmentVariable(variable).Should().Be(value); + return 0; }); var config = new CommandLineBuilder(rootCommand) @@ -110,6 +114,7 @@ public async Task Ignores_environment_directive_without_equals_sign() { asserted = true; Environment.GetEnvironmentVariable(variable).Should().BeNull(); + return 0; }); var config = new CommandLineBuilder(rootCommand) @@ -132,6 +137,7 @@ public static async Task Ignores_environment_directive_with_empty_variable_name( asserted = true; var env = Environment.GetEnvironmentVariables(); env.Values.Cast().Should().NotContain(value); + return 0; }); var config = new CommandLineBuilder(rootCommand) @@ -154,6 +160,7 @@ public static async Task Ignores_environment_directive_with_whitespace_variable_ asserted = true; var env = Environment.GetEnvironmentVariables(); env.Values.Cast().Should().NotContain(value); + return 0; }); var config = new CommandLineBuilder(rootCommand) diff --git a/src/System.CommandLine.Tests/GlobalOptionTests.cs b/src/System.CommandLine.Tests/GlobalOptionTests.cs index 63e068b72c..7a91a03ae8 100644 --- a/src/System.CommandLine.Tests/GlobalOptionTests.cs +++ b/src/System.CommandLine.Tests/GlobalOptionTests.cs @@ -29,7 +29,7 @@ public void When_a_required_global_option_is_omitted_it_results_in_an_error() { var command = new Command("child"); var rootCommand = new RootCommand { command }; - command.SetHandler((_) => { }); + command.SetHandler((_) => 0); var requiredOption = new Option("--i-must-be-set") { IsRequired = true, @@ -69,7 +69,7 @@ public void When_a_required_global_option_is_present_on_child_of_command_it_was_ { var command = new Command("child"); var rootCommand = new RootCommand { command }; - command.SetHandler((_) => { }); + command.SetHandler((_) => 0); var requiredOption = new Option("--i-must-be-set") { IsRequired = true, diff --git a/src/System.CommandLine.Tests/Invocation/CancelOnProcessTerminationTests.cs b/src/System.CommandLine.Tests/Invocation/CancelOnProcessTerminationTests.cs index 6e62a9c432..35515b04d5 100644 --- a/src/System.CommandLine.Tests/Invocation/CancelOnProcessTerminationTests.cs +++ b/src/System.CommandLine.Tests/Invocation/CancelOnProcessTerminationTests.cs @@ -66,10 +66,12 @@ private static Task Program(string[] args) // and reason why we need a timeout on termination processing. CancellationToken token = infiniteDelay ? CancellationToken.None : cancellationToken; await Task.Delay(Timeout.InfiniteTimeSpan, token); + + return 0; } catch (OperationCanceledException) { - context.ExitCode = GracefulExitCode; + return GracefulExitCode; } }); diff --git a/src/System.CommandLine.Tests/Invocation/InvocationExtensionsTests.cs b/src/System.CommandLine.Tests/Invocation/InvocationExtensionsTests.cs index c1f315b753..7a4463ce47 100644 --- a/src/System.CommandLine.Tests/Invocation/InvocationExtensionsTests.cs +++ b/src/System.CommandLine.Tests/Invocation/InvocationExtensionsTests.cs @@ -52,7 +52,7 @@ public async Task RootCommand_InvokeAsync_returns_0_when_handler_is_successful() var wasCalled = false; var rootCommand = new RootCommand(); - rootCommand.SetHandler((_) => wasCalled = true); + rootCommand.SetHandler((_) => { wasCalled = true; return 0; }); var result = await rootCommand.InvokeAsync(""); @@ -66,7 +66,7 @@ public void RootCommand_Invoke_returns_0_when_handler_is_successful() var wasCalled = false; var rootCommand = new RootCommand(); - rootCommand.SetHandler((_) => wasCalled = true); + rootCommand.SetHandler((_) => { wasCalled = true; return 0; }); int result = rootCommand.Invoke(""); @@ -127,8 +127,7 @@ public async Task RootCommand_InvokeAsync_can_set_custom_result_code() rootCommand.SetHandler((context, cancellationToken) => { - context.ExitCode = 123; - return Task.CompletedTask; + return Task.FromResult(123); }); var resultCode = await rootCommand.InvokeAsync(""); @@ -143,8 +142,7 @@ public void RootCommand_Invoke_can_set_custom_result_code() rootCommand.SetHandler((context, cancellationToken) => { - context.ExitCode = 123; - return Task.CompletedTask; + return Task.FromResult(123); }); int resultCode = rootCommand.Invoke(""); diff --git a/src/System.CommandLine.Tests/Invocation/InvocationPipelineTests.cs b/src/System.CommandLine.Tests/Invocation/InvocationPipelineTests.cs index 1076923f85..d556df1a6f 100644 --- a/src/System.CommandLine.Tests/Invocation/InvocationPipelineTests.cs +++ b/src/System.CommandLine.Tests/Invocation/InvocationPipelineTests.cs @@ -39,10 +39,10 @@ public async Task InvokeAsync_chooses_the_appropriate_command() var secondWasCalled = false; var first = new Command("first"); - first.SetHandler((_) => firstWasCalled = true); + first.SetHandler((_) => { firstWasCalled = true; return 0; }); var second = new Command("second"); - second.SetHandler((_) => secondWasCalled = true); + second.SetHandler((_) => { secondWasCalled = true; return 0; }); var config = new CommandLineBuilder(new RootCommand { @@ -64,10 +64,10 @@ public void Invoke_chooses_the_appropriate_command() var secondWasCalled = false; var first = new Command("first"); - first.SetHandler((_) => firstWasCalled = true); + first.SetHandler((_) => { firstWasCalled = true; return 0; }); var second = new Command("second"); - second.SetHandler((_) => secondWasCalled = true); + second.SetHandler((_) => { secondWasCalled = true; return 0; }); var config = new CommandLineBuilder(new RootCommand { @@ -350,7 +350,7 @@ public async Task Middleware_can_throw_OperationCancelledException() { throw new OperationCanceledException(cancellationToken); }) - .UseExceptionHandler((ex, ctx) => ctx.ExitCode = ex is OperationCanceledException ? 123 : 456) + .UseExceptionHandler((ex, ctx) => ex is OperationCanceledException ? 123 : 456) .Build(); int result = await config.InvokeAsync("the-command"); diff --git a/src/System.CommandLine.Tests/ParserTests.MultiplePositions.cs b/src/System.CommandLine.Tests/ParserTests.MultiplePositions.cs index 77b7266a2d..4d3165d89d 100644 --- a/src/System.CommandLine.Tests/ParserTests.MultiplePositions.cs +++ b/src/System.CommandLine.Tests/ParserTests.MultiplePositions.cs @@ -118,7 +118,7 @@ public void A_command_can_be_specified_in_more_than_one_position( string expectedParent) { var reusedCommand = new Command("reused"); - reusedCommand.SetHandler((_) => { }); + reusedCommand.SetHandler((_) => 0); reusedCommand.Add(new Option("--the-option")); var outer = new Command("outer") diff --git a/src/System.CommandLine.Tests/ParsingValidationTests.cs b/src/System.CommandLine.Tests/ParsingValidationTests.cs index c773d772ec..c975e4ae51 100644 --- a/src/System.CommandLine.Tests/ParsingValidationTests.cs +++ b/src/System.CommandLine.Tests/ParsingValidationTests.cs @@ -514,9 +514,9 @@ public async Task A_custom_validator_added_to_a_global_option_is_checked(string rootCommand.Options.Add(globalOption); - rootCommand.SetHandler((ctx) => handlerWasCalled = true); - childCommand.SetHandler((ctx) => handlerWasCalled = true); - grandchildCommand.SetHandler((ctx) => handlerWasCalled = true); + rootCommand.SetHandler((ctx) => { handlerWasCalled = true; return 0; }); + childCommand.SetHandler((ctx) => { handlerWasCalled = true; return 0; }); + grandchildCommand.SetHandler((ctx) => { handlerWasCalled = true; return 0; }); var result = await rootCommand.InvokeAsync(commandLine); @@ -1145,7 +1145,7 @@ public void A_command_with_subcommands_is_valid_to_invoke_if_it_has_a_handler() { var outer = new Command("outer"); var inner = new Command("inner"); - inner.SetHandler((_) => { }); + inner.SetHandler((_) => 0); var innerer = new Command("inner-er"); outer.Subcommands.Add(inner); inner.Subcommands.Add(innerer); diff --git a/src/System.CommandLine.Tests/TestApps/NativeAOT/Program.cs b/src/System.CommandLine.Tests/TestApps/NativeAOT/Program.cs index 548fbab58b..1b8cf071d2 100644 --- a/src/System.CommandLine.Tests/TestApps/NativeAOT/Program.cs +++ b/src/System.CommandLine.Tests/TestApps/NativeAOT/Program.cs @@ -20,10 +20,12 @@ private static int Main(string[] args) return new CommandLineBuilder(command).Build().Invoke(args); - void Run(InvocationContext context) + int Run(InvocationContext context) { context.Console.WriteLine($"Bool option: {context.ParseResult.GetValue(boolOption)}"); context.Console.WriteLine($"String option: {context.ParseResult.GetValue(stringOption)}"); + + return 0; } } } \ No newline at end of file diff --git a/src/System.CommandLine.Tests/TestApps/Trimming/Program.cs b/src/System.CommandLine.Tests/TestApps/Trimming/Program.cs index f01ec4a7d4..2a13fcd30a 100644 --- a/src/System.CommandLine.Tests/TestApps/Trimming/Program.cs +++ b/src/System.CommandLine.Tests/TestApps/Trimming/Program.cs @@ -12,6 +12,8 @@ command.SetHandler(context => { context.Console.Write($"The file you chose was: {context.ParseResult.GetValue(fileArgument)}"); + + return 0; }); command.Invoke(args); diff --git a/src/System.CommandLine.Tests/UseExceptionHandlerTests.cs b/src/System.CommandLine.Tests/UseExceptionHandlerTests.cs index 744185c856..033a332f13 100644 --- a/src/System.CommandLine.Tests/UseExceptionHandlerTests.cs +++ b/src/System.CommandLine.Tests/UseExceptionHandlerTests.cs @@ -53,14 +53,7 @@ public async Task UseExceptionHandler_catches_middleware_exceptions_and_writes_d public async Task UseExceptionHandler_catches_command_handler_exceptions_and_sets_result_code_to_1() { var command = new Command("the-command"); - command.SetHandler((_, __) => - { - throw new Exception("oops!"); - // Help the compiler pick a CommandHandler.Create overload. -#pragma warning disable CS0162 // Unreachable code detected - return Task.FromResult(0); -#pragma warning restore CS0162 - }); + command.SetHandler((_, __) => Task.FromException(new Exception("oops!"))); var config = new CommandLineBuilder(new RootCommand { @@ -78,14 +71,7 @@ public async Task UseExceptionHandler_catches_command_handler_exceptions_and_set public async Task UseExceptionHandler_catches_command_handler_exceptions_and_writes_details_to_standard_error() { var command = new Command("the-command"); - command.SetHandler((_, __) => - { - throw new Exception("oops!"); - // Help the compiler pick a CommandHandler.Create overload. -#pragma warning disable CS0162 // Unreachable code detected - return Task.FromResult(0); -#pragma warning restore CS0162 - }); + command.SetHandler((_, __) => Task.FromException(new Exception("oops!"))); var config = new CommandLineBuilder(new RootCommand { @@ -143,7 +129,7 @@ public async Task UseExceptionHandler_output_can_be_customized() .UseExceptionHandler((exception, context) => { context.Console.Out.Write("Well that's awkward."); - context.ExitCode = 22; + return 22; }) .AddMiddleware(_ => throw new Exception("oops!")) .Build() diff --git a/src/System.CommandLine.Tests/UseHelpTests.cs b/src/System.CommandLine.Tests/UseHelpTests.cs index 353770b564..a5635ec88b 100644 --- a/src/System.CommandLine.Tests/UseHelpTests.cs +++ b/src/System.CommandLine.Tests/UseHelpTests.cs @@ -46,7 +46,7 @@ public async Task UseHelp_interrupts_execution_of_the_specified_command() var wasCalled = false; var command = new Command("command"); var subcommand = new Command("subcommand"); - subcommand.SetHandler((_) => wasCalled = true); + subcommand.SetHandler((_) => { wasCalled = true; return 0; }); command.Subcommands.Add(subcommand); var config = diff --git a/src/System.CommandLine.Tests/VersionOptionTests.cs b/src/System.CommandLine.Tests/VersionOptionTests.cs index 9624286caa..cbd4e4912f 100644 --- a/src/System.CommandLine.Tests/VersionOptionTests.cs +++ b/src/System.CommandLine.Tests/VersionOptionTests.cs @@ -37,7 +37,7 @@ public async Task When_the_version_option_is_specified_then_invocation_is_short_ { var wasCalled = false; var rootCommand = new RootCommand(); - rootCommand.SetHandler((_) => wasCalled = true); + rootCommand.SetHandler((_) => { wasCalled = true; return 0; }); var config = new CommandLineBuilder(rootCommand) .UseVersionOption() @@ -78,7 +78,7 @@ public async Task When_the_version_option_is_specified_and_there_are_default_opt DefaultValueFactory = (_) => true } }; - rootCommand.SetHandler((_) => { }); + rootCommand.SetHandler((_) => 0); var configuration = new CommandLineBuilder(rootCommand) .UseVersionOption() @@ -98,7 +98,7 @@ public async Task When_the_version_option_is_specified_and_there_are_default_arg { new Argument("x") { DefaultValueFactory =(_) => true } }; - rootCommand.SetHandler((_) => { }); + rootCommand.SetHandler((_) => 0); var configuration = new CommandLineBuilder(rootCommand) .UseVersionOption() @@ -117,13 +117,13 @@ public async Task When_the_version_option_is_specified_and_there_are_default_arg public void Version_is_not_valid_with_other_tokens(string commandLine) { var subcommand = new Command("subcommand"); - subcommand.SetHandler((_) => { }); + subcommand.SetHandler((_) => 0); var rootCommand = new RootCommand { subcommand, new Option("-x") }; - rootCommand.SetHandler((_) => { }); + rootCommand.SetHandler((_) => 0); var configuration = new CommandLineBuilder(rootCommand) .UseVersionOption() @@ -138,13 +138,13 @@ public void Version_is_not_valid_with_other_tokens(string commandLine) public void Version_option_is_not_added_to_subcommands() { var childCommand = new Command("subcommand"); - childCommand.SetHandler((_) => { }); + childCommand.SetHandler((_) => 0); var rootCommand = new RootCommand { childCommand, }; - rootCommand.SetHandler((_) => { }); + rootCommand.SetHandler((_) => 0); var configuration = new CommandLineBuilder(rootCommand) .UseVersionOption() @@ -198,12 +198,12 @@ public async Task Version_can_specify_additional_alias() public void Version_is_not_valid_with_other_tokens_uses_custom_alias() { var childCommand = new Command("subcommand"); - childCommand.SetHandler((_) => { }); + childCommand.SetHandler((_) => 0); var rootCommand = new RootCommand { childCommand }; - rootCommand.SetHandler((_) => { }); + rootCommand.SetHandler((_) => 0); var configuration = new CommandLineBuilder(rootCommand) .UseVersionOption("-v") diff --git a/src/System.CommandLine/Builder/CommandLineBuilder.cs b/src/System.CommandLine/Builder/CommandLineBuilder.cs index 1941badfce..d5375f95a9 100644 --- a/src/System.CommandLine/Builder/CommandLineBuilder.cs +++ b/src/System.CommandLine/Builder/CommandLineBuilder.cs @@ -27,7 +27,7 @@ public partial class CommandLineBuilder internal int MaxLevenshteinDistance; /// - internal Action? ExceptionHandler; + internal Func? ExceptionHandler; internal TimeSpan? ProcessTerminationTimeout; diff --git a/src/System.CommandLine/Builder/CommandLineBuilderExtensions.cs b/src/System.CommandLine/Builder/CommandLineBuilderExtensions.cs index 211a97d09d..feb2ca610d 100644 --- a/src/System.CommandLine/Builder/CommandLineBuilderExtensions.cs +++ b/src/System.CommandLine/Builder/CommandLineBuilderExtensions.cs @@ -158,18 +158,17 @@ public CommandLineBuilder UseDefaults() /// /// Enables an exception handler to catch any unhandled exceptions thrown by a command handler during invocation. /// - /// A delegate that will be called when an exception is thrown by a command handler. + /// A delegate that will be called when an exception is thrown by a command handler. + /// It needs to return an exit code to be used when an exception is thrown. /// The exit code to be used when an exception is thrown. /// The reference to this instance. - public CommandLineBuilder UseExceptionHandler( - Action? onException = null, - int? errorExitCode = null) + public CommandLineBuilder UseExceptionHandler(Func? onException = null, int errorExitCode = 1) { ExceptionHandler = onException ?? Default; return this; - void Default(Exception exception, InvocationContext context) + int Default(Exception exception, InvocationContext context) { if (exception is not OperationCanceledException) { @@ -181,7 +180,7 @@ void Default(Exception exception, InvocationContext context) context.Console.ResetTerminalForegroundColor(); } - context.ExitCode = errorExitCode ?? 1; + return errorExitCode; } } diff --git a/src/System.CommandLine/Command.cs b/src/System.CommandLine/Command.cs index e95b9af290..1f7359e6dc 100644 --- a/src/System.CommandLine/Command.cs +++ b/src/System.CommandLine/Command.cs @@ -126,7 +126,7 @@ public void Add(Symbol symbol) /// that will be performed when the command is invoked. /// /// - /// Use one of the overloads to construct a handler. + /// Use one of the overloads to construct a handler. /// If the handler is not specified, parser errors will be generated for command line input that /// invokes this command. public ICommandHandler? Handler { get; set; } @@ -255,16 +255,16 @@ void AddCompletionsFor(Symbol identifier, AliasSet? aliases) } /// - /// Sets a command's handler based on an . + /// Sets a synchronous command handler. The handler should return an exit code. /// - public void SetHandler(Action handle) - => Handler = new AnonymousCommandHandler(handle); + public void SetHandler(Func handler) + => Handler = new AnonymousCommandHandler(handler); /// - /// Sets a command's handler based on a . + /// Sets an asynchronous command handler. The handler should return an exit code. /// - public void SetHandler(Func handle) - => Handler = new AnonymousCommandHandler(handle); + public void SetHandler(Func> handler) + => Handler = new AnonymousCommandHandler(handler); internal bool EqualsNameOrAlias(string name) => Name.Equals(name, StringComparison.Ordinal) || (_aliases is not null && _aliases.Contains(name)); diff --git a/src/System.CommandLine/CommandLineConfiguration.cs b/src/System.CommandLine/CommandLineConfiguration.cs index efd791aa35..3a1091163d 100644 --- a/src/System.CommandLine/CommandLineConfiguration.cs +++ b/src/System.CommandLine/CommandLineConfiguration.cs @@ -21,7 +21,7 @@ public class CommandLineConfiguration /// /// A delegate that will be called when an exception is thrown by a command handler. /// - internal readonly Action? ExceptionHandler; + internal readonly Func? ExceptionHandler; /// /// The exit code to use when parser errors occur. @@ -82,7 +82,7 @@ internal CommandLineConfiguration( IReadOnlyList? middlewarePipeline, Func? helpBuilderFactory, TryReplaceToken? tokenReplacer, - Action? exceptionHandler) + Func? exceptionHandler) { RootCommand = command ?? throw new ArgumentNullException(nameof(command)); Directives = directives is not null ? directives : Array.Empty(); diff --git a/src/System.CommandLine/Directive.cs b/src/System.CommandLine/Directive.cs index e299058ad8..c32fbf7d6a 100644 --- a/src/System.CommandLine/Directive.cs +++ b/src/System.CommandLine/Directive.cs @@ -24,8 +24,8 @@ public class Directive : Symbol /// The synchronous action that is invoked when directive is parsed. /// The asynchronous action that is invoked when directive is parsed. public Directive(string name, - Action? syncHandler = null, - Func? asyncHandler = null) + Func? syncHandler = null, + Func>? asyncHandler = null) : base(name) { if (syncHandler is not null) @@ -38,7 +38,7 @@ public Directive(string name, } } - public void SetAsynchronousHandler(Func handler) + public void SetAsynchronousHandler(Func> handler) { if (handler is null) { @@ -48,7 +48,7 @@ public void SetAsynchronousHandler(Func handler) + public void SetSynchronousHandler(Func handler) { if (handler is null) { diff --git a/src/System.CommandLine/EnvironmentVariablesDirective.cs b/src/System.CommandLine/EnvironmentVariablesDirective.cs index 6f65d52c87..43a74cbf49 100644 --- a/src/System.CommandLine/EnvironmentVariablesDirective.cs +++ b/src/System.CommandLine/EnvironmentVariablesDirective.cs @@ -13,7 +13,7 @@ public EnvironmentVariablesDirective() : base("env") SetSynchronousHandler(SyncHandler); } - private void SyncHandler(InvocationContext context) + private int SyncHandler(InvocationContext context) { DirectiveResult directiveResult = context.ParseResult.FindResultFor(this)!; @@ -37,7 +37,7 @@ private void SyncHandler(InvocationContext context) } // we need a cleaner, more flexible and intuitive way of continuing the execution - context.ParseResult.CommandResult.Command.Handler?.Invoke(context); + return context.ParseResult.CommandResult.Command.Handler?.Invoke(context) ?? 0; } } } diff --git a/src/System.CommandLine/Help/HelpOption.cs b/src/System.CommandLine/Help/HelpOption.cs index 094ce1f2e3..2ba88cc0d3 100644 --- a/src/System.CommandLine/Help/HelpOption.cs +++ b/src/System.CommandLine/Help/HelpOption.cs @@ -25,7 +25,7 @@ internal HelpOption() : this("--help", new[] { "-h", "/h", "-?", "/?" }) public override int GetHashCode() => typeof(HelpOption).GetHashCode(); - internal static void Handler(InvocationContext context) + internal static int Handler(InvocationContext context) { var output = context.Console.Out.CreateTextWriter(); @@ -35,6 +35,8 @@ internal static void Handler(InvocationContext context) context.ParseResult); context.HelpBuilder.Write(helpContext); + + return 0; } } } \ No newline at end of file diff --git a/src/System.CommandLine/Help/VersionOption.cs b/src/System.CommandLine/Help/VersionOption.cs index 25d1d2fc3e..9410ab2d82 100644 --- a/src/System.CommandLine/Help/VersionOption.cs +++ b/src/System.CommandLine/Help/VersionOption.cs @@ -53,7 +53,10 @@ private static bool IsNotImplicit(SymbolResult symbolResult) public override int GetHashCode() => typeof(VersionOption).GetHashCode(); - internal static void Handler(InvocationContext context) - => context.Console.Out.WriteLine(RootCommand.ExecutableVersion); + internal static int Handler(InvocationContext context) + { + context.Console.Out.WriteLine(RootCommand.ExecutableVersion); + return 0; + } } } \ No newline at end of file diff --git a/src/System.CommandLine/Invocation/AnonymousCommandHandler.cs b/src/System.CommandLine/Invocation/AnonymousCommandHandler.cs index 0a941e3948..4925840a6a 100644 --- a/src/System.CommandLine/Invocation/AnonymousCommandHandler.cs +++ b/src/System.CommandLine/Invocation/AnonymousCommandHandler.cs @@ -8,21 +8,20 @@ namespace System.CommandLine.Invocation { internal sealed class AnonymousCommandHandler : ICommandHandler { - private readonly Func? _asyncHandle; - private readonly Action? _syncHandle; + private readonly Func>? _asyncHandle; + private readonly Func? _syncHandle; - internal AnonymousCommandHandler(Func handle) + internal AnonymousCommandHandler(Func> handle) => _asyncHandle = handle ?? throw new ArgumentNullException(nameof(handle)); - internal AnonymousCommandHandler(Action handle) + internal AnonymousCommandHandler(Func handle) => _syncHandle = handle ?? throw new ArgumentNullException(nameof(handle)); public int Invoke(InvocationContext context) { if (_syncHandle is not null) { - _syncHandle(context); - return context.ExitCode; + return _syncHandle(context); } return SyncUsingAsync(context); // kept in a separate method to avoid JITting @@ -31,21 +30,9 @@ int SyncUsingAsync(InvocationContext context) => InvokeAsync(context, CancellationToken.None).GetAwaiter().GetResult(); } - public async Task InvokeAsync(InvocationContext context, CancellationToken cancellationToken) - { - if (_syncHandle is not null) - { - return Invoke(context); - } - - Task handler = _asyncHandle!(context, cancellationToken); - if (handler is Task intReturning) - { - return await intReturning; - } - - await handler; - return context.ExitCode; - } + public Task InvokeAsync(InvocationContext context, CancellationToken cancellationToken) + => _asyncHandle is not null + ? _asyncHandle(context, cancellationToken) + : Task.FromResult(Invoke(context)); } } \ No newline at end of file diff --git a/src/System.CommandLine/Invocation/InvocationContext.cs b/src/System.CommandLine/Invocation/InvocationContext.cs index 03de4d652f..a1fcd2f02d 100644 --- a/src/System.CommandLine/Invocation/InvocationContext.cs +++ b/src/System.CommandLine/Invocation/InvocationContext.cs @@ -49,17 +49,6 @@ public IConsole Console /// public ParseResult ParseResult { get; set; } - /// - /// A value that can be used to set the exit code for the process. - /// - public int ExitCode { get; set; } - - /// - /// The result of the current invocation. - /// - /// As the is passed through the invocation pipeline to the associated with the invoked command, only the last value of this property will be the one applied. - public Action? InvocationResult { get; set; } - /// public T? GetValue(Option option) => ParseResult.GetValue(option); diff --git a/src/System.CommandLine/Invocation/InvocationPipeline.cs b/src/System.CommandLine/Invocation/InvocationPipeline.cs index 9e609f3bb9..ad3ced430a 100644 --- a/src/System.CommandLine/Invocation/InvocationPipeline.cs +++ b/src/System.CommandLine/Invocation/InvocationPipeline.cs @@ -1,7 +1,6 @@ // Copyright (c) .NET Foundation and contributors. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. -using System.Collections.Generic; using System.Linq; using System.Threading; using System.Threading.Tasks; @@ -12,20 +11,24 @@ internal static class InvocationPipeline { internal static async Task InvokeAsync(ParseResult parseResult, IConsole? console, CancellationToken cancellationToken) { - InvocationContext context = new (parseResult, console); + if (parseResult.Handler is null && parseResult.Configuration.Middleware.Count == 0) + { + return 0; + } + InvocationContext context = new (parseResult, console); + ProcessTerminationHandler? terminationHandler = null; using CancellationTokenSource cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); - Task startedInvocation = parseResult.Handler is not null && parseResult.Configuration.Middleware.Count == 0 - ? parseResult.Handler.InvokeAsync(context, cts.Token) - : InvokeHandlerWithMiddleware(context, cts.Token); - - ProcessTerminationHandler? terminationHandler = parseResult.Configuration.ProcessTerminationTimeout.HasValue - ? new (cts, startedInvocation, parseResult.Configuration.ProcessTerminationTimeout.Value) - : null; - try { + Task startedInvocation = parseResult.Handler is not null && parseResult.Configuration.Middleware.Count == 0 + ? parseResult.Handler.InvokeAsync(context, cts.Token) + : InvokeHandlerWithMiddleware(context, cts.Token); + + if (parseResult.Configuration.ProcessTerminationTimeout.HasValue) + terminationHandler = new(cts, startedInvocation, parseResult.Configuration.ProcessTerminationTimeout.Value); + if (terminationHandler is null) { return await startedInvocation; @@ -41,8 +44,7 @@ internal static async Task InvokeAsync(ParseResult parseResult, IConsole? c } catch (Exception ex) when (parseResult.Configuration.ExceptionHandler is not null) { - parseResult.Configuration.ExceptionHandler(ex, context); - return context.ExitCode; + return parseResult.Configuration.ExceptionHandler(ex, context); } finally { @@ -51,16 +53,27 @@ internal static async Task InvokeAsync(ParseResult parseResult, IConsole? c static async Task InvokeHandlerWithMiddleware(InvocationContext context, CancellationToken token) { - InvocationMiddleware invocationChain = BuildInvocationChain(context, true); - - await invocationChain(context, token, (_, _) => Task.CompletedTask); + int exitCode = 0; + InvocationMiddleware invocationChain = BuildInvocationChain(context); + await invocationChain(context, token, async (ctx, token) => + { + if (ctx.ParseResult.Handler is { } handler) + { + exitCode = await handler.InvokeAsync(ctx, token); + } + }); - return GetExitCode(context); + return exitCode; } } internal static int Invoke(ParseResult parseResult, IConsole? console = null) { + if (parseResult.Handler is null && parseResult.Configuration.Middleware.Count == 0) + { + return 0; + } + InvocationContext context = new (parseResult, console); try @@ -74,47 +87,33 @@ internal static int Invoke(ParseResult parseResult, IConsole? console = null) } catch (Exception ex) when (parseResult.Configuration.ExceptionHandler is not null) { - parseResult.Configuration.ExceptionHandler(ex, context); - return context.ExitCode; + return parseResult.Configuration.ExceptionHandler(ex, context); } static int InvokeHandlerWithMiddleware(InvocationContext context) { - InvocationMiddleware invocationChain = BuildInvocationChain(context, false); - - invocationChain(context, CancellationToken.None, static (_, _) => Task.CompletedTask).ConfigureAwait(false).GetAwaiter().GetResult(); - - return GetExitCode(context); + int exitCode = 0; + InvocationMiddleware invocationChain = BuildInvocationChain(context); + invocationChain(context, CancellationToken.None, (ctx, token) => + { + if (ctx.ParseResult.Handler is { } handler) + { + exitCode = handler.Invoke(ctx); + } + + return Task.CompletedTask; + }).GetAwaiter().GetResult(); + return exitCode; } } - private static InvocationMiddleware BuildInvocationChain(InvocationContext context, bool invokeAsync) + private static InvocationMiddleware BuildInvocationChain(InvocationContext context) { - var invocations = new List(context.ParseResult.Configuration.Middleware.Count + 1); - invocations.AddRange(context.ParseResult.Configuration.Middleware); - - invocations.Add(async (invocationContext, cancellationToken, _) => - { - if (invocationContext.ParseResult.Handler is { } handler) - { - context.ExitCode = invokeAsync - ? await handler.InvokeAsync(invocationContext, cancellationToken) - : handler.Invoke(invocationContext); - } - }); - - return invocations.Aggregate( + return context.ParseResult.Configuration.Middleware.Aggregate( (first, second) => (ctx, token, next) => first(ctx, token, (c, t) => second(c, t, next))); } - - private static int GetExitCode(InvocationContext context) - { - context.InvocationResult?.Invoke(context); - - return context.ExitCode; - } } } diff --git a/src/System.CommandLine/Invocation/ParseErrorResult.cs b/src/System.CommandLine/Invocation/ParseErrorResult.cs index 72d4beb84e..11573f4244 100644 --- a/src/System.CommandLine/Invocation/ParseErrorResult.cs +++ b/src/System.CommandLine/Invocation/ParseErrorResult.cs @@ -8,7 +8,7 @@ namespace System.CommandLine.Invocation { internal static class ParseErrorResult { - internal static void Apply(InvocationContext context) + internal static int Apply(InvocationContext context) { context.Console.ResetTerminalForegroundColor(); context.Console.SetTerminalForegroundRed(); @@ -20,11 +20,11 @@ internal static void Apply(InvocationContext context) context.Console.Error.WriteLine(); - context.ExitCode = context.ParseResult.Configuration.ParseErrorReportingExitCode!.Value; - context.Console.ResetTerminalForegroundColor(); HelpOption.Handler(context); + + return context.ParseResult.Configuration.ParseErrorReportingExitCode!.Value; } } } \ No newline at end of file diff --git a/src/System.CommandLine/Invocation/TypoCorrection.cs b/src/System.CommandLine/Invocation/TypoCorrection.cs index 26a5cbf7bd..52481610c6 100644 --- a/src/System.CommandLine/Invocation/TypoCorrection.cs +++ b/src/System.CommandLine/Invocation/TypoCorrection.cs @@ -9,7 +9,7 @@ namespace System.CommandLine.Invocation { internal static class TypoCorrection { - internal static void ProvideSuggestions(InvocationContext context) + internal static int ProvideSuggestions(InvocationContext context) { ParseResult result = context.ParseResult; IConsole console = context.Console; @@ -32,6 +32,8 @@ internal static void ProvideSuggestions(InvocationContext context) console.Out.WriteLine(suggestion); } } + + return 0; } private static IEnumerable GetPossibleTokens(Command targetSymbol, string token, int maxLevenshteinDistance) diff --git a/src/System.CommandLine/ParseDirective.cs b/src/System.CommandLine/ParseDirective.cs index e6235caae4..805f1b9cae 100644 --- a/src/System.CommandLine/ParseDirective.cs +++ b/src/System.CommandLine/ParseDirective.cs @@ -18,11 +18,11 @@ public ParseDirective(int errorExitCode = 1) : base("parse") internal int ErrorExitCode { get; } - private void SyncHandler(InvocationContext context) + private int SyncHandler(InvocationContext context) { var parseResult = context.ParseResult; context.Console.Out.WriteLine(parseResult.Diagram()); - context.ExitCode = parseResult.Errors.Count == 0 ? 0 : ErrorExitCode; + return parseResult.Errors.Count == 0 ? 0 : ErrorExitCode; } } } diff --git a/src/System.CommandLine/SuggestDirective.cs b/src/System.CommandLine/SuggestDirective.cs index c0c445eefb..6d9716f61f 100644 --- a/src/System.CommandLine/SuggestDirective.cs +++ b/src/System.CommandLine/SuggestDirective.cs @@ -16,7 +16,7 @@ public SuggestDirective() : base("suggest") SetSynchronousHandler(SyncHandler); } - private void SyncHandler(InvocationContext context) + private int SyncHandler(InvocationContext context) { ParseResult parseResult = context.ParseResult; string? parsedValues = parseResult.FindResultFor(this)!.Values.SingleOrDefault(); @@ -34,6 +34,8 @@ private void SyncHandler(InvocationContext context) string.Join( Environment.NewLine, completions)); + + return 0; } } }