From e27c879b841070185688da4543c4a9e4309fe845 Mon Sep 17 00:00:00 2001 From: JKamsker Date: Sat, 30 Sep 2023 03:31:04 +0200 Subject: [PATCH] Add TypeResolver to ExceptionHandler --- src/Spectre.Console.Cli/CommandApp.cs | 16 +- .../ConfiguratorExtensions.cs | 13 +- .../ICommandAppSettings.cs | 2 +- .../Internal/CommandExecutor.cs | 172 ++++++++---------- .../Internal/CommandExecutorFactory.cs | 97 ++++++++++ .../Configuration/CommandAppSettings.cs | 2 +- 6 files changed, 195 insertions(+), 107 deletions(-) create mode 100644 src/Spectre.Console.Cli/Internal/CommandExecutorFactory.cs diff --git a/src/Spectre.Console.Cli/CommandApp.cs b/src/Spectre.Console.Cli/CommandApp.cs index 4882fc0c3..6613f8c8c 100644 --- a/src/Spectre.Console.Cli/CommandApp.cs +++ b/src/Spectre.Console.Cli/CommandApp.cs @@ -1,3 +1,4 @@ +using Spectre.Console.Cli.Internal; using Spectre.Console.Cli.Internal.Configuration; namespace Spectre.Console.Cli; @@ -8,7 +9,7 @@ namespace Spectre.Console.Cli; public sealed class CommandApp : ICommandApp { private readonly Configurator _configurator; - private readonly CommandExecutor _executor; + private readonly CommandExecutorFactory _executorFactory; private bool _executed; /// @@ -20,7 +21,7 @@ public CommandApp(ITypeRegistrar? registrar = null) registrar ??= new DefaultTypeRegistrar(); _configurator = new Configurator(registrar); - _executor = new CommandExecutor(registrar); + _executorFactory = new CommandExecutorFactory(registrar); } /// @@ -81,8 +82,9 @@ public async Task RunAsync(IEnumerable args) _executed = true; } - return await _executor - .Execute(_configurator, args) + return await _executorFactory + .CreateCommandExecutor(_configurator, args) + .Execute() .ConfigureAwait(false); } catch (Exception ex) @@ -102,7 +104,11 @@ public async Task RunAsync(IEnumerable args) if (_configurator.Settings.ExceptionHandler != null) { - return _configurator.Settings.ExceptionHandler(ex); + using var resolver = _executorFactory + .CreateCommandExecutor(_configurator, args) + .BuildTypeResolver(); + + return _configurator.Settings.ExceptionHandler(ex, resolver); } // Render the exception. diff --git a/src/Spectre.Console.Cli/ConfiguratorExtensions.cs b/src/Spectre.Console.Cli/ConfiguratorExtensions.cs index 1a27e5aa3..732c299e5 100644 --- a/src/Spectre.Console.Cli/ConfiguratorExtensions.cs +++ b/src/Spectre.Console.Cli/ConfiguratorExtensions.cs @@ -359,7 +359,7 @@ public static IConfigurator SetExceptionHandler(this IConfigurator configurator, /// The configurator. /// The Action that handles the exception. /// A configurator that can be used to configure the application further. - public static IConfigurator SetExceptionHandler(this IConfigurator configurator, Func? exceptionHandler) + public static IConfigurator SetExceptionHandler(this IConfigurator configurator, Func? exceptionHandler) { if (configurator == null) { @@ -369,4 +369,15 @@ public static IConfigurator SetExceptionHandler(this IConfigurator configurator, configurator.Settings.ExceptionHandler = exceptionHandler; return configurator; } + + /// + /// Sets the ExceptionsHandler. + /// + /// The configurator. + /// The Action that handles the exception. + /// A configurator that can be used to configure the application further. + public static IConfigurator SetExceptionHandler(this IConfigurator configurator, Func? exceptionHandler) + { + return configurator.SetExceptionHandler(exceptionHandler == null ? null : (ex, _) => exceptionHandler(ex)); + } } \ No newline at end of file diff --git a/src/Spectre.Console.Cli/ICommandAppSettings.cs b/src/Spectre.Console.Cli/ICommandAppSettings.cs index 12fa7e14c..0227ba55e 100644 --- a/src/Spectre.Console.Cli/ICommandAppSettings.cs +++ b/src/Spectre.Console.Cli/ICommandAppSettings.cs @@ -80,5 +80,5 @@ public interface ICommandAppSettings /// Gets or sets a handler for Exceptions. /// This handler will not be called, if is set to true. /// - public Func? ExceptionHandler { get; set; } + public Func? ExceptionHandler { get; set; } } \ No newline at end of file diff --git a/src/Spectre.Console.Cli/Internal/CommandExecutor.cs b/src/Spectre.Console.Cli/Internal/CommandExecutor.cs index 0268a3cc1..7fbd1f555 100644 --- a/src/Spectre.Console.Cli/Internal/CommandExecutor.cs +++ b/src/Spectre.Console.Cli/Internal/CommandExecutor.cs @@ -4,119 +4,93 @@ internal sealed class CommandExecutor { private readonly ITypeRegistrar _registrar; - public CommandExecutor(ITypeRegistrar registrar) + public IConfiguration Configuration { get; } + public IEnumerable Args { get; } + public bool RequiresVersion { get; internal set; } + public HelpProvider DefaultHelpProvider { get; internal set; } + public CommandTreeParserResult ParsedResult { get; internal set; } + public CommandModel Model { get; internal set; } + + public CommandExecutor( + ITypeRegistrar registrar, + IConfiguration configuration, + IEnumerable args, + bool requiresVersion, + HelpProvider? defaultHelpProvider, + CommandTreeParserResult? parsedResult, + CommandModel? model) { _registrar = registrar ?? throw new ArgumentNullException(nameof(registrar)); + Configuration = configuration; + Args = args; _registrar.Register(typeof(DefaultPairDeconstructor), typeof(DefaultPairDeconstructor)); - } - - public async Task Execute(IConfiguration configuration, IEnumerable args) - { - if (configuration == null) + RequiresVersion = requiresVersion; + + if (requiresVersion) { - throw new ArgumentNullException(nameof(configuration)); - } - - args ??= new List(); - - _registrar.RegisterInstance(typeof(IConfiguration), configuration); - _registrar.RegisterLazy(typeof(IAnsiConsole), () => configuration.Settings.Console.GetConsole()); - - // Register the help provider - var defaultHelpProvider = new HelpProvider(configuration.Settings); - _registrar.RegisterInstance(typeof(IHelpProvider), defaultHelpProvider); - - // Create the command model. - var model = CommandModelBuilder.Build(configuration); - _registrar.RegisterInstance(typeof(CommandModel), model); - _registrar.RegisterDependencies(model); - + return; + } + + DefaultHelpProvider = defaultHelpProvider ?? throw new ArgumentNullException(nameof(defaultHelpProvider)); + ParsedResult = parsedResult ?? throw new ArgumentNullException(nameof(parsedResult)); + Model = model ?? throw new ArgumentNullException(nameof(model)); + } + + public async Task Execute() + { // Asking for version? Kind of a hack, but it's alright. // We should probably make this a bit better in the future. - if (args.Contains("-v") || args.Contains("--version")) + if (RequiresVersion) { - var console = configuration.Settings.Console.GetConsole(); - console.WriteLine(ResolveApplicationVersion(configuration)); + var console = Configuration.Settings.Console.GetConsole(); + console.WriteLine(ResolveApplicationVersion(Configuration)); return 0; } - - // Parse and map the model against the arguments. - var parsedResult = ParseCommandLineArguments(model, configuration.Settings, args); - // Register the arguments with the container. - _registrar.RegisterInstance(typeof(CommandTreeParserResult), parsedResult); - _registrar.RegisterInstance(typeof(IRemainingArguments), parsedResult.Remaining); - // Create the resolver. - using (var resolver = new TypeResolverAdapter(_registrar.Build())) - { - // Get the registered help provider, falling back to the default provider - // registered above if no custom implementations have been registered. - var helpProvider = resolver.Resolve(typeof(IHelpProvider)) as IHelpProvider ?? defaultHelpProvider; - - // Currently the root? - if (parsedResult?.Tree == null) - { - // Display help. - configuration.Settings.Console.SafeRender(helpProvider.Write(model, null)); - return 0; - } - - // Get the command to execute. - var leaf = parsedResult.Tree.GetLeafCommand(); - if (leaf.Command.IsBranch || leaf.ShowHelp) - { - // Branches can't be executed. Show help. - configuration.Settings.Console.SafeRender(helpProvider.Write(model, leaf.Command)); - return leaf.ShowHelp ? 0 : 1; - } - - // Is this the default and is it called without arguments when there are required arguments? - if (leaf.Command.IsDefaultCommand && args.Count() == 0 && leaf.Command.Parameters.Any(p => p.Required)) - { - // Display help for default command. - configuration.Settings.Console.SafeRender(helpProvider.Write(model, leaf.Command)); - return 1; - } - - // Create the content. - var context = new CommandContext(parsedResult.Remaining, leaf.Command.Name, leaf.Command.Data); - - // Execute the command tree. - return await Execute(leaf, parsedResult.Tree, context, resolver, configuration).ConfigureAwait(false); + using var resolver = new TypeResolverAdapter(_registrar.Build()); + + // Get the registered help provider, falling back to the default provider + // registered above if no custom implementations have been registered. + var helpProvider = resolver.Resolve(typeof(IHelpProvider)) as IHelpProvider ?? DefaultHelpProvider; + + // Currently the root? + if (ParsedResult?.Tree == null) + { + // Display help. + Configuration.Settings.Console.SafeRender(helpProvider.Write(Model, null)); + return 0; } + + // Get the command to execute. + var leaf = ParsedResult.Tree.GetLeafCommand(); + if (leaf.Command.IsBranch || leaf.ShowHelp) + { + // Branches can't be executed. Show help. + Configuration.Settings.Console.SafeRender(helpProvider.Write(Model, leaf.Command)); + return leaf.ShowHelp ? 0 : 1; + } + + // Is this the default and is it called without arguments when there are required arguments? + if (leaf.Command.IsDefaultCommand && Args.Count() == 0 && leaf.Command.Parameters.Any(p => p.Required)) + { + // Display help for default command. + Configuration.Settings.Console.SafeRender(helpProvider.Write(Model, leaf.Command)); + return 1; + } + + // Create the content. + var context = new CommandContext(ParsedResult.Remaining, leaf.Command.Name, leaf.Command.Data); + + // Execute the command tree. + return await Execute(leaf, ParsedResult.Tree, context, resolver, Configuration).ConfigureAwait(false); + } + + public TypeResolverAdapter BuildTypeResolver() + { + return new TypeResolverAdapter(_registrar.Build()); } -#pragma warning disable CS8603 // Possible null reference return. - private CommandTreeParserResult ParseCommandLineArguments(CommandModel model, CommandAppSettings settings, IEnumerable args) - { - var parser = new CommandTreeParser(model, settings.CaseSensitivity, settings.ParsingMode, settings.ConvertFlagsToRemainingArguments); - - var parserContext = new CommandTreeParserContext(args, settings.ParsingMode); - var tokenizerResult = CommandTreeTokenizer.Tokenize(args); - var parsedResult = parser.Parse(parserContext, tokenizerResult); - - var lastParsedLeaf = parsedResult?.Tree?.GetLeafCommand(); - var lastParsedCommand = lastParsedLeaf?.Command; - if (lastParsedLeaf != null && lastParsedCommand != null && - lastParsedCommand.IsBranch && !lastParsedLeaf.ShowHelp && - lastParsedCommand.DefaultCommand != null) - { - // Insert this branch's default command into the command line - // arguments and try again to see if it will parse. - var argsWithDefaultCommand = new List(args); - - argsWithDefaultCommand.Insert(tokenizerResult.Tokens.Position, lastParsedCommand.DefaultCommand.Name); - - parserContext = new CommandTreeParserContext(argsWithDefaultCommand, settings.ParsingMode); - tokenizerResult = CommandTreeTokenizer.Tokenize(argsWithDefaultCommand); - parsedResult = parser.Parse(parserContext, tokenizerResult); - } - - return parsedResult; - } -#pragma warning restore CS8603 // Possible null reference return. - private static string ResolveApplicationVersion(IConfiguration configuration) { return diff --git a/src/Spectre.Console.Cli/Internal/CommandExecutorFactory.cs b/src/Spectre.Console.Cli/Internal/CommandExecutorFactory.cs new file mode 100644 index 000000000..8ef68fbc6 --- /dev/null +++ b/src/Spectre.Console.Cli/Internal/CommandExecutorFactory.cs @@ -0,0 +1,97 @@ +namespace Spectre.Console.Cli.Internal; + +internal class CommandExecutorFactory +{ + private ITypeRegistrar _registrar; + + public CommandExecutorFactory(ITypeRegistrar registrar) + { + _registrar = registrar ?? throw new ArgumentNullException(nameof(registrar)); + } + + public CommandExecutor CreateCommandExecutor(IConfiguration configuration, IEnumerable args) + { + if (configuration == null) + { + throw new ArgumentNullException(nameof(configuration)); + } + + args ??= new List(); + + // Asking for version? Kind of a hack, but it's alright. + // We should probably make this a bit better in the future. + if (args.Contains("-v") || args.Contains("--version")) + { + return new CommandExecutor( + registrar: _registrar, + configuration: configuration, + args: args, + requiresVersion: true, + defaultHelpProvider: null, + parsedResult: null, + model: null); + } + + _registrar.Register(typeof(DefaultPairDeconstructor), typeof(DefaultPairDeconstructor)); + _registrar.RegisterInstance(typeof(IConfiguration), configuration); + _registrar.RegisterLazy(typeof(IAnsiConsole), () => configuration.Settings.Console.GetConsole()); + + // Register the help provider + var defaultHelpProvider = new HelpProvider(configuration.Settings); + _registrar.RegisterInstance(typeof(IHelpProvider), defaultHelpProvider); + + // Create the command model. + var model = CommandModelBuilder.Build(configuration); + _registrar.RegisterInstance(typeof(CommandModel), model); + _registrar.RegisterDependencies(model); + + // Parse and map the model against the arguments. + var parsedResult = ParseCommandLineArguments(model, configuration.Settings, args); + + // Register the arguments with the container. + _registrar.RegisterInstance(typeof(CommandTreeParserResult), parsedResult); + _registrar.RegisterInstance(typeof(IRemainingArguments), parsedResult.Remaining); + + return new CommandExecutor( + registrar: _registrar, + configuration: configuration, + args: args, + requiresVersion: false, + defaultHelpProvider: defaultHelpProvider, + parsedResult: parsedResult, + model: model + ); + } + +#pragma warning disable CS8603 // Possible null reference return. + + private CommandTreeParserResult ParseCommandLineArguments(CommandModel model, CommandAppSettings settings, IEnumerable args) + { + var parser = new CommandTreeParser(model, settings.CaseSensitivity, settings.ParsingMode, settings.ConvertFlagsToRemainingArguments); + + var parserContext = new CommandTreeParserContext(args, settings.ParsingMode); + var tokenizerResult = CommandTreeTokenizer.Tokenize(args); + var parsedResult = parser.Parse(parserContext, tokenizerResult); + + var lastParsedLeaf = parsedResult?.Tree?.GetLeafCommand(); + var lastParsedCommand = lastParsedLeaf?.Command; + if (lastParsedLeaf != null && lastParsedCommand != null && + lastParsedCommand.IsBranch && !lastParsedLeaf.ShowHelp && + lastParsedCommand.DefaultCommand != null) + { + // Insert this branch's default command into the command line + // arguments and try again to see if it will parse. + var argsWithDefaultCommand = new List(args); + + argsWithDefaultCommand.Insert(tokenizerResult.Tokens.Position, lastParsedCommand.DefaultCommand.Name); + + parserContext = new CommandTreeParserContext(argsWithDefaultCommand, settings.ParsingMode); + tokenizerResult = CommandTreeTokenizer.Tokenize(argsWithDefaultCommand); + parsedResult = parser.Parse(parserContext, tokenizerResult); + } + + return parsedResult; + } + +#pragma warning restore CS8603 // Possible null reference return. +} \ No newline at end of file diff --git a/src/Spectre.Console.Cli/Internal/Configuration/CommandAppSettings.cs b/src/Spectre.Console.Cli/Internal/Configuration/CommandAppSettings.cs index d1fba735b..b931f2f54 100644 --- a/src/Spectre.Console.Cli/Internal/Configuration/CommandAppSettings.cs +++ b/src/Spectre.Console.Cli/Internal/Configuration/CommandAppSettings.cs @@ -19,7 +19,7 @@ internal sealed class CommandAppSettings : ICommandAppSettings public ParsingMode ParsingMode => StrictParsing ? ParsingMode.Strict : ParsingMode.Relaxed; - public Func? ExceptionHandler { get; set; } + public Func? ExceptionHandler { get; set; } public CommandAppSettings(ITypeRegistrar registrar) {