From c87ac5c667f16e0268ebc7666fdc82d6d4899a14 Mon Sep 17 00:00:00 2001 From: James White Date: Sun, 31 Dec 2023 13:33:25 -0800 Subject: [PATCH 1/2] Simplified setup, with options still --- .../Commands/OtherCommand.cs | 45 +++++++++++++ .../Program.cs | 25 +++++--- .../Properties/launchSettings.json | 19 +++++- .../Internal/CommandRegistration.cs | 16 +++++ .../SpectreConsoleHostBuilderExtensions.cs | 63 +++++++++++++++++++ .../Internal/TypedCommandRegistration.cs | 24 +++++++ .../SpectreConsoleHostBuilderExtensions.cs | 37 +++++------ 7 files changed, 196 insertions(+), 33 deletions(-) create mode 100644 src/Community.Extensions.Spectre.Cli.Hosting.Sample/Commands/OtherCommand.cs create mode 100644 src/Community.Extensions.Spectre.Cli.Hosting/Internal/CommandRegistration.cs create mode 100644 src/Community.Extensions.Spectre.Cli.Hosting/Internal/SpectreConsoleHostBuilderExtensions.cs create mode 100644 src/Community.Extensions.Spectre.Cli.Hosting/Internal/TypedCommandRegistration.cs diff --git a/src/Community.Extensions.Spectre.Cli.Hosting.Sample/Commands/OtherCommand.cs b/src/Community.Extensions.Spectre.Cli.Hosting.Sample/Commands/OtherCommand.cs new file mode 100644 index 0000000..e8bd7a7 --- /dev/null +++ b/src/Community.Extensions.Spectre.Cli.Hosting.Sample/Commands/OtherCommand.cs @@ -0,0 +1,45 @@ +using System.ComponentModel; +using System.ComponentModel.DataAnnotations; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Spectre.Console; +using Spectre.Console.Cli; + +namespace Community.Extensions.Spectre.Cli.Hosting.Sample.Commands; + +/// +/// Another command, just to show that multiple commands can be added +/// +public class OtherCommand : AsyncCommand +{ + private readonly IAnsiConsole _console; + + /// + /// Creates a OtherCommand with access to the console and logging + /// + /// + /// + public OtherCommand(IAnsiConsole console, ILogger log) + { + _console = console; + } + + /// Executes the command. + /// The command context. + /// The command options. + /// An integer indicating whether or not the command executed successfully. + public override async Task ExecuteAsync(CommandContext context, Options options) + { + _console.MarkupLineInterpolated($"[springgreen2_1] Other {options.Stuff}![/]"); + + return 0; + } + + [Description("OtherOptions")] + public class Options : CommandSettings + { + [Description("Other Stuff")] + [CommandArgument(0, "")] + public string? Stuff { get; set; } + } +} \ No newline at end of file diff --git a/src/Community.Extensions.Spectre.Cli.Hosting.Sample/Program.cs b/src/Community.Extensions.Spectre.Cli.Hosting.Sample/Program.cs index ee4752e..b8a50f8 100644 --- a/src/Community.Extensions.Spectre.Cli.Hosting.Sample/Program.cs +++ b/src/Community.Extensions.Spectre.Cli.Hosting.Sample/Program.cs @@ -9,25 +9,30 @@ var builder = Host.CreateApplicationBuilder(args); builder.Logging.AddSimpleConsole(); -// Yes this is duplicated, this is the one we'll use and that -// can receive services from the outer host service provider. -builder.Services.AddCommand(); +// Adds the commands to the outer IServiceCollection and registers them +// to be added when Spectre.Console.Cli is configured below. + +builder.Services.AddCommand("hello", config => +{ + config + .WithDescription("A command that says hello") + .WithExample("An example of other stuff"); + + //.WithAlias("yo"); +}); + +builder.Services.AddCommand("other"); + builder.UseSpectreConsole(config => { + // All commands above are passed to config.AddCommand() by this point #if DEBUG config.PropagateExceptions(); config.ValidateExamples(); #endif config.SetApplicationName("hello"); config.SetExceptionHandler(BasicExceptionHandler.WriteException); - - // This configures the command with the internal service provider. - // Unfortunately, it comes after the external service provider & host have - // already been built. In future configuration should be extracted to a builder - // that can be configured prior to service provider creation allowing the two - // AddCommand calls to be combined. - config.AddCommand("hello"); }); var app = builder.Build(); diff --git a/src/Community.Extensions.Spectre.Cli.Hosting.Sample/Properties/launchSettings.json b/src/Community.Extensions.Spectre.Cli.Hosting.Sample/Properties/launchSettings.json index a8897fe..a6140d1 100644 --- a/src/Community.Extensions.Spectre.Cli.Hosting.Sample/Properties/launchSettings.json +++ b/src/Community.Extensions.Spectre.Cli.Hosting.Sample/Properties/launchSettings.json @@ -1,8 +1,23 @@ { "profiles": { - "Community.Extensions.Spectre.Cli.Hosting.Sample": { + "Sample": { "commandName": "Project", - "commandLineArgs": "You -p Sadie" + //"commandLineArgs": "You -p Sadie" + //"commandLineArgs": "other stuff" + "commandLineArgs": "--help" + }, + "Interactive": { + "commandName": "Executable", + "executablePath": "pwsh.exe", + "commandLineArgs": "-NoExit -c \"Set-Alias -Name hello -Value \"$(TargetDir)$(AssemblyName).exe\"", + "workingDirectory": "$(ProjectDir)" } + /*, For reference only + "WT": { + "commandName": "Executable", + "executablePath": "wt.exe", + "commandLineArgs": "pwsh.exe -NoExit -c \"Set-Alias -Name hello -Value \"$(TargetDir)$(AssemblyName).exe\"", + "workingDirectory": "$(ProjectDir)" + }*/ } } \ No newline at end of file diff --git a/src/Community.Extensions.Spectre.Cli.Hosting/Internal/CommandRegistration.cs b/src/Community.Extensions.Spectre.Cli.Hosting/Internal/CommandRegistration.cs new file mode 100644 index 0000000..46f07ed --- /dev/null +++ b/src/Community.Extensions.Spectre.Cli.Hosting/Internal/CommandRegistration.cs @@ -0,0 +1,16 @@ +using Spectre.Console.Cli; + +namespace Community.Extensions.Spectre.Cli.Hosting.Internal; + +/// +/// A base registration class for commands with their types and name +/// +/// +/// +public abstract record CommandRegistration(Type CommandType, string Name) +{ + /// + /// + /// + public abstract void Configure(IConfigurator configuration); +} \ No newline at end of file diff --git a/src/Community.Extensions.Spectre.Cli.Hosting/Internal/SpectreConsoleHostBuilderExtensions.cs b/src/Community.Extensions.Spectre.Cli.Hosting/Internal/SpectreConsoleHostBuilderExtensions.cs new file mode 100644 index 0000000..811ed6f --- /dev/null +++ b/src/Community.Extensions.Spectre.Cli.Hosting/Internal/SpectreConsoleHostBuilderExtensions.cs @@ -0,0 +1,63 @@ +using System.Text; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Hosting.Internal; +using Spectre.Console; +using Spectre.Console.Cli; + +namespace Community.Extensions.Spectre.Cli.Hosting.Internal; + +/// +/// Extends with SpectreConsole commands. +/// +public static class CommandRegistrationExtensions +{ + + /// + /// Returns registered commands + /// + /// + /// + public static IEnumerable GetRegisteredCommands(this IServiceProvider serviceProvider) => + serviceProvider.GetServices(); + + /// + /// Registers a command with it's primary type, name and optional configuration action + /// + /// + /// + /// + /// + /// + public static IServiceCollection RegisterCommand(this IServiceCollection services, string name, + Action? commandConfigurator = null) + where TCommand : class, ICommand + { + return services.AddTransient>(c => + new TypedCommandRegistration(name, commandConfigurator)); + } + + /// + /// Adds registered commands to the provided app and allows further customization of the app and commands + /// + /// + /// + /// + /// + internal static ICommandApp ConfigureAppAndRegisteredCommands(this ICommandApp app, IServiceProvider provider, Action? configureCommandApp = null) + { + app.Configure(config => + { + // Add/Configure registered commands + foreach (var cmd in provider.GetRegisteredCommands()) + { + cmd.Configure(config); + } + + // Optionally allow caller to configure the command app + configureCommandApp?.Invoke(config); + }); + + return app; + } +} \ No newline at end of file diff --git a/src/Community.Extensions.Spectre.Cli.Hosting/Internal/TypedCommandRegistration.cs b/src/Community.Extensions.Spectre.Cli.Hosting/Internal/TypedCommandRegistration.cs new file mode 100644 index 0000000..8b296da --- /dev/null +++ b/src/Community.Extensions.Spectre.Cli.Hosting/Internal/TypedCommandRegistration.cs @@ -0,0 +1,24 @@ +using Spectre.Console.Cli; + +namespace Community.Extensions.Spectre.Cli.Hosting.Internal; + +/// +/// A typed registration class for commands with their types and name +/// +/// +/// +public record TypedCommandRegistration(string Name, Action? CommandConfigurator = null) + : CommandRegistration(typeof(TCommand), Name) where TCommand : class, ICommand +{ + /// + /// + /// + public override void Configure(IConfigurator configuration) + { + // Add the command to Spectre's configuration + var cmdConfig = configuration.AddCommand(Name); + + // Optionally configure the command + CommandConfigurator?.Invoke(cmdConfig); + } +} \ No newline at end of file diff --git a/src/Community.Extensions.Spectre.Cli.Hosting/SpectreConsoleHostBuilderExtensions.cs b/src/Community.Extensions.Spectre.Cli.Hosting/SpectreConsoleHostBuilderExtensions.cs index ef05e2d..d191d57 100644 --- a/src/Community.Extensions.Spectre.Cli.Hosting/SpectreConsoleHostBuilderExtensions.cs +++ b/src/Community.Extensions.Spectre.Cli.Hosting/SpectreConsoleHostBuilderExtensions.cs @@ -1,4 +1,5 @@ using System.Text; +using Community.Extensions.Spectre.Cli.Hosting.Internal; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Hosting.Internal; @@ -13,31 +14,36 @@ namespace Community.Extensions.Spectre.Cli.Hosting; public static class SpectreConsoleHostBuilderExtensions { /// - /// Adds a command and it's options to the service collection + /// Adds a command and it's options to the service collection. Also registers the command + /// to be added & configured during the UseSpectreConsole call. /// /// /// /// + /// + /// The configuration action applied to the command /// - public static IServiceCollection AddCommand(this IServiceCollection services) + public static IServiceCollection AddCommand(this IServiceCollection services, string name, + Action? commandConfigurator = null) where TCommand : class, ICommand where TOptions : CommandSettings { // Could use ConfigurationHelper.GetSettingsType(typeof(TCommand)) but I want options flexible services.AddSingleton(); - services.AddTransient(); + services.AddTransient(); // Not actually using currently? + services.RegisterCommand(name, commandConfigurator); return services; } /// - /// Adds the internal services to the host builder. + /// Adds the internal services to the host builder. /// /// /// private static HostApplicationBuilder AddInternalServices(HostApplicationBuilder builder) { - System.Console.OutputEncoding = Encoding.Default; + Console.OutputEncoding = Encoding.Default; builder.Services.AddHostedService(); builder.Services.AddSingleton(x => AnsiConsole.Console); @@ -60,14 +66,8 @@ public static HostApplicationBuilder UseSpectreConsole(this HostApplicationBuild builder.Services.AddSingleton(x => { - var command = new CommandApp(new CustomTypeRegistrar(builder.Services, x)); - - if (configureCommandApp != null) - { - command.Configure(configureCommandApp); - } - - return command; + var app = new CommandApp(new CustomTypeRegistrar(builder.Services, x)); + return app.ConfigureAppAndRegisteredCommands(x, configureCommandApp); }); return AddInternalServices(builder); @@ -89,14 +89,9 @@ public static HostApplicationBuilder UseSpectreConsole(this Hos builder.Services.AddSingleton(x => { - var command = new CommandApp(new CustomTypeRegistrar(builder.Services, x)); - - if (configureCommandApp != null) - { - command.Configure(configureCommandApp); - } - - return command; + // Create the command app + var app = new CommandApp(new CustomTypeRegistrar(builder.Services, x)); + return app.ConfigureAppAndRegisteredCommands(x, configureCommandApp); }); return AddInternalServices(builder); From 1666eb87d2aa40356a95ff205bc433f3472d6667 Mon Sep 17 00:00:00 2001 From: James White Date: Sun, 31 Dec 2023 13:58:07 -0800 Subject: [PATCH 2/2] Reworked the AddCommand to remove options --- .../Program.cs | 26 ++++++++----------- .../BasicExceptionHandler.cs | 9 +++++++ ...ion.cs => CommandRegistration.TCommand.cs} | 2 +- ...ns.cs => CommandRegistrationExtensions.cs} | 4 +-- .../{ => Internal}/CustomTypeRegistrar.cs | 2 +- .../{ => Internal}/CustomTypeResolver.cs | 2 +- .../SpectreConsoleHostBuilderExtensions.cs | 10 +++---- .../SpectreConsoleWorker.cs | 9 +++++++ 8 files changed, 37 insertions(+), 27 deletions(-) rename src/Community.Extensions.Spectre.Cli.Hosting/Internal/{TypedCommandRegistration.cs => CommandRegistration.TCommand.cs} (85%) rename src/Community.Extensions.Spectre.Cli.Hosting/Internal/{SpectreConsoleHostBuilderExtensions.cs => CommandRegistrationExtensions.cs} (92%) rename src/Community.Extensions.Spectre.Cli.Hosting/{ => Internal}/CustomTypeRegistrar.cs (98%) rename src/Community.Extensions.Spectre.Cli.Hosting/{ => Internal}/CustomTypeResolver.cs (96%) diff --git a/src/Community.Extensions.Spectre.Cli.Hosting.Sample/Program.cs b/src/Community.Extensions.Spectre.Cli.Hosting.Sample/Program.cs index b8a50f8..c1e819e 100644 --- a/src/Community.Extensions.Spectre.Cli.Hosting.Sample/Program.cs +++ b/src/Community.Extensions.Spectre.Cli.Hosting.Sample/Program.cs @@ -1,29 +1,25 @@ using System.Diagnostics; +using Community.Extensions.Spectre.Cli.Hosting; +using Community.Extensions.Spectre.Cli.Hosting.Sample.Commands; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; using Spectre.Console; using Spectre.Console.Cli; -using Community.Extensions.Spectre.Cli.Hosting; -using Community.Extensions.Spectre.Cli.Hosting.Sample.Commands; var builder = Host.CreateApplicationBuilder(args); -builder.Logging.AddSimpleConsole(); -// Adds the commands to the outer IServiceCollection and registers them -// to be added when Spectre.Console.Cli is configured below. - -builder.Services.AddCommand("hello", config => +// Add a command and optionally configure it. +builder.Services.AddCommand("hello", cmd => { - config - .WithDescription("A command that says hello") - .WithExample("An example of other stuff"); - - //.WithAlias("yo"); + cmd.WithDescription("A command that says hello"); }); -builder.Services.AddCommand("other"); - +// Add another command +builder.Services.AddCommand("other"); +// +// The standard call save for the commands will be pre-added & configured +// builder.UseSpectreConsole(config => { // All commands above are passed to config.AddCommand() by this point @@ -32,7 +28,7 @@ config.ValidateExamples(); #endif config.SetApplicationName("hello"); - config.SetExceptionHandler(BasicExceptionHandler.WriteException); + config.UseBasicExceptionHandler(); }); var app = builder.Build(); diff --git a/src/Community.Extensions.Spectre.Cli.Hosting/BasicExceptionHandler.cs b/src/Community.Extensions.Spectre.Cli.Hosting/BasicExceptionHandler.cs index 4a368f9..f08d06a 100644 --- a/src/Community.Extensions.Spectre.Cli.Hosting/BasicExceptionHandler.cs +++ b/src/Community.Extensions.Spectre.Cli.Hosting/BasicExceptionHandler.cs @@ -8,6 +8,15 @@ namespace Community.Extensions.Spectre.Cli.Hosting; /// public static class BasicExceptionHandler { + /// + /// Sets the exception handler to write the exception to the AnsiConsole. + /// + /// + /// + public static IConfigurator UseBasicExceptionHandler(this IConfigurator configurator) + { + return configurator.SetExceptionHandler(WriteException); + } /// /// Writes the exception to the AnsiConsole. /// diff --git a/src/Community.Extensions.Spectre.Cli.Hosting/Internal/TypedCommandRegistration.cs b/src/Community.Extensions.Spectre.Cli.Hosting/Internal/CommandRegistration.TCommand.cs similarity index 85% rename from src/Community.Extensions.Spectre.Cli.Hosting/Internal/TypedCommandRegistration.cs rename to src/Community.Extensions.Spectre.Cli.Hosting/Internal/CommandRegistration.TCommand.cs index 8b296da..73e7ec8 100644 --- a/src/Community.Extensions.Spectre.Cli.Hosting/Internal/TypedCommandRegistration.cs +++ b/src/Community.Extensions.Spectre.Cli.Hosting/Internal/CommandRegistration.TCommand.cs @@ -7,7 +7,7 @@ namespace Community.Extensions.Spectre.Cli.Hosting.Internal; /// /// /// -public record TypedCommandRegistration(string Name, Action? CommandConfigurator = null) +public record CommandRegistration(string Name, Action? CommandConfigurator = null) : CommandRegistration(typeof(TCommand), Name) where TCommand : class, ICommand { /// diff --git a/src/Community.Extensions.Spectre.Cli.Hosting/Internal/SpectreConsoleHostBuilderExtensions.cs b/src/Community.Extensions.Spectre.Cli.Hosting/Internal/CommandRegistrationExtensions.cs similarity index 92% rename from src/Community.Extensions.Spectre.Cli.Hosting/Internal/SpectreConsoleHostBuilderExtensions.cs rename to src/Community.Extensions.Spectre.Cli.Hosting/Internal/CommandRegistrationExtensions.cs index 811ed6f..2d9be50 100644 --- a/src/Community.Extensions.Spectre.Cli.Hosting/Internal/SpectreConsoleHostBuilderExtensions.cs +++ b/src/Community.Extensions.Spectre.Cli.Hosting/Internal/CommandRegistrationExtensions.cs @@ -33,8 +33,8 @@ public static IServiceCollection RegisterCommand(this IServiceCollecti Action? commandConfigurator = null) where TCommand : class, ICommand { - return services.AddTransient>(c => - new TypedCommandRegistration(name, commandConfigurator)); + return services.AddTransient>(c => + new CommandRegistration(name, commandConfigurator)); } /// diff --git a/src/Community.Extensions.Spectre.Cli.Hosting/CustomTypeRegistrar.cs b/src/Community.Extensions.Spectre.Cli.Hosting/Internal/CustomTypeRegistrar.cs similarity index 98% rename from src/Community.Extensions.Spectre.Cli.Hosting/CustomTypeRegistrar.cs rename to src/Community.Extensions.Spectre.Cli.Hosting/Internal/CustomTypeRegistrar.cs index 5160d53..cf761f3 100644 --- a/src/Community.Extensions.Spectre.Cli.Hosting/CustomTypeRegistrar.cs +++ b/src/Community.Extensions.Spectre.Cli.Hosting/Internal/CustomTypeRegistrar.cs @@ -3,7 +3,7 @@ using Spectre.Console.Cli; -namespace Community.Extensions.Spectre.Cli.Hosting; +namespace Community.Extensions.Spectre.Cli.Hosting.Internal; internal sealed class CustomTypeRegistrar : ITypeRegistrar { diff --git a/src/Community.Extensions.Spectre.Cli.Hosting/CustomTypeResolver.cs b/src/Community.Extensions.Spectre.Cli.Hosting/Internal/CustomTypeResolver.cs similarity index 96% rename from src/Community.Extensions.Spectre.Cli.Hosting/CustomTypeResolver.cs rename to src/Community.Extensions.Spectre.Cli.Hosting/Internal/CustomTypeResolver.cs index b880a29..0f62673 100644 --- a/src/Community.Extensions.Spectre.Cli.Hosting/CustomTypeResolver.cs +++ b/src/Community.Extensions.Spectre.Cli.Hosting/Internal/CustomTypeResolver.cs @@ -2,7 +2,7 @@ using Microsoft.Extensions.Logging; using Spectre.Console.Cli; -namespace Community.Extensions.Spectre.Cli.Hosting; +namespace Community.Extensions.Spectre.Cli.Hosting.Internal; internal sealed class CustomTypeResolver : ITypeResolver, IDisposable { diff --git a/src/Community.Extensions.Spectre.Cli.Hosting/SpectreConsoleHostBuilderExtensions.cs b/src/Community.Extensions.Spectre.Cli.Hosting/SpectreConsoleHostBuilderExtensions.cs index d191d57..b660d72 100644 --- a/src/Community.Extensions.Spectre.Cli.Hosting/SpectreConsoleHostBuilderExtensions.cs +++ b/src/Community.Extensions.Spectre.Cli.Hosting/SpectreConsoleHostBuilderExtensions.cs @@ -18,20 +18,16 @@ public static class SpectreConsoleHostBuilderExtensions /// to be added & configured during the UseSpectreConsole call. /// /// - /// /// /// /// The configuration action applied to the command /// - public static IServiceCollection AddCommand(this IServiceCollection services, string name, - Action? commandConfigurator = null) - where TCommand : class, ICommand - where TOptions : CommandSettings + public static IServiceCollection AddCommand(this IServiceCollection services, string name, + Action? commandConfigurator = null) + where TCommand : class, ICommand { - // Could use ConfigurationHelper.GetSettingsType(typeof(TCommand)) but I want options flexible services.AddSingleton(); - services.AddTransient(); // Not actually using currently? services.RegisterCommand(name, commandConfigurator); return services; } diff --git a/src/Community.Extensions.Spectre.Cli.Hosting/SpectreConsoleWorker.cs b/src/Community.Extensions.Spectre.Cli.Hosting/SpectreConsoleWorker.cs index 02994d5..f02c3a7 100644 --- a/src/Community.Extensions.Spectre.Cli.Hosting/SpectreConsoleWorker.cs +++ b/src/Community.Extensions.Spectre.Cli.Hosting/SpectreConsoleWorker.cs @@ -4,6 +4,9 @@ namespace Community.Extensions.Spectre.Cli.Hosting; +/// +/// A background service that runs the Spectre Console App +/// public class SpectreConsoleWorker : BackgroundService { private readonly ICommandApp _commandApp; @@ -14,6 +17,12 @@ public class SpectreConsoleWorker : BackgroundService private int _exitCode; + /// + /// + /// + /// + /// + /// public SpectreConsoleWorker(ILogger logger, ICommandApp commandApp, IHostApplicationLifetime hostLifetime) {