Skip to content
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

Feature singular addcommand #1

Merged
merged 2 commits into from
Dec 31, 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
@@ -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;

/// <summary>
/// Another command, just to show that multiple commands can be added
/// </summary>
public class OtherCommand : AsyncCommand<OtherCommand.Options>
{
private readonly IAnsiConsole _console;

/// <summary>
/// Creates a OtherCommand with access to the console and logging
/// </summary>
/// <param name="console"></param>
/// <param name="log"></param>
public OtherCommand(IAnsiConsole console, ILogger<HelloCommand> log)
{
_console = console;
}

/// <summary>Executes the command.</summary>
/// <param name="context">The command context.</param>
/// <param name="options">The command options.</param>
/// <returns>An integer indicating whether or not the command executed successfully.</returns>
public override async Task<int> 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, "<stuff>")]
public string? Stuff { get; set; }
}
}
29 changes: 15 additions & 14 deletions src/Community.Extensions.Spectre.Cli.Hosting.Sample/Program.cs
Original file line number Diff line number Diff line change
@@ -1,33 +1,34 @@
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();

// 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<HelloCommand, HelloCommand.Options>();
// Add a command and optionally configure it.
builder.Services.AddCommand<HelloCommand>("hello", cmd =>
{
cmd.WithDescription("A command that says hello");
});

// Add another command
builder.Services.AddCommand<OtherCommand>("other");

//
// The standard call save for the commands will be pre-added & configured
//
builder.UseSpectreConsole<HelloCommand>(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<HelloCommand>("hello");
config.UseBasicExceptionHandler();
});

