diff --git a/src/System.CommandLine.Hosting.Tests/HostingTests.cs b/src/System.CommandLine.Hosting.Tests/HostingTests.cs index c827befa35..b6ff8e01d1 100644 --- a/src/System.CommandLine.Hosting.Tests/HostingTests.cs +++ b/src/System.CommandLine.Hosting.Tests/HostingTests.cs @@ -3,7 +3,8 @@ using System.CommandLine.Invocation; using System.CommandLine.Parsing; using System.Linq; - +using System.Threading; +using System.Threading.Tasks; using FluentAssertions; using Microsoft.Extensions.Configuration; @@ -228,6 +229,51 @@ public static void UseHost_binds_parsed_arguments_to_options() Assert.Equal(myValue, options.MyArgument); } + + [Fact(Skip ="WIP")] + public static async Task CommandLineHost_creates_host_for_simple_command() + { + //Arrange + // var args = new string[] { $"--foo", "42" }; + MyOptions options = null; + IHost hostToBind = null; + + var rootCmd = new RootCommand(); + rootCmd.AddOption(new Option($"--foo") { Argument = new Argument() }); + rootCmd.Handler = CommandHandler.Create((host) => + { + hostToBind = host; + }); + // Act + // var tokenSource = new CancellationTokenSource(TimeSpan.FromSeconds(1)); + CancellationTokenSource tokenSource = null; + await CommandLineHost.CreateDefaultBuilder() + .ConfigureCommandLineDefaults((CommandLineBuilder builder) => + { + // TODO: it is not possible to add it like this atm. + builder.AddCommand(rootCmd); + }) + .Build() + .RunAsync(tokenSource?.Token ?? default); + // Assert + Assert.NotNull(hostToBind); + Assert.Equal(42, options.MyArgument); + } + + [Fact] + public static async Task CommandLineHost_contains_errors_in_ParseResult_service_for_not_mapped_input() + { + //Arrange + // Act + var host = CommandLineHost.CreateDefaultBuilder(new string[]{"--foo", "bar"}) + .ConfigureCommandLineDefaults((CommandLineBuilder builder) => {}) + .Build(); + var parseResult = host.Services.GetService(); + await host.StartAsync(); + parseResult.Errors.Should().NotBeEmpty(); + //TODO: clarify how parsing errors and command execution exceptions should be handled + } + private class MyOptions { public int MyArgument { get; set; } diff --git a/src/System.CommandLine.Hosting.Tests/System.CommandLine.Hosting.Tests.csproj b/src/System.CommandLine.Hosting.Tests/System.CommandLine.Hosting.Tests.csproj index 791b5c7186..01874246b4 100644 --- a/src/System.CommandLine.Hosting.Tests/System.CommandLine.Hosting.Tests.csproj +++ b/src/System.CommandLine.Hosting.Tests/System.CommandLine.Hosting.Tests.csproj @@ -15,7 +15,7 @@ - + diff --git a/src/System.CommandLine.Hosting/DefaultBuilder/CommandLineExecutorService.cs b/src/System.CommandLine.Hosting/DefaultBuilder/CommandLineExecutorService.cs new file mode 100644 index 0000000000..465dbeadfa --- /dev/null +++ b/src/System.CommandLine.Hosting/DefaultBuilder/CommandLineExecutorService.cs @@ -0,0 +1,52 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.CommandLine.Invocation; +using System.CommandLine.Parsing; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; + +namespace System.CommandLine.Hosting +{ + internal class CommandLineExecutorService : IHostedService + { + private readonly ILogger logger; + private readonly IHostApplicationLifetime appLifetime; + private readonly IHost host; + private readonly InvocationContext invocation; + private readonly ParseResult parseResult; + + public CommandLineExecutorService( + ILogger logger, + IHostApplicationLifetime appLifetime, + IHost host, + InvocationContext invocation, + ParseResult parseResult) + { + this.logger = logger; + this.appLifetime = appLifetime; + this.host = host; + this.invocation = invocation; + this.parseResult = parseResult; + } + + public async Task StartAsync(CancellationToken cancellationToken) + { + invocation.BindingContext.AddService(typeof(IHost), _ => host); + if(parseResult.Errors.Any()) + { + logger.LogWarning($"Executing {nameof(Parser)} with errors: {parseResult.Errors.Count}"); + } + await parseResult.InvokeAsync(); + appLifetime.StopApplication(); + } + + public Task StopAsync(CancellationToken cancellationToken) + { + return Task.CompletedTask; + } + } +} diff --git a/src/System.CommandLine.Hosting/DefaultBuilder/CommandLineHost.cs b/src/System.CommandLine.Hosting/DefaultBuilder/CommandLineHost.cs new file mode 100644 index 0000000000..0523c8f266 --- /dev/null +++ b/src/System.CommandLine.Hosting/DefaultBuilder/CommandLineHost.cs @@ -0,0 +1,39 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using Microsoft.Extensions.Hosting; +using System.CommandLine.Builder; + +namespace System.CommandLine.Hosting +{ + /// + /// Provides convenience methods for creating instances of and with pre-configured defaults. + /// + public static class CommandLineHost + { + /// + /// Initializes a new instance of the with pre-configured defaults. + /// + /// + /// + /// The initialized . + public static IHostBuilder CreateDefaultBuilder() => + CreateDefaultBuilder(args: null); + + /// + /// Initializes a new instance of the with pre-configured defaults. + /// + /// + /// + /// The command line args. + /// The initialized . + public static IHostBuilder CreateDefaultBuilder(string[] args) + { + var builder = Host.CreateDefaultBuilder(args) + .ConfigureCommandLineDefaults(cmdBuilder => cmdBuilder.UseDefaults(), args); + return builder; + // TODO: add remaining args parsing + // var argsRemaining = invocation.ParseResult.UnparsedTokens.ToArray(); + } + } +} diff --git a/src/System.CommandLine.Hosting/DefaultBuilder/GenericCommandLineHostBuilder.cs b/src/System.CommandLine.Hosting/DefaultBuilder/GenericCommandLineHostBuilder.cs new file mode 100644 index 0000000000..41d4ab5b85 --- /dev/null +++ b/src/System.CommandLine.Hosting/DefaultBuilder/GenericCommandLineHostBuilder.cs @@ -0,0 +1,55 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.CommandLine.Builder; +using System.CommandLine.Invocation; +using System.CommandLine.Parsing; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; +using Microsoft.Extensions.Hosting; + +namespace System.CommandLine.Hosting +{ + internal class GenericCommandLineHostBuilder + { + + private const string ConfigurationDirectiveName = "config"; + public string[] Args { get; } + private readonly IHostBuilder builder; + private CommandLineBuilder commandLineBuilder; + + public GenericCommandLineHostBuilder(IHostBuilder builder, string[] args = default) + { + this.builder = builder; + commandLineBuilder = new CommandLineBuilder(); + Args = args; + } + + public void Configure(Action configure) + { + configure(commandLineBuilder); + Parser parser = commandLineBuilder.Build(); + ParseResult parseResult = parser.Parse(Args); + var invocation = new InvocationContext(parseResult); + AddSystemCommandLine(builder, invocation); + builder.ConfigureHostConfiguration(config => + { + config.AddCommandLineDirectives(invocation.ParseResult, ConfigurationDirectiveName); + }); + } + + private static void AddSystemCommandLine(IHostBuilder host, InvocationContext invocation) + { + host.ConfigureServices(services => + { + services.TryAddSingleton(invocation); + services.AddSingleton(invocation.Console); + // semantically it is transient dependency + services.AddTransient(_ => invocation.InvocationResult); + services.AddTransient(_ => invocation.ParseResult); + }); + } + + } +} diff --git a/src/System.CommandLine.Hosting/DefaultBuilder/GenericHostBuilderExtensions.cs b/src/System.CommandLine.Hosting/DefaultBuilder/GenericHostBuilderExtensions.cs new file mode 100644 index 0000000000..bf49750b5a --- /dev/null +++ b/src/System.CommandLine.Hosting/DefaultBuilder/GenericHostBuilderExtensions.cs @@ -0,0 +1,57 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.CommandLine.Builder; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; + +namespace System.CommandLine.Hosting +{ + /// + /// Extension methods for configuring the IWebHostBuilder. + /// + public static class GenericHostBuilderExtensions + { + /// + /// Initializes a new instance of the class with pre-configured defaults. + /// + /// + /// + /// The instance to configure + /// The configure callback + /// The for chaining. + public static IHostBuilder ConfigureCommandLineDefaults( + this IHostBuilder builder, + Action configure, + string[] args = default) + { + AddGenericCommandLineHostBuilder(builder, args, out var cmdHostBuilder); + cmdHostBuilder.Configure(configure); + builder.ConfigureServices((context, services) => + services.AddHostedService()); + return builder; + } + + /// + /// Adds to . + /// + /// + /// + /// + /// when new builder is intialized + private static bool AddGenericCommandLineHostBuilder(IHostBuilder builder, string[] args, out GenericCommandLineHostBuilder cmdHostBuilder) + { + var hostBuilderState = builder.Properties; + var cacheKey = nameof(GenericCommandLineHostBuilder); + hostBuilderState.TryGetValue(cacheKey, out var initializedHost); + cmdHostBuilder = initializedHost as GenericCommandLineHostBuilder; + if (cmdHostBuilder is object) + { + return false; + } + cmdHostBuilder = new GenericCommandLineHostBuilder(builder, args); + hostBuilderState[cacheKey] = cmdHostBuilder; + return true; + } + } +} diff --git a/src/System.CommandLine.Hosting/System.CommandLine.Hosting.csproj b/src/System.CommandLine.Hosting/System.CommandLine.Hosting.csproj index 1d00cff230..5b9512f79b 100644 --- a/src/System.CommandLine.Hosting/System.CommandLine.Hosting.csproj +++ b/src/System.CommandLine.Hosting/System.CommandLine.Hosting.csproj @@ -12,7 +12,7 @@ - +