From 0ee5d0f84304abc10606a1e29cdd4f8203ccaae5 Mon Sep 17 00:00:00 2001 From: walterlv Date: Tue, 30 Sep 2025 14:12:18 +0800 Subject: [PATCH 1/4] =?UTF-8?q?=E5=87=8F=E5=B0=91=20AddHandler=20=E6=97=B6?= =?UTF-8?q?=E5=9C=A8=E5=A0=86=E4=B8=AD=E5=88=86=E9=85=8D=E7=9A=84=E5=86=85?= =?UTF-8?q?=E5=AD=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Program.cs | 3 +- .../Generators/InterceptorGenerator.cs | 10 +- .../Generators/ModelBuilderGenerator.cs | 58 ++++----- .../Models/CommandObjectGeneratingModel.cs | 5 +- src/DotNetCampus.CommandLine/CommandLine.cs | 12 -- .../CommandLineExceptionHandler.cs | 16 ++- src/DotNetCampus.CommandLine/CommandRunner.cs | 76 +++++------- .../CommandRunnerBuilderExtensions.cs | 71 +++++------ .../Compiler/CommandObjectFactory.cs | 31 ----- .../Compiler/CommandRunningContext.cs | 17 +++ .../Compiler/ICommandObjectMetadata.cs | 30 +++++ .../ICommandHandler.cs | 20 ++- .../ICommandRunnerBuilder.cs | 15 ++- .../Utils/Handlers/TaskCommandHandler.cs | 116 +++++++++--------- 14 files changed, 245 insertions(+), 235 deletions(-) delete mode 100644 src/DotNetCampus.CommandLine/Compiler/CommandObjectFactory.cs create mode 100644 src/DotNetCampus.CommandLine/Compiler/CommandRunningContext.cs create mode 100644 src/DotNetCampus.CommandLine/Compiler/ICommandObjectMetadata.cs diff --git a/samples/DotNetCampus.CommandLine.Sample/Program.cs b/samples/DotNetCampus.CommandLine.Sample/Program.cs index 4260489a..9178e4fe 100644 --- a/samples/DotNetCampus.CommandLine.Sample/Program.cs +++ b/samples/DotNetCampus.CommandLine.Sample/Program.cs @@ -77,7 +77,8 @@ static void Main(string[] args) stopwatch.Restart(); for (var i = 0; i < testCount; i++) { - _ = newCommandLine.As(OptionsBuilder.CreateInstance); + var context = new CommandRunningContext { CommandLine = newCommandLine }; + _ = new OptionsBuilder().Build(context); } stopwatch.Stop(); Console.Write($"{stopwatch.ElapsedMilliseconds.ToString(),7} ms | "); diff --git a/src/DotNetCampus.CommandLine.Analyzer/Generators/InterceptorGenerator.cs b/src/DotNetCampus.CommandLine.Analyzer/Generators/InterceptorGenerator.cs index 5ac7f404..a29f9faf 100644 --- a/src/DotNetCampus.CommandLine.Analyzer/Generators/InterceptorGenerator.cs +++ b/src/DotNetCampus.CommandLine.Analyzer/Generators/InterceptorGenerator.cs @@ -151,7 +151,7 @@ private void GenerateCommandLineAddHandlerCode(TypeDeclarationSourceTextBuilder .AddTypeConstraints("where T : class, global::DotNetCampus.Cli.ICommandHandler") .AddRawStatement(GenerateComment(model)) .AddRawStatements($""" - return commandLine.AsRunner().AddHandler(global::{model.CommandObjectType.ContainingNamespace}.{model.GetBuilderTypeName()}.CommandNameGroup, global::{model.CommandObjectType.ContainingNamespace}.{model.GetBuilderTypeName()}.CreateInstance); + return commandLine.AsRunner().AddHandler(global::{model.CommandObjectType.ContainingNamespace}.{model.GetBuilderTypeName()}.CommandNameGroup, new global::{model.CommandObjectType.ContainingNamespace}.{model.GetBuilderTypeName()}.Metadata()); """)); } @@ -166,7 +166,7 @@ private void GenerateCommandLineAddHandlerActionCode(TypeDeclarationSourceTextBu .AddTypeConstraints("where T : class") .AddRawStatement(GenerateComment(model)) .AddRawStatements($""" - return commandLine.AsRunner().AddHandler(handler, global::{model.CommandObjectType.ContainingNamespace}.{model.GetBuilderTypeName()}.CommandNameGroup, global::{model.CommandObjectType.ContainingNamespace}.{model.GetBuilderTypeName()}.CreateInstance); + return commandLine.AsRunner().AddHandler(handler, global::{model.CommandObjectType.ContainingNamespace}.{model.GetBuilderTypeName()}.CommandNameGroup, new global::{model.CommandObjectType.ContainingNamespace}.{model.GetBuilderTypeName()}.Metadata()); """)); } @@ -293,7 +293,7 @@ private void GenerateCommandBuilderAddHandlerCode(TypeDeclarationSourceTextBuild .AddTypeConstraints("where T : class, global::DotNetCampus.Cli.ICommandHandler") .AddRawStatement(GenerateComment(model)) .AddRawStatements($""" - return global::DotNetCampus.Cli.CommandRunnerBuilderExtensions.AddHandler(builder, global::{model.CommandObjectType.ContainingNamespace}.{model.GetBuilderTypeName()}.CommandNameGroup, global::{model.CommandObjectType.ContainingNamespace}.{model.GetBuilderTypeName()}.CreateInstance); + return global::DotNetCampus.Cli.CommandRunnerBuilderExtensions.AddHandler(builder, global::{model.CommandObjectType.ContainingNamespace}.{model.GetBuilderTypeName()}.CommandNameGroup, new global::{model.CommandObjectType.ContainingNamespace}.{model.GetBuilderTypeName()}.Metadata()); """)); } @@ -310,7 +310,7 @@ private void GenerateStatedCommandBuilderAddHandlerCode(TypeDeclarationSourceTex .AddTypeConstraints("where T : class, global::DotNetCampus.Cli.ICommandHandler") .AddRawStatement(GenerateComment(model)) .AddRawStatements($""" - return builder.AddHandler(global::{model.CommandObjectType.ContainingNamespace}.{model.GetBuilderTypeName()}.CommandNameGroup, global::{model.CommandObjectType.ContainingNamespace}.{model.GetBuilderTypeName()}.CreateInstance); + return builder.AddHandler(global::{model.CommandObjectType.ContainingNamespace}.{model.GetBuilderTypeName()}.CommandNameGroup, new global::{model.CommandObjectType.ContainingNamespace}.{model.GetBuilderTypeName()}.Metadata()); """)); } @@ -327,7 +327,7 @@ private void GenerateCommandBuilderAddHandlerActionCode(TypeDeclarationSourceTex .AddTypeConstraints("where T : class") .AddRawStatement(GenerateComment(model)) .AddRawStatements($""" - return global::DotNetCampus.Cli.CommandRunnerBuilderExtensions.AddHandler(builder, handler, global::{model.CommandObjectType.ContainingNamespace}.{model.GetBuilderTypeName()}.CommandNameGroup, global::{model.CommandObjectType.ContainingNamespace}.{model.GetBuilderTypeName()}.CreateInstance); + return global::DotNetCampus.Cli.CommandRunnerBuilderExtensions.AddHandler(builder, handler, global::{model.CommandObjectType.ContainingNamespace}.{model.GetBuilderTypeName()}.CommandNameGroup, new global::{model.CommandObjectType.ContainingNamespace}.{model.GetBuilderTypeName()}.Metadata()); """)); } diff --git a/src/DotNetCampus.CommandLine.Analyzer/Generators/ModelBuilderGenerator.cs b/src/DotNetCampus.CommandLine.Analyzer/Generators/ModelBuilderGenerator.cs index 608b74c0..84875410 100644 --- a/src/DotNetCampus.CommandLine.Analyzer/Generators/ModelBuilderGenerator.cs +++ b/src/DotNetCampus.CommandLine.Analyzer/Generators/ModelBuilderGenerator.cs @@ -1,4 +1,3 @@ -using System.Text; using DotNetCampus.CommandLine.CodeAnalysis; using DotNetCampus.CommandLine.Generators.Builders; using DotNetCampus.CommandLine.Generators.ModelProviding; @@ -29,15 +28,6 @@ private void Execute(SourceProductionContext context, CommandObjectGeneratingMod var code = GenerateCommandObjectCreatorCode(model); context.AddSource($"CommandLine.Models/{model.CommandObjectType.ToDisplayString()}.cs", code); - - // if (model.UseFullStackParser) - // { - // var originalCode = EmbeddedSourceFiles.Enumerate(null) - // .First(x => x.FileName == "CommandLineParser.cs") - // .Content; - // var parserCode = GenerateParserCode(originalCode, model); - // context.AddSource($"CommandLine.Models/{model.Namespace}.{model.CommandObjectType.Name}.parser.cs", parserCode); - // } } private static bool ReportDiagnostics(SourceProductionContext context, CommandObjectGeneratingModel model) @@ -74,10 +64,7 @@ private string GenerateCommandObjectCreatorCode(CommandObjectGeneratingModel mod .AddTypeDeclaration(GenerateBuilderTypeDeclarationLine(model), t => t .WithSummaryComment($"""辅助 生成命令行选项、子命令或处理函数的创建。""") .AddRawMembers(GenerateCommandNames(model)) - .AddMethodDeclaration( - $"public static {model.CommandObjectType.ToUsingString()} CreateInstance(global::DotNetCampus.Cli.Compiler.CommandRunningContext context)", - m => m - .AddRawStatements($"return new {model.Namespace}.{model.GetBuilderTypeName()}().Build(context);")) + .AddTypeDeclaration(GenerateMetadataTypeDeclarationLine(model), nt => GenerateCommandObjectMetadata(nt, model)) .AddRawMembers(model.OptionProperties.Select(GenerateArgumentPropertyCode)) .AddRawMembers(model.EnumeratePositionalArgumentExcludingSameNameOptions().Select(GenerateArgumentPropertyCode)) .AddRawText(GenerateBuildCode(model)) @@ -118,6 +105,12 @@ private static string GenerateBuilderTypeDeclarationLine(CommandObjectGenerating : $"{modifier} sealed class {model.GetBuilderTypeName()}"; } + private static string GenerateMetadataTypeDeclarationLine(CommandObjectGeneratingModel model) + { + var modifier = model.IsPublic ? "public" : "internal"; + return $"{modifier} sealed class Metadata : global::DotNetCampus.Cli.Compiler.ICommandObjectMetadata"; + } + private static string GenerateCommandNames(CommandObjectGeneratingModel model) { var ordinalName = model.CommandNames; @@ -130,6 +123,13 @@ private static string GenerateCommandNames(CommandObjectGeneratingModel model) """; } + private void GenerateCommandObjectMetadata(TypeDeclarationSourceTextBuilder builder, CommandObjectGeneratingModel model) + { + builder + .AddMethodDeclaration("public object Build(global::DotNetCampus.Cli.Compiler.CommandRunningContext context)", m => m + .AddRawStatement($"return new {model.Namespace}.{model.GetBuilderTypeName()}().Build(context);")); + } + private string GenerateArgumentPropertyCode(PropertyGeneratingModel model) => $"private {GetArgumentPropertyTypeName(model)} {model.PropertyName} = new();"; @@ -164,10 +164,10 @@ private static string GenerateBuildCode(CommandObjectGeneratingModel model) => $ } """; - private MethodDeclarationSourceTextBuilder GenerateMatchLongOptionCode(MethodDeclarationSourceTextBuilder builder, CommandObjectGeneratingModel model) + private void GenerateMatchLongOptionCode(MethodDeclarationSourceTextBuilder builder, CommandObjectGeneratingModel model) { var optionProperties = model.OptionProperties; - return builder + builder .Condition(optionProperties.Count is 0, b => b .AddRawStatement("// 没有长名称选项,无需匹配。")) .Otherwise(b => b @@ -208,11 +208,11 @@ static string GenerateLongOptionEqualsCode(OptionalArgumentPropertyGeneratingMod } } - private MethodDeclarationSourceTextBuilder GenerateMatchShortOptionCode(MethodDeclarationSourceTextBuilder builder, CommandObjectGeneratingModel model) + private void GenerateMatchShortOptionCode(MethodDeclarationSourceTextBuilder builder, CommandObjectGeneratingModel model) { var optionProperties = model.OptionProperties; var hasShortName = optionProperties.SelectMany(x => x.GetShortNames()).Any(); - return builder + builder .Condition(!hasShortName, b => b .AddRawStatement("// 没有短名称选项,无需匹配。")) .Otherwise(b => b @@ -259,12 +259,12 @@ static string GenerateOptionEqualsCode(OptionalArgumentPropertyGeneratingModel m } } - private MethodDeclarationSourceTextBuilder GenerateMatchPositionalArgumentsCode(MethodDeclarationSourceTextBuilder builder, + private void GenerateMatchPositionalArgumentsCode(MethodDeclarationSourceTextBuilder builder, CommandObjectGeneratingModel model) { var positionalArgumentProperties = model.PositionalArgumentProperties; var matchAllProperty = positionalArgumentProperties.FirstOrDefault(x => x.Index is 0 && x.Length is int.MaxValue); - return builder + builder .Condition(positionalArgumentProperties.Count is 0, b => b .AddRawStatement("// 没有位置参数,无需匹配。") .AddRawStatement("return global::DotNetCampus.Cli.Utils.Parsers.PositionalArgumentValueMatch.NotMatch;")) @@ -328,7 +328,7 @@ private string GenerateAssignPropertyValueCode(PropertyGeneratingModel model) """; } - private MethodDeclarationSourceTextBuilder GenerateBuildCoreCode(MethodDeclarationSourceTextBuilder builder, CommandObjectGeneratingModel model) + private void GenerateBuildCoreCode(MethodDeclarationSourceTextBuilder builder, CommandObjectGeneratingModel model) { var initRawArgumentsProperties = model.RawArgumentsProperties.Where(x => x.IsRequiredOrInit).ToList(); var initOptionProperties = model.OptionProperties.Where(x => x.IsRequiredOrInit).ToList(); @@ -337,7 +337,7 @@ private MethodDeclarationSourceTextBuilder GenerateBuildCoreCode(MethodDeclarati var setOptionProperties = model.OptionProperties.Where(x => !x.IsRequiredOrInit).ToList(); var setPositionalArgumentProperties = model.PositionalArgumentProperties.Where(x => !x.IsRequiredOrInit).ToList(); - return builder + builder .AddBracketScope($"var result = new {model.CommandObjectType.ToUsingString()}", "{", "};", c => c // 1. [RawArguments] @@ -398,7 +398,7 @@ private MethodDeclarationSourceTextBuilder GenerateBuildCoreCode(MethodDeclarati .AddRawStatement("return result;"); } - private MethodDeclarationSourceTextBuilder GenerateBuildDefaultCode(MethodDeclarationSourceTextBuilder builder, CommandObjectGeneratingModel model) + private void GenerateBuildDefaultCode(MethodDeclarationSourceTextBuilder builder, CommandObjectGeneratingModel model) { var initRawArgumentsProperties = model.RawArgumentsProperties.Where(x => x.IsRequiredOrInit).ToList(); var initOptionProperties = model.OptionProperties.Where(x => x.IsRequiredOrInit).ToList(); @@ -411,10 +411,10 @@ private MethodDeclarationSourceTextBuilder GenerateBuildDefaultCode(MethodDeclar builder.AddRawStatement(""" throw new global::DotNetCampus.Cli.Exceptions.RequiredPropertyNotAssignedException($"The command line arguments doesn't contain any required option or positional argument. Command line: {commandLine}", null!); """); - return builder; + return; } - return builder + builder .AddBracketScope($"var result = new {model.CommandObjectType.ToUsingString()}", "{", "};", c => c // 1. [RawArguments] @@ -570,14 +570,6 @@ private string GenerateEnumDeclarationCode(ITypeSymbol enumType) } """; } - - private string GenerateParserCode(string originalCode, CommandObjectGeneratingModel model) => new StringBuilder() - .AppendLine("#nullable enable") - .AppendLine("using DotNetCampus.Cli;") - .Append(originalCode) - .Replace("namespace DotNetCampus.Cli.Utils.Parsers;", $"namespace {model.Namespace};") - .Replace("public readonly ref struct CommandLineParser", $"partial struct {model.GetBuilderTypeName()}") - .ToString(); } file static class Extensions diff --git a/src/DotNetCampus.CommandLine.Analyzer/Generators/Models/CommandObjectGeneratingModel.cs b/src/DotNetCampus.CommandLine.Analyzer/Generators/Models/CommandObjectGeneratingModel.cs index c8cf4379..7b0e65bf 100644 --- a/src/DotNetCampus.CommandLine.Analyzer/Generators/Models/CommandObjectGeneratingModel.cs +++ b/src/DotNetCampus.CommandLine.Analyzer/Generators/Models/CommandObjectGeneratingModel.cs @@ -28,7 +28,10 @@ internal record CommandObjectGeneratingModel public required IReadOnlyList PositionalArgumentProperties { get; init; } - public string GetBuilderTypeName() => GetBuilderTypeName(CommandObjectType); + public string GetBuilderTypeName() + { + return GetBuilderTypeName(CommandObjectType); + } public static string GetBuilderTypeName(INamedTypeSymbol commandObjectType) { diff --git a/src/DotNetCampus.CommandLine/CommandLine.cs b/src/DotNetCampus.CommandLine/CommandLine.cs index da5e61d9..9499ee54 100644 --- a/src/DotNetCampus.CommandLine/CommandLine.cs +++ b/src/DotNetCampus.CommandLine/CommandLine.cs @@ -102,18 +102,6 @@ public static CommandLine Parse(string singleLineCommandLineArgs, CommandLinePar public T As() where T : notnull => throw MethodShouldBeInspected(); #pragma warning restore CA1822 - /// - /// 尝试将命令行参数转换为指定类型的实例。 - /// - /// 由拦截器传入的命令处理器创建方法。 - /// 要转换的类型。 - /// 转换后的实例。 - [Pure, EditorBrowsable(EditorBrowsableState.Never)] - public T As(CommandObjectFactory factory) where T : notnull - { - return (T)factory(new CommandRunningContext { CommandLine = this }); - } - /// /// 输出传入的命令行参数字符串。如果命令行参数中传入的是 URL,此方法会将 URL 转换为普通的命令行参数再输出。 /// diff --git a/src/DotNetCampus.CommandLine/CommandLineExceptionHandler.cs b/src/DotNetCampus.CommandLine/CommandLineExceptionHandler.cs index 8b454f1a..9fc0f5a7 100644 --- a/src/DotNetCampus.CommandLine/CommandLineExceptionHandler.cs +++ b/src/DotNetCampus.CommandLine/CommandLineExceptionHandler.cs @@ -1,3 +1,4 @@ +using DotNetCampus.Cli.Compiler; using DotNetCampus.Cli.Utils.Parsers; namespace DotNetCampus.Cli; @@ -20,6 +21,19 @@ public Task RunAsync() } } +internal sealed class CommandLineExceptionHandlerMetadata(bool ignoreAllExceptions) : ICommandObjectMetadata +{ + public object Build(CommandRunningContext context) + { + return new CommandLineExceptionHandler(context.CommandLine, ignoreAllExceptions); + } + + public Task RunAsync(object createdCommandObject) + { + return ((CommandLineExceptionHandler)createdCommandObject).RunAsync(); + } +} + /// /// 辅助创建命令行异常处理器。 /// @@ -34,6 +48,6 @@ public static class CommandLineExceptionHandlerExtensions [Obsolete("此方法的实现正在讨论中,API 可能不稳定,请谨慎使用。")] public static IAsyncCommandRunnerBuilder HandleException(this ICoreCommandRunnerBuilder builder, bool ignoreAllExceptions) { - return builder.AsRunner().AddFallbackHandler(c => new CommandLineExceptionHandler(c.CommandLine, ignoreAllExceptions)); + return builder.AsRunner().AddFallbackHandler(new CommandLineExceptionHandlerMetadata(ignoreAllExceptions)); } } diff --git a/src/DotNetCampus.CommandLine/CommandRunner.cs b/src/DotNetCampus.CommandLine/CommandRunner.cs index d6df6577..e5e8437d 100644 --- a/src/DotNetCampus.CommandLine/CommandRunner.cs +++ b/src/DotNetCampus.CommandLine/CommandRunner.cs @@ -2,34 +2,32 @@ using System.Runtime.ExceptionServices; using DotNetCampus.Cli.Compiler; using DotNetCampus.Cli.Exceptions; -using DotNetCampus.Cli.Utils.Handlers; using DotNetCampus.Cli.Utils.Parsers; namespace DotNetCampus.Cli; -using FactoryAndRunner = (CommandObjectFactory Factory, CommandHandlerRunner? Runner); - /// /// 辅助 根据已解析的命令行参数执行对应的命令处理器。 /// public class CommandRunner : ICommandRunnerBuilder, IAsyncCommandRunnerBuilder { private readonly CommandLine _commandLine; - private readonly SortedList _factories; private readonly StringComparison _stringComparison; private readonly bool _supportsOrdinal; private readonly bool _supportsPascalCase; - private FactoryAndRunner? _defaultFactory; - private CommandObjectFactory? _fallbackFactory; private int _maxCommandLength; + private readonly SortedList _candidates; + private ICommandObjectMetadata? _default; + private ICommandObjectMetadata? _fallback; + internal CommandRunner(CommandLine commandLine) { _commandLine = commandLine; var caseSensitive = commandLine.ParsingOptions.Style.CaseSensitive; - _factories = caseSensitive - ? new SortedList(StringLengthDescendingComparer.CaseSensitive) - : new SortedList(StringLengthDescendingComparer.CaseInsensitive); + _candidates = caseSensitive + ? new SortedList(StringLengthDescendingComparer.CaseSensitive) + : new SortedList(StringLengthDescendingComparer.CaseInsensitive); _stringComparison = caseSensitive ? StringComparison.Ordinal : StringComparison.OrdinalIgnoreCase; @@ -65,7 +63,7 @@ internal bool RunFallback(CommandLineParsingResult result) CommandLine = _commandLine, CommandRunner = this, }; - if (_fallbackFactory?.Invoke(context) is not ICommandHandler fallback) + if (_fallback?.Build(context) is not ICommandHandler fallback) { return false; } @@ -85,9 +83,9 @@ internal bool RunFallback(CommandLineParsingResult result) /// public Task RunAsync() { - var (possibleCommandNames, nullableFactoryAndRunner) = MatchCreator(); + var (possibleCommandNames, nullableMetadata) = MatchCommandObject(); - if (nullableFactoryAndRunner is not { } factoryAndRunner) + if (nullableMetadata is not { } metadata) { throw new CommandNameNotFoundException( string.IsNullOrEmpty(possibleCommandNames) @@ -96,29 +94,29 @@ public Task RunAsync() possibleCommandNames); } - var (factory, runner) = factoryAndRunner; var context = new CommandRunningContext { CommandLine = _commandLine, CommandRunner = this, }; - var handler = factory(context); - var exitCode = runner switch + var commandObject = metadata.Build(context); + var exitCode = (metadata, commandObject) switch { - null => ((ICommandHandler)handler).RunAsync(), - _ => runner(handler), + (ICommandHandlerMetadata a, var o) => a.RunAsync(o), + (_, ICommandHandler o) => o.RunAsync(), + _ => throw new CommandLineException("Unreachable code."), }; - return CommandRunningResult.FromTask(exitCode, _commandLine, handler); + return CommandRunningResult.FromTask(exitCode, _commandLine, commandObject); } - private (string PossibleCommandNames, FactoryAndRunner? FactoryAndRunner) MatchCreator() + private (string PossibleCommandNames, ICommandObjectMetadata? Metadata) MatchCommandObject() { - if (_factories.Count > 0) + if (_candidates.Count > 0) { var maxLength = _maxCommandLength; var header = _commandLine.GetHeader(maxLength); - foreach (var (command, factory) in _factories) + foreach (var (command, factory) in _candidates) { if (header.StartsWith(command, _stringComparison)) { @@ -131,7 +129,7 @@ public Task RunAsync() } } - if (_defaultFactory is { } defaultFactory) + if (_default is { } defaultFactory) { return ("", defaultFactory); } @@ -143,18 +141,17 @@ public Task RunAsync() /// 添加一个命令处理器。 /// /// 由拦截器传入的的命令处理器的命令, 表示此处理器没有命令名称。 - /// 由拦截器传入的命令处理器创建方法。 - /// 由拦截器传入的命令处理器运行方法。 + /// 由拦截器传入的包含命令对象如何创建和运行的元数据。 /// 返回一个命令处理器构建器。 [EditorBrowsable(EditorBrowsableState.Never)] - internal CommandRunner AddHandlerCore(NamingPolicyNameGroup command, CommandObjectFactory factory, CommandHandlerRunner? runner) + internal CommandRunner AddHandlerCore(NamingPolicyNameGroup command, ICommandObjectMetadata metadata) { if (_supportsOrdinal) { if (command.Ordinal is { } ordinal && !string.IsNullOrEmpty(ordinal)) { // 包含命令名称。 - var isAdded = _factories.TryAdd(ordinal, (factory, runner)); + var isAdded = _candidates.TryAdd(ordinal, metadata); if (!isAdded) { throw new CommandNameAmbiguityException($"The command '{ordinal}' is already registered.", ordinal); @@ -164,11 +161,11 @@ internal CommandRunner AddHandlerCore(NamingPolicyNameGroup command, CommandObje else { // 不包含命令名称,表示这是默认命令。 - if (_defaultFactory is not null) + if (_default is not null) { throw new CommandNameAmbiguityException("The default command handler is already registered.", null); } - _defaultFactory = (factory, runner); + _default = metadata; } } if (_supportsPascalCase) @@ -176,7 +173,7 @@ internal CommandRunner AddHandlerCore(NamingPolicyNameGroup command, CommandObje if (command.PascalCase is { } pascal && !string.IsNullOrEmpty(pascal)) { // 包含命令名称。 - var isAdded = _factories.TryAdd(pascal, (factory, runner)); + var isAdded = _candidates.TryAdd(pascal, metadata); if (!isAdded && !_supportsOrdinal) { // 转换的名称,之后在仅用转换名称时才需要抛出异常;否则很可能前面已经添加了一个相同的名称。 @@ -187,12 +184,12 @@ internal CommandRunner AddHandlerCore(NamingPolicyNameGroup command, CommandObje else { // 不包含命令名称,表示这是默认命令。 - if (_defaultFactory is not null && !_supportsOrdinal) + if (_default is not null && !_supportsOrdinal) { // 如果支持双命名法,则允许前面已经注册了一个默认命令。 throw new CommandNameAmbiguityException("The default command handler is already registered.", null); } - _defaultFactory = (factory, runner); + _default = metadata; } } return this; @@ -201,11 +198,11 @@ internal CommandRunner AddHandlerCore(NamingPolicyNameGroup command, CommandObje /// /// 添加一个回退的命令处理器。当其他命令出现了错误时,会执行此命令处理器。 /// - /// 回退命令处理器的创建方法。 + /// 由拦截器传入的包含命令对象如何创建和运行的元数据。 /// 返回一个命令处理器构建器。 - internal CommandRunner AddFallbackHandler(CommandObjectFactory factory) + internal CommandRunner AddFallbackHandler(ICommandObjectMetadata metadata) { - _fallbackFactory = factory; + _fallback = metadata; return this; } } @@ -242,19 +239,15 @@ public readonly record struct CommandRunningResult /// /// 异步的命令行处理任务。 /// 被执行的命令行。 - /// 处理此命令行的命令处理器实例。 + /// 处理此命令行的命令对象实例。 /// 命令行处理结果。 - public static async Task FromTask(Task exitCodeTask, CommandLine commandLine, object handler) + public static async Task FromTask(Task exitCodeTask, CommandLine commandLine, object commandObject) { return new CommandRunningResult { ExitCode = await exitCodeTask, CommandLine = commandLine, - HandledBy = handler switch - { - IAnonymousCommandHandler anonymousHandler => anonymousHandler.CreatedCommandOptions, - _ => handler, - }, + HandledBy = commandObject, }; } } @@ -276,7 +269,6 @@ public static async Task AsExitCodeTask(this Task tas } #if NETCOREAPP3_1_OR_GREATER - /// /// 将一个异步的命令行处理结果任务转换为一个异步的退出代码任务。 /// diff --git a/src/DotNetCampus.CommandLine/CommandRunnerBuilderExtensions.cs b/src/DotNetCampus.CommandLine/CommandRunnerBuilderExtensions.cs index af41efd5..1529f2ee 100644 --- a/src/DotNetCampus.CommandLine/CommandRunnerBuilderExtensions.cs +++ b/src/DotNetCampus.CommandLine/CommandRunnerBuilderExtensions.cs @@ -173,62 +173,56 @@ public static IAsyncCommandRunnerBuilder AddHandler(this IAsyncCommandRunnerB /// /// 命令行执行器构造的链式调用。 /// 由拦截器传入的的命令处理器的命令, 或空字符串表示此处理器没有命令名称。 - /// 由拦截器传入的命令处理器创建方法。 - /// 如果有需要,拦截器可以传入一个已有的命令行执行器实例。 + /// 由拦截器传入的包含命令对象如何创建和运行的元数据。 /// 命令处理器的类型。 /// 命令行执行器构造的链式调用。 [EditorBrowsable(EditorBrowsableState.Never)] public static IAsyncCommandRunnerBuilder AddHandler(this ICommandRunnerBuilder builder, - NamingPolicyNameGroup command, CommandObjectFactory factory, CommandHandlerRunner? runner = null - ) + NamingPolicyNameGroup command, ICommandObjectMetadata metadata) where T : class, ICommandHandler { return builder.AsRunner() - .AddHandlerCore(command, factory, runner); + .AddHandlerCore(command, metadata); } - /// + /// [EditorBrowsable(EditorBrowsableState.Never)] public static IAsyncCommandRunnerBuilder AddHandler(this IAsyncCommandRunnerBuilder builder, - NamingPolicyNameGroup command, CommandObjectFactory factory, CommandHandlerRunner? runner = null - ) + NamingPolicyNameGroup command, ICommandObjectMetadata metadata) where T : class, ICommandHandler { return builder.AsRunner() - .AddHandlerCore(command, factory, runner); + .AddHandlerCore(command, metadata); } - /// + /// [EditorBrowsable(EditorBrowsableState.Never)] public static ICommandRunnerBuilder AddHandler(this ICommandRunnerBuilder builder, Action handler, - NamingPolicyNameGroup command, CommandObjectFactory factory - ) + NamingPolicyNameGroup command, ICommandObjectMetadata metadata) where T : class { return builder.AsRunner() - .AddHandlerCore(command, c => new AnonymousCommandHandler(c, factory, handler), null); + .AddHandlerCore(command, new AnonymousActionCommandHandler(metadata, handler)); } - /// + /// [EditorBrowsable(EditorBrowsableState.Never)] public static ICommandRunnerBuilder AddHandler(this ICommandRunnerBuilder builder, Func handler, - NamingPolicyNameGroup command, CommandObjectFactory factory - ) + NamingPolicyNameGroup command, ICommandObjectMetadata metadata) where T : class { return builder.AsRunner() - .AddHandlerCore(command, c => new AnonymousInt32CommandHandler(c, factory, handler), null); + .AddHandlerCore(command, new AnonymousFuncInt32CommandHandler(metadata, handler)); } - /// + /// [EditorBrowsable(EditorBrowsableState.Never)] public static IAsyncCommandRunnerBuilder AddHandler(this ICommandRunnerBuilder builder, Func handler, - NamingPolicyNameGroup command, CommandObjectFactory factory - ) + NamingPolicyNameGroup command, ICommandObjectMetadata metadata) where T : class { return builder.AsRunner() - .AddHandlerCore(command, c => new AnonymousTaskCommandHandler(c, factory, handler), null); + .AddHandlerCore(command, new AnonymousFuncTaskCommandHandler(metadata, handler)); } /// @@ -237,61 +231,56 @@ public static IAsyncCommandRunnerBuilder AddHandler(this ICommandRunnerBuilde /// 命令行执行器构造的链式调用。 /// 用于处理已解析的命令行参数的委托。 /// 由拦截器传入的的命令处理器的命令, 或空字符串表示此处理器没有命令名称。 - /// 由拦截器传入的命令处理器创建方法。 + /// 由拦截器传入的包含命令对象如何创建和运行的元数据。 /// 命令处理器的类型。 /// 命令行执行器构造的链式调用。 [EditorBrowsable(EditorBrowsableState.Never)] public static IAsyncCommandRunnerBuilder AddHandler(this ICommandRunnerBuilder builder, Func> handler, - NamingPolicyNameGroup command, CommandObjectFactory factory - ) + NamingPolicyNameGroup command, ICommandObjectMetadata metadata) where T : class { return builder.AsRunner() - .AddHandlerCore(command, c => new AnonymousTaskInt32CommandHandler(c, factory, handler), null); + .AddHandlerCore(command, new AnonymousFuncTaskInt32CommandHandler(metadata, handler)); } - /// + /// [EditorBrowsable(EditorBrowsableState.Never)] public static IAsyncCommandRunnerBuilder AddHandler(this IAsyncCommandRunnerBuilder builder, Action handler, - NamingPolicyNameGroup command, CommandObjectFactory factory - ) + NamingPolicyNameGroup command, ICommandObjectMetadata metadata) where T : class { return builder.AsRunner() - .AddHandlerCore(command, c => new AnonymousCommandHandler(c, factory, handler), null); + .AddHandlerCore(command, new AnonymousActionCommandHandler(metadata, handler)); } - /// + /// [EditorBrowsable(EditorBrowsableState.Never)] public static IAsyncCommandRunnerBuilder AddHandler(this IAsyncCommandRunnerBuilder builder, Func handler, - NamingPolicyNameGroup command, CommandObjectFactory factory - ) + NamingPolicyNameGroup command, ICommandObjectMetadata metadata) where T : class { return builder.AsRunner() - .AddHandlerCore(command, c => new AnonymousInt32CommandHandler(c, factory, handler), null); + .AddHandlerCore(command, new AnonymousFuncInt32CommandHandler(metadata, handler)); } - /// + /// [EditorBrowsable(EditorBrowsableState.Never)] public static IAsyncCommandRunnerBuilder AddHandler(this IAsyncCommandRunnerBuilder builder, Func handler, - NamingPolicyNameGroup command, CommandObjectFactory factory - ) + NamingPolicyNameGroup command, ICommandObjectMetadata metadata) where T : class { return builder.AsRunner() - .AddHandlerCore(command, c => new AnonymousTaskCommandHandler(c, factory, handler), null); + .AddHandlerCore(command, new AnonymousFuncTaskCommandHandler(metadata, handler)); } - /// + /// [EditorBrowsable(EditorBrowsableState.Never)] public static IAsyncCommandRunnerBuilder AddHandler(this IAsyncCommandRunnerBuilder builder, Func> handler, - NamingPolicyNameGroup command, CommandObjectFactory factory - ) + NamingPolicyNameGroup command, ICommandObjectMetadata metadata) where T : class { return builder.AsRunner() - .AddHandlerCore(command, c => new AnonymousTaskInt32CommandHandler(c, factory, handler), null); + .AddHandlerCore(command, new AnonymousFuncTaskInt32CommandHandler(metadata, handler)); } /// diff --git a/src/DotNetCampus.CommandLine/Compiler/CommandObjectFactory.cs b/src/DotNetCampus.CommandLine/Compiler/CommandObjectFactory.cs deleted file mode 100644 index b6b64aa5..00000000 --- a/src/DotNetCampus.CommandLine/Compiler/CommandObjectFactory.cs +++ /dev/null @@ -1,31 +0,0 @@ -namespace DotNetCampus.Cli.Compiler; - -/// -/// 从已解析的命令行参数创建命令数据模型或处理器的委托。 -/// -/// 命令数据模型或处理器的创建上下文。 -/// 命令数据模型或处理器。 -public delegate object CommandObjectFactory(CommandRunningContext context); - -/// -/// 运行命令处理器的委托。 -/// -/// 传递给命令处理器的状态对象。 -/// 命令处理器的返回值。 -public delegate Task CommandHandlerRunner(object state); - -/// -/// 命令执行的上下文。 -/// -public readonly struct CommandRunningContext -{ - /// - /// 已解析的命令行参数。 - /// - public required CommandLine CommandLine { get; init; } - - /// - /// 运行执行器的实例。如果直接通过 方法创建执行器,则该属性为 null。 - /// - internal CommandRunner? CommandRunner { get; init; } -} diff --git a/src/DotNetCampus.CommandLine/Compiler/CommandRunningContext.cs b/src/DotNetCampus.CommandLine/Compiler/CommandRunningContext.cs new file mode 100644 index 00000000..e905de26 --- /dev/null +++ b/src/DotNetCampus.CommandLine/Compiler/CommandRunningContext.cs @@ -0,0 +1,17 @@ +namespace DotNetCampus.Cli.Compiler; + +/// +/// 命令执行的上下文。 +/// +public readonly struct CommandRunningContext +{ + /// + /// 已解析的命令行参数。 + /// + public required CommandLine CommandLine { get; init; } + + /// + /// 运行执行器的实例。如果直接通过 方法创建执行器,则该属性为 null。 + /// + internal CommandRunner? CommandRunner { get; init; } +} diff --git a/src/DotNetCampus.CommandLine/Compiler/ICommandObjectMetadata.cs b/src/DotNetCampus.CommandLine/Compiler/ICommandObjectMetadata.cs new file mode 100644 index 00000000..e7991166 --- /dev/null +++ b/src/DotNetCampus.CommandLine/Compiler/ICommandObjectMetadata.cs @@ -0,0 +1,30 @@ +namespace DotNetCampus.Cli.Compiler; + +/// +/// 提供给源生成器生成的类型实现,用于构建命令行对象 的元数据。 +/// +/// +/// 源生成器生成元数据类型时,必须要求不包含任何字段(包括隐式字段)。 +/// +public interface ICommandObjectMetadata +{ + /// + /// 构建命令行对象实例。 + /// + /// 包含此命令行对象创建时,命令行运行命令的相关信息。 + /// 命令行对象实例。 + object Build(CommandRunningContext context); +} + +/// +/// 框架内部发现元数据不自带命令执行功能时,会在内部寻找适合的类型代理执行命令。 +/// +internal interface ICommandHandlerMetadata : ICommandObjectMetadata +{ + /// + /// 运行命令处理器。 + /// + /// 通过 创建的命令行对象实例。 + /// 命令处理器的返回值。 + Task RunAsync(object createdCommandObject); +} diff --git a/src/DotNetCampus.CommandLine/ICommandHandler.cs b/src/DotNetCampus.CommandLine/ICommandHandler.cs index 8d585e36..27ed82b6 100644 --- a/src/DotNetCampus.CommandLine/ICommandHandler.cs +++ b/src/DotNetCampus.CommandLine/ICommandHandler.cs @@ -3,21 +3,25 @@ namespace DotNetCampus.Cli; /// /// 表示一个可以接收命令行参数的对象。 /// -public interface ICommandOptions -{ -} +public interface ICommandObject; /// /// 表示可以接收命令行参数,然后带着额外状态处理一条命令。 /// -public interface IStatedCommandHandler : ICommandOptions +public interface IStatedCommandHandler : ICommandObject +#pragma warning disable CS0618 // 类型或成员已过时 + , ICommandOptions +#pragma warning restore CS0618 // 类型或成员已过时 { } /// /// 表示可以接收命令行参数,然后处理一条命令。 /// -public interface ICommandHandler : ICommandOptions +public interface ICommandHandler : ICommandObject +#pragma warning disable CS0618 // 类型或成员已过时 + , ICommandOptions +#pragma warning restore CS0618 // 类型或成员已过时 { /// /// 处理一条命令。 @@ -38,3 +42,9 @@ public interface ICommandHandler : IStatedCommandHandler /// 返回处理结果。 Task RunAsync(T state); } + +/// +/// 表示一个可以接收命令行参数的对象。 +/// +[Obsolete("已重命名为 ICommandObject")] +public interface ICommandOptions; diff --git a/src/DotNetCampus.CommandLine/ICommandRunnerBuilder.cs b/src/DotNetCampus.CommandLine/ICommandRunnerBuilder.cs index defb5c7d..c748e21e 100644 --- a/src/DotNetCampus.CommandLine/ICommandRunnerBuilder.cs +++ b/src/DotNetCampus.CommandLine/ICommandRunnerBuilder.cs @@ -1,5 +1,6 @@ using System.ComponentModel; using DotNetCampus.Cli.Compiler; +using DotNetCampus.Cli.Utils.Handlers; namespace DotNetCampus.Cli; @@ -74,18 +75,17 @@ public StatedCommandRunnerLinkedBuilder AddHandler() /// 由拦截器调用,用于添加一个带有状态的命令处理器。 /// /// 由拦截器传入的的命令处理器的命令, 或空字符串表示此处理器没有命令名称。 - /// 由拦截器传入的命令处理器创建方法。 + /// 由拦截器传入的包含命令对象如何创建和运行的元数据。 /// 带有状态的命令处理器的类型。 /// 命令行执行器构造的链式调用。 [EditorBrowsable(EditorBrowsableState.Never)] public StatedCommandRunnerLinkedBuilder AddHandler( - NamingPolicyNameGroup command, CommandObjectFactory factory - ) + NamingPolicyNameGroup command, ICommandObjectMetadata metadata) where T : class, ICommandHandler { var state = _state; _builder.AsRunner() - .AddHandlerCore(command, factory, o => ((T)o).RunAsync(state)); + .AddHandlerCore(command, new AnonymousStatedCommandHandler(metadata, state)); return new StatedCommandRunnerLinkedBuilder(_builder, _state); } } @@ -145,18 +145,17 @@ public StatedCommandRunnerLinkedBuilder AddHandler() /// 由拦截器调用,用于添加一个带有状态的命令处理器。 /// /// 由拦截器传入的的命令处理器的命令, 或空字符串表示此处理器没有命令名称。 - /// 由拦截器传入的命令处理器创建方法。 + /// 由拦截器传入的包含命令对象如何创建和运行的元数据。 /// 带有状态的命令处理器的类型。 /// 命令行执行器构造的链式调用。 [EditorBrowsable(EditorBrowsableState.Never)] public StatedCommandRunnerLinkedBuilder AddHandler( - NamingPolicyNameGroup command, CommandObjectFactory factory - ) + NamingPolicyNameGroup command, ICommandObjectMetadata metadata) where T : class, ICommandHandler { var state = _state; _builder.AsRunner() - .AddHandlerCore(command, factory, o => ((T)o).RunAsync(state)); + .AddHandlerCore(command, new AnonymousStatedCommandHandler(metadata, state)); return this; } diff --git a/src/DotNetCampus.CommandLine/Utils/Handlers/TaskCommandHandler.cs b/src/DotNetCampus.CommandLine/Utils/Handlers/TaskCommandHandler.cs index 16b7c901..0b5dbf9a 100644 --- a/src/DotNetCampus.CommandLine/Utils/Handlers/TaskCommandHandler.cs +++ b/src/DotNetCampus.CommandLine/Utils/Handlers/TaskCommandHandler.cs @@ -1,94 +1,100 @@ +using System.Runtime.ExceptionServices; using DotNetCampus.Cli.Compiler; namespace DotNetCampus.Cli.Utils.Handlers; -internal interface IAnonymousCommandHandler : ICommandHandler +internal sealed class AnonymousStatedCommandHandler( + ICommandObjectMetadata factory, + TState state) : ICommandHandlerMetadata { - object? CreatedCommandOptions { get; } + public object Build(CommandRunningContext context) + { + return factory.Build(context); + } + + public Task RunAsync(object createdCommandObject) + { + var instance = (ICommandHandler)createdCommandObject; + return instance.RunAsync(state); + } } -internal sealed class AnonymousCommandHandler( - CommandRunningContext context, - CommandObjectFactory factory, - Action handler) : IAnonymousCommandHandler +internal sealed class AnonymousActionCommandHandler( + ICommandObjectMetadata factory, + Action handler) : ICommandHandlerMetadata where T : notnull { - private T? _options; - - public object? CreatedCommandOptions => _options; + public object Build(CommandRunningContext context) + { + return factory.Build(context); + } - public Task RunAsync() + public Task RunAsync(object createdCommandObject) { - _options ??= (T)factory(context); - if (_options is null) - { - throw new InvalidOperationException($"No options of type {typeof(T)} were created."); - } - handler(_options); + var instance = (T)createdCommandObject; + handler(instance); return Task.FromResult(0); } } -internal sealed class AnonymousInt32CommandHandler( - CommandRunningContext context, - CommandObjectFactory factory, - Func handler) : IAnonymousCommandHandler +internal sealed class AnonymousFuncInt32CommandHandler( + ICommandObjectMetadata factory, + Func handler) : ICommandHandlerMetadata where T : notnull { - private T? _options; - - public object? CreatedCommandOptions => _options; + public object Build(CommandRunningContext context) + { + return factory.Build(context); + } - public Task RunAsync() + public Task RunAsync(object createdCommandObject) { - _options ??= (T)factory(context); - if (_options is null) - { - throw new InvalidOperationException($"No options of type {typeof(T)} were created."); - } - return Task.FromResult(handler(_options)); + var instance = (T)createdCommandObject; + var exitCode = handler(instance); + return Task.FromResult(exitCode); } } -internal sealed class AnonymousTaskCommandHandler( - CommandRunningContext context, - CommandObjectFactory factory, - Func handler) : IAnonymousCommandHandler +internal sealed class AnonymousFuncTaskCommandHandler( + ICommandObjectMetadata factory, + Func handler) : ICommandHandlerMetadata where T : notnull { - private T? _options; + public object Build(CommandRunningContext context) + { + return factory.Build(context); + } - public object? CreatedCommandOptions => _options; + public Task RunAsync(object createdCommandObject) + { + var instance = (T)createdCommandObject; + var task = handler(instance); + return task.ContinueWith(ThrowIfFaulted); + } - public async Task RunAsync() + private static int ThrowIfFaulted(Task task) { - _options ??= (T)factory(context); - if (_options is null) + if (task.IsFaulted) { - throw new InvalidOperationException($"No options of type {typeof(T)} were created."); + ExceptionDispatchInfo.Capture(task.Exception!).Throw(); } - await handler(_options); return 0; } } -internal sealed class AnonymousTaskInt32CommandHandler( - CommandRunningContext context, - CommandObjectFactory factory, - Func> handler) : IAnonymousCommandHandler +internal sealed class AnonymousFuncTaskInt32CommandHandler( + ICommandObjectMetadata factory, + Func> handler) : ICommandHandlerMetadata where T : notnull { - private T? _options; - - public object? CreatedCommandOptions => _options; + public object Build(CommandRunningContext context) + { + return factory.Build(context); + } - public Task RunAsync() + public Task RunAsync(object createdCommandObject) { - _options ??= (T)factory(context); - if (_options is null) - { - throw new InvalidOperationException($"No options of type {typeof(T)} were created."); - } - return handler(_options); + var instance = (T)createdCommandObject; + return handler(instance); } } From 899193d30a8c4170f52d31bf68f6b00519befa6c Mon Sep 17 00:00:00 2001 From: walterlv Date: Tue, 30 Sep 2025 15:22:43 +0800 Subject: [PATCH 2/4] =?UTF-8?q?=E5=88=A0=E9=99=A4=20Builder=20=E4=B8=AD?= =?UTF-8?q?=E5=88=9B=E5=BB=BA=20Parser=20=E6=97=B6=E7=9A=84=E5=9B=9B?= =?UTF-8?q?=E4=B8=AA=E5=A7=94=E6=89=98=EF=BC=8C=E5=88=86=E9=85=8D=E4=BB=8E?= =?UTF-8?q?=20888B=20=E9=99=8D=E4=BD=8E=E5=88=B0=20624B=EF=BC=9B=E8=80=97?= =?UTF-8?q?=E6=97=B6=E4=BB=8E=20303ns=20=E9=99=8D=E5=88=B0=20245ns?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Generators/ModelBuilderGenerator.cs | 20 +++----- .../Compiler/ICommandObjectMetadata.cs | 42 ++++++++++++++++ .../Utils/Parsers/CommandLineParser.cs | 50 +++++++++++-------- 3 files changed, 78 insertions(+), 34 deletions(-) diff --git a/src/DotNetCampus.CommandLine.Analyzer/Generators/ModelBuilderGenerator.cs b/src/DotNetCampus.CommandLine.Analyzer/Generators/ModelBuilderGenerator.cs index 84875410..323d61cc 100644 --- a/src/DotNetCampus.CommandLine.Analyzer/Generators/ModelBuilderGenerator.cs +++ b/src/DotNetCampus.CommandLine.Analyzer/Generators/ModelBuilderGenerator.cs @@ -69,16 +69,16 @@ private string GenerateCommandObjectCreatorCode(CommandObjectGeneratingModel mod .AddRawMembers(model.EnumeratePositionalArgumentExcludingSameNameOptions().Select(GenerateArgumentPropertyCode)) .AddRawText(GenerateBuildCode(model)) .AddMethodDeclaration( - "private global::DotNetCampus.Cli.Utils.Parsers.OptionValueMatch MatchLongOption(ReadOnlySpan longOption, bool defaultCaseSensitive, global::DotNetCampus.Cli.CommandNamingPolicy namingPolicy)", + "public global::DotNetCampus.Cli.Utils.Parsers.OptionValueMatch MatchLongOption(ReadOnlySpan longOption, bool defaultCaseSensitive, global::DotNetCampus.Cli.CommandNamingPolicy namingPolicy)", m => GenerateMatchLongOptionCode(m, model)) .AddMethodDeclaration( - "private global::DotNetCampus.Cli.Utils.Parsers.OptionValueMatch MatchShortOption(ReadOnlySpan shortOption, bool defaultCaseSensitive)", + "public global::DotNetCampus.Cli.Utils.Parsers.OptionValueMatch MatchShortOption(ReadOnlySpan shortOption, bool defaultCaseSensitive)", m => GenerateMatchShortOptionCode(m, model)) .AddMethodDeclaration( - "private global::DotNetCampus.Cli.Utils.Parsers.PositionalArgumentValueMatch MatchPositionalArguments(ReadOnlySpan value, int argumentIndex)", + "public global::DotNetCampus.Cli.Utils.Parsers.PositionalArgumentValueMatch MatchPositionalArguments(ReadOnlySpan value, int argumentIndex)", m => GenerateMatchPositionalArgumentsCode(m, model)) .AddMethodDeclaration( - "private void AssignPropertyValue(string propertyName, int propertyIndex, ReadOnlySpan key, ReadOnlySpan value)", + "public void AssignPropertyValue(string propertyName, int propertyIndex, ReadOnlySpan key, ReadOnlySpan value)", m => m .Condition(model.OptionProperties.Count > 0 || model.PositionalArgumentProperties.Count > 0, b => b .AddBracketScope("switch (propertyIndex)", l => l @@ -101,8 +101,8 @@ private static string GenerateBuilderTypeDeclarationLine(CommandObjectGenerating { var modifier = model.IsPublic ? "public" : "internal"; return model.UseFullStackParser - ? $"{modifier} partial struct {model.GetBuilderTypeName()}()" - : $"{modifier} sealed class {model.GetBuilderTypeName()}"; + ? $"{modifier} partial struct {model.GetBuilderTypeName()}() : global::DotNetCampus.Cli.Compiler.ICommandObjectBuilder // 临时继承,后面要去掉" + : $"{modifier} sealed class {model.GetBuilderTypeName()} : global::DotNetCampus.Cli.Compiler.ICommandObjectBuilder"; } private static string GenerateMetadataTypeDeclarationLine(CommandObjectGeneratingModel model) @@ -152,13 +152,7 @@ private static string GenerateBuildCode(CommandObjectGeneratingModel model) => $ return BuildDefault(context.CommandLine); } - var parser = new global::DotNetCampus.Cli.Utils.Parsers.CommandLineParser(context.CommandLine, "{{model.CommandObjectType.Name}}", {{model.GetCommandLevel()}}) - { - MatchLongOption = MatchLongOption, - MatchShortOption = MatchShortOption, - MatchPositionalArguments = MatchPositionalArguments, - AssignPropertyValue = AssignPropertyValue, - }; + var parser = new global::DotNetCampus.Cli.Utils.Parsers.CommandLineParser(context.CommandLine, this, "{{model.CommandObjectType.Name}}", {{model.GetCommandLevel()}}); parser.Parse().WithFallback(context); return BuildCore(context.CommandLine); } diff --git a/src/DotNetCampus.CommandLine/Compiler/ICommandObjectMetadata.cs b/src/DotNetCampus.CommandLine/Compiler/ICommandObjectMetadata.cs index e7991166..0bd49096 100644 --- a/src/DotNetCampus.CommandLine/Compiler/ICommandObjectMetadata.cs +++ b/src/DotNetCampus.CommandLine/Compiler/ICommandObjectMetadata.cs @@ -1,3 +1,5 @@ +using DotNetCampus.Cli.Utils.Parsers; + namespace DotNetCampus.Cli.Compiler; /// @@ -28,3 +30,43 @@ internal interface ICommandHandlerMetadata : ICommandObjectMetadata /// 命令处理器的返回值。 Task RunAsync(object createdCommandObject); } + +/// +/// 提供给源生成器生成的类型实现,用于在解析命令行的过程中不断配合赋值,最终生成完整的命令行对象。 +/// +public interface ICommandObjectBuilder +{ + /// + /// 匹配长选项。 + /// + /// 来自用户输入的命令行长选项,不包含前导的两个短横线(--)或任何其他允许的前缀。 + /// 指示此命令行对象的选项默认是否区分大小写。 + /// 命名策略,用于将用户输入的选项名称转换为属性名称。 + /// 匹配结果。 + OptionValueMatch MatchLongOption(ReadOnlySpan longOption, bool defaultCaseSensitive, CommandNamingPolicy namingPolicy); + + /// + /// 匹配短选项。 + /// + /// 来自用户输入的命令行短选项,不包含前导的单个短横线(-)或任何其他允许的前缀。 + /// 指示此命令行对象的选项默认是否区分大小写。 + /// 匹配结果。 + OptionValueMatch MatchShortOption(ReadOnlySpan shortOption, bool defaultCaseSensitive); + + /// + /// 匹配位置参数。 + /// + /// 来自用户输入的命令行位置参数。 + /// 位置参数的索引,从 0 开始。 + /// 匹配结果。 + PositionalArgumentValueMatch MatchPositionalArguments(ReadOnlySpan value, int argumentIndex); + + /// + /// 为指定的属性赋值。 + /// + /// 属性名,仅用于调试和日志记录。 + /// 属性索引,源生成器应该根据此索引快速定位到对应的属性。 + /// 如果选项是字典类型的,则为字典的键,否则为空。 + /// 要赋予属性的值;如果属性是布尔类型且选项没有显式提供值,则此值为空;如果选项是字典类型的,则为字典的值。 + void AssignPropertyValue(string propertyName, int propertyIndex, ReadOnlySpan key, ReadOnlySpan value); +} diff --git a/src/DotNetCampus.CommandLine/Utils/Parsers/CommandLineParser.cs b/src/DotNetCampus.CommandLine/Utils/Parsers/CommandLineParser.cs index 3bf1bf6d..38c6f013 100644 --- a/src/DotNetCampus.CommandLine/Utils/Parsers/CommandLineParser.cs +++ b/src/DotNetCampus.CommandLine/Utils/Parsers/CommandLineParser.cs @@ -1,3 +1,4 @@ +using System.Runtime.CompilerServices; using DotNetCampus.Cli.Compiler; using DotNetCampus.Cli.Exceptions; using Cat = DotNetCampus.Cli.Utils.Parsers.CommandArgumentType; @@ -10,6 +11,7 @@ namespace DotNetCampus.Cli.Utils.Parsers; public readonly ref struct CommandLineParser { private readonly CommandLine _commandLine; + private readonly ICommandObjectBuilder _builder; private readonly string _commandObjectName; private readonly int _commandCount; private readonly bool _caseSensitive; @@ -19,11 +21,13 @@ public readonly ref struct CommandLineParser /// 通用的命令行参数解析器。(此解析器不可解析 URL 类型的参数。) /// /// 要解析的命令行参数。 + /// 配合此解析器使用,用于将解析结果赋值给命令对象。 /// 正在解析此参数的命令对象的名称。 /// 主命令/子命令/多级子命令的数量。在解析时,要跳过这些命令。 - public CommandLineParser(CommandLine commandLine, string commandObjectName, int commandCount) + public CommandLineParser(CommandLine commandLine, ICommandObjectBuilder builder, string commandObjectName, int commandCount) { _commandLine = commandLine; + _builder = builder; _commandObjectName = commandObjectName; _commandCount = commandCount; var isUrl = commandLine.MatchedUrlScheme is not null; @@ -83,26 +87,6 @@ public CommandLineParser(CommandLine commandLine, string commandObjectName, int /// public UnknownCommandArgumentHandling UnknownArgumentsHandling { get; } - /// - /// 要求源生成器匹配长名称,返回此长选项的值类型。 - /// - public required LongOptionMatchingCallback MatchLongOption { get; init; } - - /// - /// 要求源生成器匹配短名称,返回此短选项的值类型。 - /// - public required ShortOptionMatchingCallback MatchShortOption { get; init; } - - /// - /// 要求源生成器匹配位置参数,返回位置参数的范围。 - /// - public required PositionalArgumentMatchingCallback MatchPositionalArguments { get; init; } - - /// - /// 要求源生成器将解析到的值赋值给指定索引处的属性。 - /// - public required AssignPropertyValueCallback AssignPropertyValue { get; init; } - /// /// 获取默认的选项值处理器(默认的选项处理器仅为了避免代码错误产生误用,实际永远不会被使用)。 /// @@ -444,6 +428,30 @@ private CommandLineParsingResult SplitKeyValue(ReadOnlySpan item, } return null; } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private OptionValueMatch MatchLongOption(ReadOnlySpan optionName, bool caseSensitive, CommandNamingPolicy namingPolicy) + { + return _builder.MatchLongOption(optionName, caseSensitive, namingPolicy); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private OptionValueMatch MatchShortOption(ReadOnlySpan optionName, bool caseSensitive) + { + return _builder.MatchShortOption(optionName, caseSensitive); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private PositionalArgumentValueMatch MatchPositionalArguments(ReadOnlySpan value, int positionIndex) + { + return _builder.MatchPositionalArguments(value, positionIndex); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void AssignPropertyValue(string propertyName, int propertyIndex, ReadOnlySpan key, ReadOnlySpan value) + { + _builder.AssignPropertyValue(propertyName, propertyIndex, key, value); + } } /// From 9723f6d0bb28eefcad0e15e479642306565a12e7 Mon Sep 17 00:00:00 2001 From: walterlv Date: Tue, 30 Sep 2025 15:25:42 +0800 Subject: [PATCH 3/4] =?UTF-8?q?=E5=8A=A0=E5=85=A5=20AddHandler=20=E7=9A=84?= =?UTF-8?q?=E6=80=A7=E8=83=BD=E6=B5=8B=E8=AF=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../CommandMatching/AddHandlers.cs | 48 +++++++++++++++++++ .../Fakes/CommandLineArguments.cs | 2 + 2 files changed, 50 insertions(+) create mode 100644 tests/DotNetCampus.CommandLine.Performance/CommandMatching/AddHandlers.cs diff --git a/tests/DotNetCampus.CommandLine.Performance/CommandMatching/AddHandlers.cs b/tests/DotNetCampus.CommandLine.Performance/CommandMatching/AddHandlers.cs new file mode 100644 index 00000000..ce1f6360 --- /dev/null +++ b/tests/DotNetCampus.CommandLine.Performance/CommandMatching/AddHandlers.cs @@ -0,0 +1,48 @@ +using System.Threading.Tasks; +using BenchmarkDotNet.Attributes; +using DotNetCampus.Cli.Compiler; +using DotNetCampus.Cli.Performance.Fakes; + +namespace DotNetCampus.Cli.Performance.CommandMatching; + +[MemoryDiagnoser] +[BenchmarkCategory("Add handlers")] +public class AddHandlers +{ + [Benchmark(Description = "add-handler -v=4.1 -p=dotnet")] + public void AddHandler() + { + // Arrange + var commandLine = CommandLine.Parse(CommandLineArguments.CommandArgs, Options41.DotNet); + + // Act + var result = commandLine + .AddHandler() + .AddHandler() + .RunAsync(); + var exitCode = result.Result.ExitCode; + } + + public record DefaultHandler : ICommandHandler + { + [Value(0)] + public string? Value { get; set; } = "DefaultHandler"; + + public Task RunAsync() + { + return Task.FromResult(1); + } + } + + [Command("foo")] + public record FooHandler : ICommandHandler + { + [Value(0)] + public string? Value { get; set; } = "FooHandler"; + + public Task RunAsync() + { + return Task.FromResult(2); + } + } +} diff --git a/tests/DotNetCampus.CommandLine.Performance/Fakes/CommandLineArguments.cs b/tests/DotNetCampus.CommandLine.Performance/Fakes/CommandLineArguments.cs index 56ccdf34..35141861 100644 --- a/tests/DotNetCampus.CommandLine.Performance/Fakes/CommandLineArguments.cs +++ b/tests/DotNetCampus.CommandLine.Performance/Fakes/CommandLineArguments.cs @@ -4,6 +4,8 @@ internal static class CommandLineArguments { public static readonly string[] NoArgs = []; + public static readonly string[] CommandArgs = ["foo"]; + public static readonly string[] DotNetArgs = [ "DotNetCampus.CommandLine.Performance.dll", From 77563841467659fc0b612e102592a9be9db075ad Mon Sep 17 00:00:00 2001 From: walterlv Date: Tue, 30 Sep 2025 15:28:55 +0800 Subject: [PATCH 4/4] =?UTF-8?q?=E4=BD=BF=E7=94=A8=E5=BC=82=E6=AD=A5?= =?UTF-8?q?=E6=96=B9=E6=B3=95=E8=BD=AC=E6=8D=A2=E5=91=BD=E4=BB=A4=E8=BF=94?= =?UTF-8?q?=E5=9B=9E=E5=80=BC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Utils/Handlers/TaskCommandHandler.cs | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/src/DotNetCampus.CommandLine/Utils/Handlers/TaskCommandHandler.cs b/src/DotNetCampus.CommandLine/Utils/Handlers/TaskCommandHandler.cs index 0b5dbf9a..43f3acfa 100644 --- a/src/DotNetCampus.CommandLine/Utils/Handlers/TaskCommandHandler.cs +++ b/src/DotNetCampus.CommandLine/Utils/Handlers/TaskCommandHandler.cs @@ -1,4 +1,3 @@ -using System.Runtime.ExceptionServices; using DotNetCampus.Cli.Compiler; namespace DotNetCampus.Cli.Utils.Handlers; @@ -69,15 +68,12 @@ public Task RunAsync(object createdCommandObject) { var instance = (T)createdCommandObject; var task = handler(instance); - return task.ContinueWith(ThrowIfFaulted); + return Await(task); } - private static int ThrowIfFaulted(Task task) + private static async Task Await(Task task) { - if (task.IsFaulted) - { - ExceptionDispatchInfo.Capture(task.Exception!).Throw(); - } + await task.ConfigureAwait(false); return 0; } }