var app = builder.Build();
Expand Down
Original file line number Diff line number Diff line change
@@ -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)"
}*/
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,15 @@ namespace Community.Extensions.Spectre.Cli.Hosting;
/// </summary>
public static class BasicExceptionHandler
{
/// <summary>
/// Sets the exception handler to write the exception to the AnsiConsole.
/// </summary>
/// <param name="configurator"></param>
/// <returns></returns>
public static IConfigurator UseBasicExceptionHandler(this IConfigurator configurator)
{
return configurator.SetExceptionHandler(WriteException);
}
/// <summary>
/// Writes the exception to the AnsiConsole.
/// </summary>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
using Spectre.Console.Cli;

namespace Community.Extensions.Spectre.Cli.Hosting.Internal;

/// <summary>
/// A typed registration class for commands with their types and name
/// </summary>
/// <param name="Name"></param>
/// <typeparam name="TCommand"></typeparam>
public record CommandRegistration<TCommand>(string Name, Action<ICommandConfigurator>? CommandConfigurator = null)
: CommandRegistration(typeof(TCommand), Name) where TCommand : class, ICommand
{
/// <summary>
/// </summary>
/// <param name="configuration"></param>
public override void Configure(IConfigurator configuration)
{
// Add the command to Spectre's configuration
var cmdConfig = configuration.AddCommand<TCommand>(Name);

// Optionally configure the command
CommandConfigurator?.Invoke(cmdConfig);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
using Spectre.Console.Cli;

namespace Community.Extensions.Spectre.Cli.Hosting.Internal;

/// <summary>
/// A base registration class for commands with their types and name
/// </summary>
/// <param name="CommandType"></param>
/// <param name="Name"></param>
public abstract record CommandRegistration(Type CommandType, string Name)
{
/// <summary>
/// </summary>
/// <param name="configuration"></param>
public abstract void Configure(IConfigurator configuration);
}
Original file line number Diff line number Diff line change
@@ -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;

/// <summary>
/// Extends <see cref="IHostBuilder" /> with SpectreConsole commands.
/// </summary>
public static class CommandRegistrationExtensions
{

/// <summary>
/// Returns registered commands
/// </summary>
/// <param name="serviceProvider"></param>
/// <returns></returns>
public static IEnumerable<CommandRegistration> GetRegisteredCommands(this IServiceProvider serviceProvider) =>
serviceProvider.GetServices<CommandRegistration>();

/// <summary>
/// Registers a command with it's primary type, name and optional configuration action
/// </summary>
/// <param name="services"></param>
/// <param name="name"></param>
/// <param name="commandConfigurator"></param>
/// <typeparam name="TCommand"></typeparam>
/// <returns></returns>
public static IServiceCollection RegisterCommand<TCommand>(this IServiceCollection services, string name,
Action<ICommandConfigurator>? commandConfigurator = null)
where TCommand : class, ICommand
{
return services.AddTransient<CommandRegistration, CommandRegistration<TCommand>>(c =>
new CommandRegistration<TCommand>(name, commandConfigurator));
}

/// <summary>
/// Adds registered commands to the provided app and allows further customization of the app and commands
/// </summary>
/// <param name="app"></param>
/// <param name="provider"></param>
/// <param name="configureCommandApp"></param>
/// <returns></returns>
internal static ICommandApp ConfigureAppAndRegisteredCommands(this ICommandApp app, IServiceProvider provider, Action<IConfigurator>? 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;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
{
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -13,31 +14,32 @@ namespace Community.Extensions.Spectre.Cli.Hosting;
public static class SpectreConsoleHostBuilderExtensions
{
/// <summary>
/// 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.
/// </summary>
/// <typeparam name="TCommand"></typeparam>
/// <typeparam name="TOptions"></typeparam>
/// <param name="services"></param>
/// <param name="name"></param>
/// <param name="commandConfigurator">The configuration action applied to the command</param>
/// <returns></returns>
public static IServiceCollection AddCommand<TCommand, TOptions>(this IServiceCollection services)
where TCommand : class, ICommand<TOptions>
where TOptions : CommandSettings
public static IServiceCollection AddCommand<TCommand>(this IServiceCollection services, string name,
Action<ICommandConfigurator>? commandConfigurator = null)
where TCommand : class, ICommand

{
// Could use ConfigurationHelper.GetSettingsType(typeof(TCommand)) but I want options flexible
services.AddSingleton<TCommand>();
services.AddTransient<TOptions>();
services.RegisterCommand<TCommand>(name, commandConfigurator);
return services;
}

/// <summary>
/// Adds the internal services to the host builder.
/// Adds the internal services to the host builder.
/// </summary>
/// <param name="builder"></param>
/// <returns></returns>
private static HostApplicationBuilder AddInternalServices(HostApplicationBuilder builder)
{
System.Console.OutputEncoding = Encoding.Default;
Console.OutputEncoding = Encoding.Default;

builder.Services.AddHostedService<SpectreConsoleWorker>();
builder.Services.AddSingleton(x => AnsiConsole.Console);
Expand All @@ -60,14 +62,8 @@ public static HostApplicationBuilder UseSpectreConsole(this HostApplicationBuild

builder.Services.AddSingleton<ICommandApp>(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);
Expand All @@ -89,14 +85,9 @@ public static HostApplicationBuilder UseSpectreConsole<TDefaultCommand>(this Hos

builder.Services.AddSingleton<ICommandApp>(x =>
{
var command = new CommandApp<TDefaultCommand>(new CustomTypeRegistrar(builder.Services, x));

if (configureCommandApp != null)
{
command.Configure(configureCommandApp);
}

return command;
// Create the command app
var app = new CommandApp<TDefaultCommand>(new CustomTypeRegistrar(builder.Services, x));
return app.ConfigureAppAndRegisteredCommands(x, configureCommandApp);
});

return AddInternalServices(builder);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@

namespace Community.Extensions.Spectre.Cli.Hosting;

/// <summary>
/// A background service that runs the Spectre Console App
/// </summary>
public class SpectreConsoleWorker : BackgroundService
{
private readonly ICommandApp _commandApp;
Expand All @@ -14,6 +17,12 @@ public class SpectreConsoleWorker : BackgroundService

private int _exitCode;

/// <summary>
///
/// </summary>
/// <param name="logger"></param>
/// <param name="commandApp"></param>
/// <param name="hostLifetime"></param>
public SpectreConsoleWorker(ILogger<SpectreConsoleWorker> logger, ICommandApp commandApp,
IHostApplicationLifetime hostLifetime)
{
Expand Down
Loading